ci(security): add automated security and vulnerability scans (#1297)

* ci(security): add CodeQL SAST workflow for JS/TS and Python

CodeQL analyzes both languages on PR, main push, and weekly schedule.
Findings upload to the Security tab as SARIF. Advisory only on
introduction; promote to required check after baseline triage.

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U1)

* ci(security): add Dependency Review PR gate

Blocks PRs introducing high+ severity dependency vulnerabilities.
Posts inline summary comment on failure. Required-check candidate
after one week of clean runs.

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U2)

* ci(security): add Gitleaks secret scanning

PR runs scan the diff; main pushes scan full history.
Defense-in-depth on top of GitHub native push protection
(documented as a recommended Settings toggle in SECURITY.md).

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U3)

* ci(security): add OpenSSF Scorecard workflow

Weekly + on main push. SARIF uploads to Security tab; public
badge URL resolves after first scheduled run lands.

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U4)

* ci(security): add zizmor workflow lint

Lints .github/workflows/** for known Actions security misconfigurations
(unpinned actions, dangerous interpolation, missing permissions).
Triggered only on PRs touching .github/**.

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U5)

* ci(security): add Trivy container image scanning

Builds Dockerfile.cli and Dockerfile.web, then scans images for
HIGH/CRITICAL CVEs. Findings record-only on Security tab; not
PR-blocking. Weekly schedule + main push for freshness.

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U6)

* docs(security): add SECURITY.md policy and Scorecard badge

Vulnerability disclosure policy points to GitHub Private Vulnerability
Reporting. Documents in-CI scans landed in this branch and recommended
admin actions for forks.

Plan: docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md (U7)

* fix(review): apply autofix feedback

- CodeQL paths-ignore: replace brace expansion (parser.{c,js}) with two
  explicit entries — CodeQL uses .gitignore-style globs that do NOT support
  brace expansion, so the original pattern matched no files.
- Trivy: pin aquasecurity/trivy-action from @master to @0.28.0 — mutable
  refs are a supply-chain risk and are exactly what zizmor (added in this
  same plan) is meant to flag.

ce-code-review run: /tmp/compound-engineering/ce-code-review/20260503-104259-279c3bc4/

* docs(review): record residual review findings

ce-code-review autofix run flagged three downstream-resolver items
that are not blockers but should land before promoting any of the new
security workflows to required PR checks.

Source: /tmp/compound-engineering/ce-code-review/20260503-104259-279c3bc4/

* fix(ci-security): address all zizmor + dependency-review violations

Resolves all GitHub Advanced Security findings on PR #1297:

- Add 'persist-credentials: false' to actions/checkout in 5 workflows
  (codeql, dependency-review, gitleaks, trivy, workflow-lint). Prevents
  the GITHUB_TOKEN from persisting in .git/config for downstream steps
  to read. Scorecard already had it.
- Pin every net-new third-party Action to a commit SHA (was: major-tag
  refs flagged by zizmor as 'unpinned action reference'):
    github/codeql-action -> v3.35.3 (0daab03)
    actions/dependency-review-action -> v4.9.0 (2031cfc)
    gitleaks/gitleaks-action -> v2.3.9 (ff98106)
    ossf/scorecard-action -> v2.4.3 (4eaacf0)
    docker/build-push-action -> v6.19.2 (10e90e3)
- Bump aquasecurity/trivy-action 0.28.0 -> 0.36.0 (ed142fd). Versions
  < 0.35.0 are flagged by GHSA-69fq-xp46-6x23 (briefly compromised
  supply chain). Caught by Dependency Review on the introducing PR.
- Pin pipx-installed zizmor to 1.24.1 (was unpinned 'pipx install
  zizmor' resolving to latest at run time).

Removes the now-stale residual-findings doc since every item it
recorded is resolved on this branch.

* fix(ci-security): clear remaining zizmor findings

After landing the new security workflows, zizmor reported 5 high+
findings against pre-existing workflows (none introduced by this PR's
new files, all introduced by zizmor's wider scope). Resolved per
research at docs.zizmor.sh and PyO3/maturin issue #2425:

Real fixes (cache-poisoning):
- publish.yml + release-candidate.yml: add 'package-manager-cache:
  false' to actions/setup-node. setup-node v5+ enables caching by
  default when a packageManager field is present in package.json;
  explicit opt-out keeps release installs hermetic and clears the
  audit. Cost: ~30s slower per release run.

Documented exemptions (dangerous-triggers, .github/zizmor.yml):
- ci-report.yml: workflow_run is REQUIRED to post sticky comments
  on fork PRs (forks have read-only GITHUB_TOKEN on pull_request).
- claude.yml: pull_request_target is required by claude-code-action
  to access secrets and post fork-PR review comments. PR checkouts
  pin fork HEAD SHA to mitigate TOCTOU.
- pr-labeler.yml: pull_request_target on the autolabel job needs
  pull-requests:write. release-drafter runs with dry-run:true and
  reads config from the BASE ref only.

Each exemption carries the documented mitigation in zizmor.yml.
workflow-lint.yml now passes --config to both the SARIF and the
gate invocations.

Local 'zizmor --config .github/zizmor.yml --min-severity high .'
reports: No findings to report. Good job!
This commit is contained in:
Gergő Magyar 2026-05-04 08:21:53 +01:00 • committed by GitHub
parent e2b0b7a27f
commit b6bbede07b
11 changed files with 452 additions and 4 deletions

67
.github/workflows/codeql.yml vendored Normal file
View file

@ -0,0 +1,67 @@
name: CodeQL
# Static analysis (SAST) for TypeScript/JavaScript and Python sources.
# Findings upload to the GitHub Security tab as SARIF.
#
# Advisory only on first introduction — see docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md.
# Promote to a required check after baseline triage (operator decision).
on:
pull_request:
branches: [main]
paths-ignore: ['**.md', 'docs/**', 'LICENSE']
push:
branches: [main]
schedule:
# Weekly Monday 06:00 UTC — catches advisories newly published against
# already-merged code without waiting for the next PR.
- cron: '0 6 * * 1'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
actions: read
contents: read
# security-events:write is what enables SARIF upload to the Security tab.
security-events: write
strategy:
fail-fast: false
matrix:
language: [javascript-typescript, python]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Don't leave GITHUB_TOKEN in .git/config for downstream steps to read.
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@0daab03d71ff584ef619d027a3fd9146679c5d84 # v3.35.3
with:
languages: ${{ matrix.language }}
queries: security-and-quality
# Exclude generated/vendored code; tune after first-run signal.
# gitnexus/vendor/ holds tree-sitter-proto sources (regenerated, not authored).
# CodeQL path filters use .gitignore-style globs and do NOT support
# brace expansion — list each generated parser file separately.
config: |
paths-ignore:
- '**/dist/**'
- '**/node_modules/**'
- 'gitnexus/vendor/**'
- 'gitnexus/src/core/parsing/**/parser.c'
- 'gitnexus/src/core/parsing/**/parser.js'
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0daab03d71ff584ef619d027a3fd9146679c5d84 # v3.35.3
with:
category: '/language:${{ matrix.language }}'

36
.github/workflows/dependency-review.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Dependency Review
# Blocks PRs that introduce dependencies with high/critical known vulnerabilities.
# Reads the dependency graph diff between PR head and base.
#
# This is a required-check candidate after one week of clean runs
# (operator decision — see docs/plans/2026-05-03-001-feat-automated-security-scans-plan.md).
on:
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
# pull-requests:write enables the inline summary comment on failure.
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Dependency Review
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
with:
fail-on-severity: high
comment-summary-in-pr: on-failure

45
.github/workflows/gitleaks.yml vendored Normal file
View file

@ -0,0 +1,45 @@
name: Gitleaks
# Deterministic in-CI secret scanning. Defense-in-depth on top of GitHub's
# native secret-scanning push protection (which is a repo Settings toggle —
# see SECURITY.md for the recommended admin action).
#
# PR runs scan the diff (fast); main pushes scan full history.
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
gitleaks:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Full history needed for the on-push full-history scan; on PRs the
# action diffs against the base ref so the cost is bounded by the PR.
fetch-depth: 0
# Don't bake the token into the cloned .git/config; downstream
# steps (and Gitleaks itself) don't need it for repo operations.
persist-credentials: false
# No GITLEAKS_LICENSE secret is required for OSS / public-repo usage.
# If this repo becomes private, the action will require a license key.
- name: Gitleaks
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_ENABLE_UPLOAD_ARTIFACT: true
GITLEAKS_ENABLE_SUMMARY: true

View file

@ -37,8 +37,12 @@ jobs:
with:
node-version: 20
registry-url: https://registry.npmjs.org
cache: npm
cache-dependency-path: gitnexus/package-lock.json
# Hermetic install for the published artifact — no cache carry-over
# from non-tag contexts. setup-node v5+ caches by default when a
# packageManager field is present in package.json, so the explicit
# opt-out is required to clear the zizmor cache-poisoning audit.
# ~30s slower per release; runs rarely.
package-manager-cache: false
- name: Build gitnexus-shared
run: npm install && npm run build
working-directory: gitnexus-shared

View file

@ -137,8 +137,11 @@ jobs:
with:
node-version: 20
registry-url: https://registry.npmjs.org
cache: npm
cache-dependency-path: gitnexus/package-lock.json
# Hermetic install — release-candidate produces shipped artifacts.
# setup-node v5+ caches by default when a packageManager field is
# present in package.json; explicit opt-out is required to clear
# the zizmor cache-poisoning audit. See cache-poisoning audit.
package-manager-cache: false
- name: Build gitnexus-shared
run: npm install && npm run build

58
.github/workflows/scorecard.yml vendored Normal file
View file

@ -0,0 +1,58 @@
name: Scorecard
# OpenSSF Scorecard supply-chain posture check. Runs weekly + on main push +
# branch_protection_rule changes. SARIF uploads to the Security tab; the public
# badge URL resolves once the first scheduled run lands (see README badge wiring).
on:
branch_protection_rule:
schedule:
- cron: '0 7 * * 1'
push:
branches: [main]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
# Needed to upload SARIF results to the Security tab.
security-events: write
# Needed for the publish_results badge flow (OIDC).
id-token: write
contents: read
actions: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run Scorecard
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
# publish_results enables the public Scorecard badge.
publish_results: true
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: Upload to Security tab
uses: github/codeql-action/upload-sarif@0daab03d71ff584ef619d027a3fd9146679c5d84 # v3.35.3
with:
sarif_file: results.sarif

73
.github/workflows/trivy.yml vendored Normal file
View file

@ -0,0 +1,73 @@
name: Trivy Image Scan
# Builds Dockerfile.cli and Dockerfile.web, then scans the resulting images
# for OS-package and language-package CVEs at HIGH/CRITICAL severity.
# Findings upload to the Security tab; record-only (does not block merges).
#
# NOT triggered on PRs — image builds are slow and base-image CVE churn
# shouldn't gate feature delivery.
on:
push:
branches: [main]
schedule:
- cron: '0 8 * * 1'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
scan:
name: Trivy (${{ matrix.image.name }})
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
image:
- { dockerfile: Dockerfile.cli, name: gitnexus-cli }
- { dockerfile: Dockerfile.web, name: gitnexus-web }
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build image (load locally for scan)
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: ${{ matrix.image.dockerfile }}
load: true
push: false
tags: scan-target:${{ matrix.image.name }}
# aquasecurity/trivy-action versions < 0.35.0 are flagged by
# GHSA-69fq-xp46-6x23 (briefly compromised supply chain). Pinned to
# v0.36.0 (post-incident clean release) by commit SHA.
- name: Run Trivy
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: scan-target:${{ matrix.image.name }}
format: sarif
output: trivy-${{ matrix.image.name }}.sarif
severity: HIGH,CRITICAL
# Hides CVEs with no available fix in the base image.
ignore-unfixed: true
exit-code: '0'
- name: Upload to Security tab
uses: github/codeql-action/upload-sarif@0daab03d71ff584ef619d027a3fd9146679c5d84 # v3.35.3
with:
sarif_file: trivy-${{ matrix.image.name }}.sarif
category: trivy-${{ matrix.image.name }}

58
.github/workflows/workflow-lint.yml vendored Normal file
View file

@ -0,0 +1,58 @@
name: Workflow Lint (zizmor)
# Lints .github/workflows/** for known GitHub Actions security misconfigurations:
# unpinned Actions, dangerous ${{ ... }} interpolation in run: blocks,
# missing per-job permissions:, etc.
#
# Scoped to PRs that touch .github/** only — keeps off the typical PR critical path.
on:
pull_request:
branches: [main]
paths:
- '.github/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
zizmor:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
security-events: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.12'
- name: Install zizmor
# Pinned — resolves to whatever's latest on PyPI otherwise.
# Bump via Dependabot pip ecosystem (see .github/dependabot.yml).
run: pipx install zizmor==1.24.1
# Initial threshold: medium. High+ findings fail the job; medium findings
# appear in the Security tab without blocking. Tune after first run.
# Per-rule exemptions for pre-existing intentional patterns live in
# .github/zizmor.yml (each carries a documented mitigation).
- name: Run zizmor
run: zizmor --config .github/zizmor.yml --format sarif --min-severity medium . > zizmor.sarif
continue-on-error: true
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@0daab03d71ff584ef619d027a3fd9146679c5d84 # v3.35.3
with:
sarif_file: zizmor.sarif
category: zizmor
- name: Fail on high+ findings
run: zizmor --config .github/zizmor.yml --min-severity high .

34
.github/zizmor.yml vendored Normal file
View file

@ -0,0 +1,34 @@
# zizmor config — pre-existing intentional patterns flagged on initial introduction.
# Each ignore below has a documented mitigation. Re-evaluate when the source workflow changes.
#
# To run zizmor locally with this config:
# zizmor --config .github/zizmor.yml .
rules:
dangerous-triggers:
ignore:
# workflow_run is REQUIRED to post sticky comments on fork PRs — the
# default-branch privileged token isn't accessible from `pull_request`
# on a fork. Mitigated by: read-only `actions:read` + `contents:read`
# for artifact download; `pull-requests:write` is the only write scope;
# no checkout of fork code occurs. Header comment in the file documents.
- ci-report.yml
# pull_request_target needed by claude-code-action to access secrets
# and post review comments on fork PRs. Mitigated by: PR checkouts pin
# the fork's HEAD SHA (not the branch ref) to prevent TOCTOU races,
# and claude-code-action sandboxes execution. Header comment documents.
- claude.yml
# pull_request_target on the autolabel job needs `pull-requests:write`
# to apply labels. Mitigated by: release-drafter runs with `dry-run:
# true`, reads only `.github/release-drafter.yml` from the BASE ref,
# and the validate-title job (which runs untrusted `pull_request`
# context) holds no write permissions. Header comment documents.
- pr-labeler.yml
# Note: cache-poisoning is NOT exempted. The two prior findings in
# publish.yml and release-candidate.yml were fixed structurally by
# dropping `cache: npm` from those workflows (matches the pattern used
# by PyO3/maturin for the same audit). See the commit that added this
# file for the rationale.

View file

@ -18,6 +18,9 @@
<a href="https://polyformproject.org/licenses/noncommercial/1.0.0/">
<img src="https://img.shields.io/badge/License-PolyForm%20Noncommercial-blue.svg" alt="License: PolyForm Noncommercial"/>
</a>
<a href="https://securityscorecards.dev/viewer/?uri=github.com/abhigyanpatwari/GitNexus">
<img src="https://api.securityscorecards.dev/projects/github.com/abhigyanpatwari/GitNexus/badge" alt="OpenSSF Scorecard"/>
</a>
<p><strong>Enterprise (SaaS & Self-hosted)</strong> - <a href="https://akonlabs.com">akonlabs.com</a></p>

67
SECURITY.md Normal file
View file

@ -0,0 +1,67 @@
# Security Policy
## Supported Versions
GitNexus is developed on `main`. Security fixes are applied to the latest released minor on npm (`gitnexus`) and to the published Docker images (`Dockerfile.cli`, `Dockerfile.web`). Older minors are not back-patched.
## Reporting a Vulnerability
**Please do not open a public GitHub issue for security reports.**
Use **GitHub Private Vulnerability Reporting** for this repository:
→ https://github.com/abhigyanpatwari/GitNexus/security/advisories/new
Please include:
- A description of the issue and its potential impact
- Steps to reproduce (a minimal repro repo or commit hash if possible)
- The affected version(s) — `npm view gitnexus version`, image digest, or commit SHA
- Any suggested mitigation
### What to expect
- **Acknowledgement:** best-effort within 5 business days, subject to maintainer capacity.
- **Triage:** we will confirm whether the report is in scope, request clarifications if needed, and propose a fix timeline.
- **Disclosure:** coordinated. We will agree on a disclosure date with you before publishing an advisory.
### Scope
In scope:
- The `gitnexus` CLI and MCP server (`gitnexus/`)
- The `gitnexus-web` thin client (`gitnexus-web/`)
- The `gitnexus-shared` types package (`gitnexus-shared/`)
- The published Docker images (`Dockerfile.cli`, `Dockerfile.web`)
- GitHub Actions workflows in `.github/workflows/`
Out of scope:
- Vulnerabilities in third-party dependencies that we have no influence over (please report upstream; if a viable mitigation exists at the GitNexus layer, that's in scope).
- Issues requiring physical access to a developer machine or a compromised local environment.
- Theoretical attacks without a practical exploit against a default GitNexus deployment.
## Recommended Hardening for Forks and Self-Hosted Deployments
If you fork GitNexus or self-host it, we recommend enabling the following in your repository's **Settings → Code security and analysis**:
- **Private vulnerability reporting** — the channel described above.
- **Dependabot alerts** — alerts on advisories affecting your dependencies.
- **Dependabot security updates** — automated PRs for security patches (this repo's `.github/dependabot.yml` already covers version updates).
- **Secret scanning** and **Push protection** — blocks pushes that introduce known secret patterns. Defense-in-depth on top of the in-CI Gitleaks scan documented below.
- **Code scanning** — surfaces SARIF results from CodeQL, Trivy, Scorecard, and zizmor in one place.
## Automated Scans Running in CI
This repository runs the following scans automatically. Findings appear under the repository's **Security → Code scanning** tab.
| Scan | Tool | Trigger | Action on finding |
|------|------|---------|-------------------|
| Static analysis (JS/TS, Python) | [CodeQL](https://github.com/github/codeql-action) | PR, `main` push, weekly | Advisory (Security tab) |
| Dependency vulnerabilities (PR diff) | [`dependency-review-action`](https://github.com/actions/dependency-review-action) | PR | **Blocks PR** at `high+` severity |
| Secret scanning | [Gitleaks](https://github.com/gitleaks/gitleaks-action) | PR, `main` push | **Blocks PR** on default rules |
| Supply-chain posture | [OpenSSF Scorecard](https://github.com/ossf/scorecard-action) | Weekly, `main` push | Advisory (Security tab + public badge) |
| Workflow lint | [zizmor](https://github.com/woodruffw/zizmor) | PR (touching `.github/**`) | **Blocks PR** at `high+` severity |
| Container image scan | [Trivy](https://github.com/aquasecurity/trivy-action) | Weekly, `main` push | Advisory (Security tab) |
Dependency version updates are managed separately by Dependabot — see `.github/dependabot.yml`.