Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 105 additions & 4 deletions .github/workflows/_osv-scan.yml
Original file line number Diff line number Diff line change
@@ -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
# <owner>/.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:
Expand All @@ -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
Expand Down