From c7418a612f7bafb3e3c043cfc6122051ebf4b2ee Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans-personal@users.noreply.github.com> Date: Fri, 26 Jun 2026 18:38:37 -0400 Subject: [PATCH] fix(osv): make PR gate differential so pre-existing debt can't block PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reusable _osv-scan.yml ran a full repo scan on every trigger and failed on ANY finding. Because it is wired as a required PR gate (e.g. nix-ai ci-gate.yml, `if: always()`, not in the Merge Gate allowed-skips), a single open advisory on a transitive dependency anywhere in the tree red-flagged EVERY open PR — including unrelated ones that only touch flake.lock and can never clear that debt. Split into two modes by trigger: - pull_request -> DIFFERENTIAL. Scan the base ref and the head ref and fail ONLY on vulnerabilities the PR introduces, using the official osv-scanner / osv-reporter diff pattern (osv-scanner-reusable-pr.yml). Pre-existing base findings no longer block the PR. - push / schedule / other -> FULL scan (previous behavior), so the default branch still surfaces accumulated/drift debt. Callers that run this on `push:`/`schedule:` (docs/osv-scan.yml) are unaffected. Central-vs-local osv-scanner.toml resolution is unchanged and applies in both modes (and to both refs in differential mode — the file persists across the in-place ref switches). Action digests pinned to v2.3.8 (9a49870), matching the existing scanner pin. Note: docs/osv-scan.yml pins this workflow to a SHA, so it is unaffected until intentionally bumped; nix-ai and mlx-benchmarks track @main and get the change. Assisted-by: Claude:claude-opus-4-8[1m] Claude-Session: https://claude.ai/code/session_01CYau7MWswJikoctB9MUgcZ --- .github/workflows/_osv-scan.yml | 109 ++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 4 deletions(-) diff --git a/.github/workflows/_osv-scan.yml b/.github/workflows/_osv-scan.yml index 250028b..255dae9 100644 --- a/.github/workflows/_osv-scan.yml +++ b/.github/workflows/_osv-scan.yml @@ -1,8 +1,30 @@ # Multi-ecosystem lockfile vulnerability scanning via Google OSV # -# Reusable workflow — called from repo-level ci-gate.yml files. -# Scans lockfiles (uv.lock, package-lock.json, .terraform.lock.hcl, etc.) -# for known vulnerabilities across all supported ecosystems. +# Reusable workflow — called from repo-level ci-gate.yml / osv-scan.yml files. +# Scans lockfiles (uv.lock, package-lock.json, .terraform.lock.hcl, etc.) for +# known vulnerabilities across all supported ecosystems. +# +# Two modes, selected by trigger so a PR is never blocked by debt it did not +# introduce: +# +# pull_request -> DIFFERENTIAL. Scans the base ref and the head ref and fails +# ONLY on vulnerabilities the PR introduces. Pre-existing +# findings on the base branch do not block the PR. This is the +# fix for "unrelated PRs (e.g. lock-file maintenance that only +# touches flake.lock) fail because a transitive dep elsewhere +# has an open advisory". Uses the official osv-scanner / +# osv-reporter diff pattern (osv-scanner-reusable-pr.yml). +# +# push/schedule -> FULL. Scans the whole tree and fails on any finding, so the +# default branch still surfaces accumulated/drift debt (new +# advisories can land against unchanged deps). Callers that +# run this on `push:` / `schedule:` (e.g. docs/osv-scan.yml) +# keep their existing behavior unchanged. +# +# Config: a repo-local osv-scanner.toml wins; otherwise the org-wide default at +# /.github/osv-scanner.toml is fetched. The same resolution applies in +# both modes (and to both refs in differential mode, since the file persists +# across the in-place ref switches). name: _osv-scan on: @@ -25,8 +47,87 @@ permissions: contents: read jobs: - osv-scan: + # ========================================================================== + # PULL REQUEST — differential scan (fail only on introduced vulnerabilities) + # ========================================================================== + pr-diff: name: OSV Scanner + if: github.event_name == 'pull_request' + runs-on: ${{ inputs.runner_label }} + steps: + # Full history + persisted credentials so the in-place base/head ref + # switches below work. + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Fetch central OSV config + if: hashFiles('osv-scanner.toml') == '' + uses: actions/checkout@v6 + with: + repository: ${{ github.repository_owner }}/.github + path: .central-config + sparse-checkout: osv-scanner.toml + sparse-checkout-cone-mode: false + + - name: Apply config (local overrides central) + id: config + run: | + if [ -f osv-scanner.toml ]; then + echo "args=--config=osv-scanner.toml" >> "$GITHUB_OUTPUT" + elif [ -f .central-config/osv-scanner.toml ]; then + cp .central-config/osv-scanner.toml osv-scanner.toml + echo "args=--config=osv-scanner.toml" >> "$GITHUB_OUTPUT" + else + echo "args=" >> "$GITHUB_OUTPUT" + fi + + - name: Check out base ref + run: git checkout "$GITHUB_BASE_REF" + + - name: Scan base ref + uses: google/osv-scanner-action/osv-scanner-action@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8 + continue-on-error: true + with: + scan-args: |- + --format=json + --output=old-results.json + ${{ steps.config.outputs.args }} + --recursive + . + + - name: Check out head ref + # -f: the scanner left no tracked changes, but the locally-copied central + # osv-scanner.toml and old-results.json are untracked and survive this. + run: git checkout -f "$GITHUB_SHA" + + - name: Scan head ref + uses: google/osv-scanner-action/osv-scanner-action@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8 + continue-on-error: true + with: + scan-args: |- + --format=json + --output=new-results.json + ${{ steps.config.outputs.args }} + --recursive + . + + - name: Report new vulnerabilities (fail on introduced) + uses: google/osv-scanner-action/osv-reporter-action@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8 + with: + scan-args: |- + --output=results.sarif + --old=old-results.json + --new=new-results.json + --gh-annotations=true + --fail-on-vuln=true + + # ========================================================================== + # PUSH / SCHEDULE / OTHER — full scan (fail on any finding; surfaces drift) + # ========================================================================== + full: + name: OSV Scanner (full) + if: github.event_name != 'pull_request' runs-on: ${{ inputs.runner_label }} steps: - uses: actions/checkout@v6