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