dependency-vulnerability-scanning

Use when a repo needs dependency CVE scanning (SCA) in CI, a team is choosing between native audits (npm audit, pip-audit, cargo audit), OSV-Scanner and Snyk, or findings need triage. Scans lockfiles with thresholds, checks packages were seen, outputs SARIF and allows only expiring ignores.

/plugin install qa-security-scanning@testland-qa

First time in Claude Code? Add the marketplace with /plugin marketplace add testland/qa

View source (opens in a new tab)

Overview

Software composition analysis (SCA) matches the dependencies a repo declares (lockfiles, manifests, SBOMs) against vulnerability advisory databases. Each scanner family reads a different database, so their coverage differs:

FamilyDatabaseCost
Native audits (npm audit, pip-audit, cargo audit, bundle-audit, Dependency-Check)The ecosystem's own advisory feed (npm, PyPA, RustSec, RubySec, NVD)Free; usually already installed
OSV-ScannerOSV.dev, aggregated across ecosystems (OSV-Scanner usage (opens in new window))Free, open source
SnykSnyk's own database, plus snyk monitor alerts (Snyk CLI (opens in new window))Commercial license

The workflow below is the same for all three; the per-tool detail is in the references.

When to use

  • A repo has no dependency scanning in CI yet.
  • The team must choose between native audits, OSV-Scanner and Snyk, or layer two of them.
  • A scanner reports findings and someone wants to suppress, waive or gate on them.

Step 1 - Pick the scanner

Repo situationStart withReference
One ecosystem, fastest first gate, nothing extra to installThat ecosystem's native auditreferences/native-audits.md
Several ecosystems, open source only, or an SBOM as inputOSV-Scannerreferences/osv-scanner.md
A Snyk license, or alerts on new CVEs against pinned versionsSnykreferences/snyk.md

For full coverage run one native audit plus one cross-ecosystem scanner (OSV-Scanner or Snyk): each database misses advisories the others carry.

Step 2 - Scan the committed lockfile

Scan lockfiles, not loose manifests: a manifest range resolves to whatever is newest today, a lockfile is what actually ships.

npm audit --audit-level=high                     # npm; also yarn npm audit / pnpm audit
pip-audit -r requirements.txt                    # Python
cargo audit                                      # Rust
bundle-audit check --update                      # Ruby; --update refreshes the advisory DB
osv-scanner scan -L package-lock.json            # one lockfile
osv-scanner scan -r ./                           # every lockfile and SBOM under ./
snyk test --all-projects --severity-threshold=high

SBOM input: OSV-Scanner picks up SBOMs whose names follow the SPDX or CycloneDX patterns (*.spdx.json, bom.json, *.cdx.json) and scans one directly with osv-scanner scan source -L my-app.cdx.json; the components need Package URLs (OSV-Scanner source scanning (opens in new window)). Snyk scans one with snyk sbom test --file=my-app.cdx.json (CycloneDX 1.4-1.6 JSON or SPDX 2.3 JSON, Enterprise plans) (Snyk sbom test (opens in new window)). syft-generation produces the SBOM.

Step 3 - Check the scan saw packages

A scan that found no lockfile passes silently. Before trusting a green result, confirm the scanner reported the packages you expect.

OSV-Scanner makes this explicit with its exit codes (OSV-Scanner output (opens in new window)):

Exit codeMeaningCI action
0Packages found, no known vulnerabilitiesPass
1Packages found, vulnerabilities foundFail (or triage, Step 5)
127General errorFail
128No packages foundFail: the scan did nothing; fix the path or lockfile name

For the other scanners, read the JSON report's dependency count, or run once against a branch with a known-vulnerable pin and confirm the job goes red.

Step 4 - Emit machine-readable output

osv-scanner scan -r ./ --format sarif --output-file osv.sarif
snyk test --sarif-file-output=snyk.sarif
pip-audit --format sarif --output pip-audit.sarif
npm audit --json > npm-audit.json

--format and --output-file are OSV-Scanner's output flags (OSV-Scanner usage (opens in new window)); the container image takes the same arguments (installation (opens in new window)). Upload SARIF to code scanning, and pass two or more reports to multi-tool-finding-triage (or the security-finding-triager agent) to dedupe findings across scanners.

Step 5 - False-positive triage

Suppress a finding only in a committed file, per advisory, with a reason, an approver and an expiry date, so it comes back for review:

ScannerSuppressionExpiry
Snyk.snyk ignore: entryexpires: (required)
OSV-Scannerosv-scanner.toml [[IgnoredVulns]]ignoreUntil
pip-audit--ignore-vuln <id>Record in a committed ignore list
Dependency-Checkdependency-check-suppressions.xml<suppress until="2026-12-31Z"> (suppression docs (opens in new window))
cargo audit.cargo/audit.toml [advisories] ignoreRecord in the justification
bundler-audit.bundler-audit.yml ignore:Record in the justification

Before suppressing as "not exploitable", prove it: reachability-analyzer checks whether the vulnerable function is called, and cve-exploitability-triage weighs EPSS and KEV. A severity threshold (--audit-level, --severity-threshold) is a scan filter, not a suppression. Templates per tool: references/snyk-policy-and-triage.md, references/osv-scanner-config-and-ci.md, references/ecosystem-config-and-triage.md, references/cargo-bundle-audit.md.

Step 6 - Gate CI

jobs:
  sca:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - if: hashFiles('package-lock.json') != ''
        run: npm audit --audit-level=high
      - if: hashFiles('requirements.txt') != ''
        run: pipx run pip-audit -r requirements.txt
      - name: OSV-Scanner (fails on findings and on exit 128)
        run: |
          docker run --rm -v "$PWD:/src" ghcr.io/google/osv-scanner:latest \
            scan -r /src --format sarif --output-file /src/osv.sarif
      - if: always()
        uses: github/codeql-action/upload-sarif@v3
        with: { sarif_file: osv.sarif }

With a Snyk license, add snyk test --severity-threshold=high on pull requests and snyk monitor on the default branch, which snapshots the dependency tree and alerts on CVEs disclosed later (Snyk CLI (opens in new window)); the full job is in references/snyk.md.

Worked example

A Node + Python service has no SCA. Step 1: two ecosystems, no Snyk license, so npm audit and pip-audit as the fast gate plus OSV-Scanner for cross-ecosystem coverage. The first OSV run exits 128: the Python lockfile is requirements-lock.txt, a name OSV-Scanner does not detect, so the job is pointed at it with -L requirements-lock.txt and now exits 1 with two findings. One is a lodash advisory in a function the service never calls; reachability-analyzer confirms it, and an [[IgnoredVulns]] entry with ignoreUntil 90 days out and the approver's name goes into osv-scanner.toml. The other is upgraded. The job goes green, and the ignore resurfaces when it expires.

Anti-patterns

Anti-patternWhy it failsFix
No severity threshold on the first runLow-severity noise; the team turns the job off--audit-level=high / --severity-threshold=high
Treating any non-1 exit as greenOSV exit 128 means nothing was scannedFail on 128 (Step 3)
Suppression with no expiryDebt never comes back for reviewexpires: / ignoreUntil (Step 5)
One scanner onlyEach advisory database has gapsNative audit plus OSV-Scanner or Snyk (Step 1)
npm audit fix --force in CISilently bumps major versionsReview forced upgrades by hand
Token in scriptsLeaks SNYK_TOKENCI secret

Limitations

  • No scanner here does reachability: every advisory on a declared dependency counts, used or not. Use reachability-analyzer.
  • Coverage differs by ecosystem in every database.
  • Container images need trivy-image or the Grype flow in syft-generation.
  • Automated upgrade PRs are dependabot-config and renovate-config.

References

Reference

cargo audit (Rust) + bundle-audit (Ruby) - deep reference

View source (opens in a new tab) (opens in new window)

Companion reference for native-audits.md (opens in new window) Steps 4-5. That file keeps the one-command scans; consult this file for committed suppression files, exit-code semantics, SARIF output, binary auditing, and Rake / GitHub Actions wiring.

cargo audit (Rust)

cargo-audit scans Cargo.lock against the RustSec Advisory Database (rustsec.org/advisories) for vulnerable, unmaintained, unsound, and yanked crates. Minimum Rust version: 1.74 per rustsec-readme (opens in new window).

cargo install cargo-audit --locked
# enable the `cargo audit fix` subcommand:
cargo install cargo-audit --features=fix --locked

cargo audit                       # run at the workspace root (Cargo.lock)
cargo audit -f path/to/Cargo.lock # explicit lockfile path
cargo audit --no-fetch            # skip advisory-db fetch (air-gapped)

First run clones the advisory DB into ~/.cargo/advisory-db (rustsec-readme (opens in new window)).

Exit codes and --deny flags

CodeMeaning
0No vulnerabilities / denial criteria not triggered
1Vulnerabilities found matching denial criteria
2Execution error (missing lockfile, DB fetch failure)

--deny turns advisory categories into hard failures; --deny warnings is the catch-all that enables all denial categories (cargo-audit audit.rs (opens in new window)):

cargo audit --deny warnings                      # fail on any vulnerability
cargo audit --deny unmaintained --deny unsound   # per-category hard fail
cargo audit --deny yanked

Output formats

--format supports terminal (default), json, and sarif (rustsec-readme (opens in new window)):

cargo audit --format json > cargo-audit.json
cargo audit --format sarif > cargo-audit.sarif   # GitHub Code Scanning upload

.cargo/audit.toml suppression

Persistent suppression belongs in a committed .cargo/audit.toml, not CLI --ignore flags (not auditable in git). Per the audit.toml example (opens in new window):

# .cargo/audit.toml
[advisories]
ignore = ["RUSTSEC-2024-0999"]
# RUSTSEC-2024-0999: serde_cbor unmaintained; test fixtures only.
# Approved-by: alice@example.com  Re-review-date: 2026-09-30
informational_warnings = ["unmaintained", "unsound"]
severity_threshold = "medium"     # none | low | medium | high | critical

[output]
format = "terminal"               # terminal | json | sarif
deny = ["warnings"]               # mirrors --deny flags
show_tree = true

[database]
fetch = false                     # offline / air-gapped builds
stale = false

cargo audit fix

cargo audit fix --dry-run   # preview
cargo audit fix             # update Cargo.toml constraints + cargo update

cargo audit fix is experimental per rustsec-readme (opens in new window); it cannot resolve conflicting version constraints. Always run cargo test after.

Binary auditing

cargo install cargo-auditable
cargo auditable build --release          # embeds Cargo.lock metadata
cargo audit bin target/release/my-app    # audit the compiled binary

Binaries built without cargo-auditable have no embedded metadata and cannot be audited (rustsec-readme (opens in new window)).

GitHub Actions

The official rustsec/audit-check (opens in new window) action wraps cargo audit, fails the check run on any security advisory, and opens a GitHub Issue per advisory on scheduled runs:

on:
  push:
    paths: ['**/Cargo.toml', '**/Cargo.lock']
  schedule:
    - cron: '0 0 * * *'
jobs:
  security_audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: rustsec/audit-check@v2.0.0
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

SARIF upload alongside:

      - run: cargo audit --format sarif > cargo-audit.sarif || true
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: cargo-audit.sarif }

Rust-specific limitations

  • RustSec DB covers crates.io; vendored / path-dependency crates are not covered.
  • Library crates must commit Cargo.lock (or cargo generate-lockfile in CI) or there is nothing to scan.
  • OSV.dev imports RustSec advisories in real time; pair with osv-scanner.md (opens in new window) for cross-DB consensus.

bundle-audit (Ruby)

bundler-audit (ba-readme (opens in new window)) scans Gemfile.lock against the ruby-advisory-db (opens in new window) - a community-maintained YAML corpus of CVE / GHSA / OSVDB advisories under gems/ (per-gem) and rubies/ (runtimes). It checks two classes of issues: vulnerable gem versions and insecure sources (http:// / git:// URIs).

gem install bundler-audit
bundle-audit update              # one-time advisory-db clone/sync
bundle-audit check --update      # refresh + scan (always --update in CI)
bundle-audit check --no-update   # fully offline once the DB is synced

Output flags (ba-readme (opens in new window)):

FlagOutput
--format jsonJSON for multi-tool SCA triage
--output FILEWrite to file (CI artifact)
--gemfile-lock PATHNon-default lockfile path

.bundler-audit.yml waivers

Committed suppression file at the project root (ba-readme (opens in new window)); the ignore array takes CVE / GHSA / OSVDB identifiers. Every ignore MUST carry an inline justification - reachability finding, approver, re-review date; reviewers treat undocumented ignores as unapproved:

---
# Suppressions last reviewed: 2026-06-04  Re-review by: 2026-09-04
ignore:
  # CVE-2024-1234: vulnerable function not reachable; foo-gem used only in
  # test fixtures. Verified via grep + code review. Approved: alice@example.com
  - CVE-2024-1234

Per-run --ignore CVE-2024-1234 flags are for temporary workarounds only; they have no expiry mechanism - enforce a quarterly review cadence in process.

Rake integration

Per ba-readme (opens in new window):

require 'bundler/audit/task'
Bundler::Audit::Task.new
task default: %w[spec bundle:audit]   # audit gates the local test run

Adds rake bundle:audit and rake bundle:audit:update; bundle:audit exits non-zero on findings.

CI gating

bundle-audit check exits 0 clean, non-zero on findings (ba-readme (opens in new window)):

jobs:
  bundle-audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with: { bundler-cache: true }
      - run: gem install bundler-audit
      - run: bundle-audit check --update --format json --output bundle-audit.json
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: bundle-audit, path: bundle-audit.json }

Ruby-specific limitations

  • Reads Gemfile.lock only; gems loaded outside Bundler are invisible.
  • ruby-advisory-db is community-maintained; unfiled advisories are absent.
  • Pair with osv-scanner.md (opens in new window) for OSV.dev cross-DB coverage.
Reference

Per-ecosystem audit config, triage, and aggregation

View source (opens in a new tab) (opens in new window)

Deep reference for native-audits.md (opens in new window). That file keeps the primary scan command per ecosystem and the CI hashFiles gate; this file holds the speed/coverage tradeoffs, the Maven plugin block, the full suppression templates, and output aggregation.

Native audit vs unified scanner tradeoffs

PropertyNative auditSnyk / OSV
Speed<5s typical10s - 60s
DB coveragePer-ecosystem onlyCross-ecosystem aggregated
False-positive triagePer-ecosystem CLIUnified config
Reachability analysisNoneNone (most tools)
CI integrationBuilt into package managerPer-tool action

Native audit catches the high-confidence per-ecosystem feed quickly; the unified scanner catches cross-ecosystem aggregations and waivers.

Maven OWASP Dependency-Check plugin

Maven's audit story is the OWASP Dependency-Check plugin (no native mvn audit):

<!-- pom.xml -->
<plugin>
  <groupId>org.owasp</groupId>
  <artifactId>dependency-check-maven</artifactId>
  <version>10.0.4</version>
  <executions>
    <execution>
      <goals>
        <goal>check</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <failBuildOnCVSS>7.0</failBuildOnCVSS>
    <suppressionFile>dependency-check-suppressions.xml</suppressionFile>
    <formats>
      <format>HTML</format>
      <format>JSON</format>
      <format>SARIF</format>
    </formats>
  </configuration>
</plugin>

For Gradle: the same plugin via the org.owasp.dependencycheck Gradle plugin. Source: jeremylong.github.io/DependencyCheck/dependency-check-maven/.

Suppression justification templates

Justification is mandatory in the suppression file or audit-skip list:

<!-- dependency-check-suppressions.xml (Maven) -->
<suppress>
  <notes>
    Reason: log4j-core 2.14.x is bundled but not loaded at runtime
            (verified via dependency tree analysis)
    Approved-by: alice@example.com
    Re-review-date: 2026-09-15
  </notes>
  <packageUrl regex="true">^pkg:maven/org\.apache\.logging\.log4j/log4j-core@2\.14\..*$</packageUrl>
  <vulnerabilityName>CVE-2021-44228</vulnerabilityName>
</suppress>

For ad-hoc CLI ignores (pip-audit --ignore-vuln, cargo audit --ignore), maintain a sibling AUDIT_IGNORES.md mapping each ID to reason + approver + re-review-date. Without the sibling file, the ignore is invisible to reviewers.

Cadence: every quarter, audit suppression entries; expired re-review dates remove entries.

Output aggregation

For downstream aggregation, output each tool's JSON to a stable filename:

npm audit --json > sca-npm.json || true        # || true: don't fail before triage
pip-audit --format json --output sca-pip.json || true
mvn dependency-check:check -Dformats=JSON
cargo audit --json > sca-cargo.json || true

The aggregation step normalizes each tool's schema + dedupes cross-tool findings.

Overview

Most package managers ship native audit subcommands that query an ecosystem-specific advisory feed (npm advisories, PyPA, RubySec, Cargo advisory DB, etc.) - the fastest first-line defense: already installed, no extra tooling, runs in seconds. For full coverage, run one native audit + a unified scanner (snyk.md (opens in new window) / osv-scanner.md (opens in new window)). The speed/coverage tradeoff table is in references/ecosystem-config-and-triage.md (opens in new window).

When to use

Step 1 - npm / Yarn / pnpm

# npm (built-in since npm 6)
npm audit
npm audit --audit-level=high              # filter to HIGH+CRITICAL
npm audit --json > audit.json
npm audit fix                              # auto-upgrade where compatible
npm audit fix --force                      # may break: bumps majors

# Yarn 2+ (Berry)
yarn npm audit
yarn npm audit --severity=high
yarn npm audit --recursive                 # scan all workspaces

# pnpm
pnpm audit
pnpm audit --audit-level high
pnpm audit --json

Source: docs.npmjs.com/cli/v10/commands/npm-audit + yarnpkg.com/cli/npm/audit + pnpm.io/cli/audit.

Step 2 - pip-audit (Python)

pip install pip-audit

# Scan installed packages in current env
pip-audit

# Scan a requirements file
pip-audit -r requirements.txt

# Scan with PyPA + OSV.dev
pip-audit -s pypi -s osv

# JSON / SARIF output
pip-audit --format json --output pip-audit.json
pip-audit --format sarif --output pip-audit.sarif

# Fix vulnerabilities (auto-upgrade)
pip-audit --fix

# Skip specific CVEs
pip-audit --ignore-vuln GHSA-xxxx-yyyy-zzzz

Source: pypi.org/project/pip-audit + github.com/pypa/pip-audit. pip-audit is the official PyPA tool (preferred over the older safety package).

Step 3 - Maven (OWASP Dependency-Check)

Maven has no native mvn audit; use the OWASP Dependency-Check plugin, which fails the build above a CVSS threshold and emits HTML/JSON/SARIF:

mvn dependency-check:check

The pom.xml plugin block (with failBuildOnCVSS + suppression file) and the Gradle equivalent are in references/ecosystem-config-and-triage.md (opens in new window).

Source: jeremylong.github.io/DependencyCheck/dependency-check-maven/.

Step 4 - cargo audit (Rust)

cargo install cargo-audit

cargo audit
cargo audit --json
cargo audit --deny warnings              # treat warnings as errors
cargo audit --ignore RUSTSEC-2023-0001   # specific advisory

Source: rustsec.org + github.com/rustsec/rustsec.

Exit codes, per-category --deny flags, the committed .cargo/audit.toml suppression schema, SARIF output, cargo audit fix, binary auditing (cargo audit bin), and the rustsec/audit-check Action are in references/cargo-bundle-audit.md (opens in new window).

Step 5 - bundler-audit (Ruby)

gem install bundler-audit

bundle-audit check                        # one-time scan
bundle-audit update                       # refresh advisory DB
bundle-audit check --update               # combined refresh + scan
bundle-audit check --ignore CVE-2024-1234 # specific CVE

Source: github.com/rubysec/bundler-audit.

The committed .bundler-audit.yml waiver template (inline justification + re-review date), Rake task wiring, JSON output, and the CI gate are in references/cargo-bundle-audit.md (opens in new window).

Step 6 - False-positive triage (MANDATORY)

Each native audit has its own suppression mechanism:

ToolSuppression
npm auditnpm audit --omit dev (skip devDependencies) + package.json overrides field for forced version pin
pip-audit--ignore-vuln <id> CLI flag (per-CVE)
dependency-check-mavendependency-check-suppressions.xml (XML schema with vuln-name regex + reason)
cargo auditcommitted .cargo/audit.toml [advisories] ignore (preferred over --ignore CLI flags - see references/cargo-bundle-audit.md)
bundle-auditcommitted .bundler-audit.yml ignore: list (preferred over --ignore CLI flags - see references/cargo-bundle-audit.md)

Every suppression carries a mandatory justification (reason + approver + re-review date). The Maven XML template, the AUDIT_IGNORES.md pattern for ad-hoc CLI ignores, and the quarterly review cadence are in references/ecosystem-config-and-triage.md (opens in new window).

Step 7 - CI integration patterns

# Fast first-line gate
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      # npm
      - if: hashFiles('package-lock.json') != ''
        run: npm audit --audit-level=high
      # Python
      - if: hashFiles('requirements.txt') != ''
        run: pip-audit -r requirements.txt
      # Maven
      - if: hashFiles('pom.xml') != ''
        run: mvn dependency-check:check
      # Rust
      - if: hashFiles('Cargo.lock') != ''
        run: cargo audit
      # Ruby
      - if: hashFiles('Gemfile.lock') != ''
        run: bundle-audit check --update

The if: hashFiles(...) pattern auto-skips ecosystems not present in the repo.

Anti-patterns

Anti-patternWhy it failsFix
npm audit without --audit-levelLow-severity noise overwhelms; team disablesStart with --audit-level=high (Step 1)
npm audit fix --force in CIBumps majors silently; breaks buildsManual review for force-fix; never in CI
Suppress without Re-review-datePermanent debtMandatory template (Step 6)
Skip --update for bundle-auditStale advisory DB; misses recent CVEsAlways --update (Step 5)
Ignore bundle-audit check exit codeFindings invisibleLet exit code propagate to CI
Use only native audit; skip Snyk/OSVPer-ecosystem-DB blind spotsLayered (Step 1 cross-ref)

Limitations

  • Per-ecosystem DB coverage varies; one ecosystem's feed may carry a CVE another lacks.
  • No reachability analysis: every CVE on a declared dep counts even if the vulnerable function isn't called.
  • Maven Dependency-Check requires an NVD data sync (slow first run, ~1 GB cache).
  • npm audit fix --force bumps majors; always manual-review before applying.
  • Yarn classic (1.x) and pnpm have slightly different audit output shapes vs npm.

References

Reference

osv-scanner config, triage, and CI

View source (opens in a new tab) (opens in new window)

Deep reference for osv-scanner.md (opens in new window). That file keeps install, the recursive/per-lockfile scan, output formats, SBOM input, exit codes, and a minimal ignore snippet; this file holds the full osv-scanner.toml schema, the justification template, and the GitHub Actions workflow.

Full osv-scanner.toml schema

Point the scanner at a config with --config (osv-usage (opens in new window)):

osv-scanner --config ./my-osv-scanner-config.toml scan -r .
# osv-scanner.toml
[[IgnoredVulns]]
id = "CVE-2024-1234"
ignoreUntil = 2026-12-15T00:00:00Z
reason = "Reachability analysis confirms unreachable; tracked in JIRA-1234"

[[IgnoredVulns]]
id = "GHSA-xxxx-yyyy-zzzz"
ignoreUntil = 2026-09-30T00:00:00Z
reason = "Vendor-supplied; pin to current version pending Q3 upgrade"

[[PackageOverrides]]
name = "lodash"
version = "4.17.20"
ecosystem = "npm"
ignore = true   # Suppress all vulns in this exact pinned version
reason = "Test fixture; not in production dependency graph"

Suppression layers and justification

Three suppression layers:

MechanismWhereUse
[[IgnoredVulns]] in osv-scanner.toml (with ignoreUntil)Repo rootPer-CVE; auditable in git
[[PackageOverrides]] in osv-scanner.tomlRepo rootPer-package version + ecosystem
--severity-threshold filter (when supported by version)CI flagScan-time filter, not suppression

Justification is mandatory in osv-scanner.toml:

[[IgnoredVulns]]
id = "CVE-2024-1234"
ignoreUntil = 2026-12-15T00:00:00Z
reason = """
Reachability: function `vulnerable_func` not exported; verified via
git grep + dynamic instrumentation. Issue requires admin context
which is separately controlled.
Approved-by: alice@example.com
Re-review-date: 2026-09-15
"""

ignoreUntil is enforced by osv-scanner - past-due ignores are re-surfaced in the scan results. Cadence: every quarter, list all [[IgnoredVulns]] entries + re-validate the reason; expired ones removed, persistent ones escalated to upgrade work.

GitHub Actions CI

The google/osv-scanner-action GHA wraps the Docker invocation + SARIF upload:

jobs:
  osv:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: google/osv-scanner-action/osv-scanner-action@v1
        with:
          scan-args: |-
            --recursive
            --skip-git
            --format=sarif
            --output=osv.sarif
            ./
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: osv.sarif }

When to use

  • Team prefers OSS tooling over commercial Snyk.
  • Layered SCA strategy - pair with snyk.md (opens in new window) for cross-DB consensus signal.
  • Custom package manager produces SBOMs not natively supported; feed SBOM directly to OSV-Scanner.
  • Lightweight CI gate without commercial-license setup.

Step 1 - Install

Per osv-usage (opens in new window):

docker pull ghcr.io/google/osv-scanner:latest

Other install paths (consult google.github.io/osv-scanner for current options per platform):

# Go install
go install github.com/google/osv-scanner/cmd/osv-scanner@v1

# Homebrew
brew install osv-scanner

# Direct binary download from github.com/google/osv-scanner/releases

Step 2 - Basic recursive scan

Per osv-usage (opens in new window):

osv-scanner scan -r ./my-project-dir/

Auto-detects manifest files (package-lock.json, Pipfile.lock, go.mod, Cargo.lock, etc.) and queries OSV.dev for each.

Per-lockfile scan (-L targets one lockfile without recursing, useful in monorepos):

osv-scanner scan -L package-lock.json --output-file scan-results.txt

Step 3 - Output formats

Per osv-usage (opens in new window): --format json and --format=vertical and --format=html are referenced. Full format list per the osv-scanner documentation:

FormatUse
--format jsonFor downstream aggregation
--format sarifGitHub Code Scanning upload
--format markdownPR comments
--format verticalDefault human-readable
--format htmlStandalone report

Step 4 - SBOM-driven scanning

For projects with custom build systems that emit SBOMs but lack natively-parsed lockfiles. A recursive scan picks up SBOMs whose names follow the format's pattern (*.spdx.json, *.spdx, bom.json, *.cdx.json, bom.xml, *.cdx.xml); pass one directly with -L (source scanning docs (opens in new window)):

osv-scanner scan source -L my-app.cdx.json
osv-scanner scan source -L my-app.spdx.json

Components need Package URLs (purls) to be matched.

This composes with syft-generation - Syft generates the SBOM, OSV-Scanner queries OSV.dev against it.

Step 5 - osv-scanner.toml config

Suppress a vuln with an expiring, reasoned entry (auditable in git):

# osv-scanner.toml
[[IgnoredVulns]]
id = "CVE-2024-1234"
ignoreUntil = 2026-12-15T00:00:00Z
reason = "Reachability confirms unreachable; tracked in JIRA-1234"

The full schema ([[PackageOverrides]], multi-entry config, --config flag, the mandatory justification template) is in osv-scanner-config-and-ci.md (opens in new window).

Step 6 - False-positive triage (MANDATORY)

Suppress via [[IgnoredVulns]] (per-CVE, with ignoreUntil) or [[PackageOverrides]] (per pinned package + ecosystem) in osv-scanner.toml; --severity-threshold is a scan-time filter, not a suppression. Every entry carries a mandatory reason + approver + re-review date. ignoreUntil is enforced - past-due ignores re-surface in the scan results. The suppression-layer table and justification template are in osv-scanner-config-and-ci.md (opens in new window).

Step 7 - Exit codes + CI gating

OSV-Scanner exit codes, per the output docs (opens in new window):

  • 0 - packages found, no known vulnerabilities
  • 1 - packages found, vulnerabilities found
  • 127 - general error
  • 128 - no packages found (the scan matched no files; treat as a failure)

The google/osv-scanner-action GHA (Docker invocation + SARIF upload) is in osv-scanner-config-and-ci.md (opens in new window).

Step 8 - License-checking (adjacent feature)

OSV-Scanner has experimental license-summary support:

osv-scanner --experimental-licenses-summary scan -r .

For full license-compliance + scanning, pair with sbom-formats or use a dedicated tool like ScanCode / FOSSology.

Anti-patterns

  • [[IgnoredVulns]] without ignoreUntil -> debt persists; always set ignoreUntil (Step 5).
  • Skipping --format=sarif upload -> findings invisible in the GitHub Security tab; always emit SARIF (Step 7).
  • Running only OSV-Scanner -> OSV.dev has its own coverage profile; layer with snyk.md (opens in new window).
  • Scanning without a committed lockfile -> manifest fallback resolves versions less accurately; commit the lockfile and scan via -L.
  • --config pointing outside the repo -> suppressions not auditable; commit osv-scanner.toml at the repo root.

Limitations

  • OSV.dev coverage varies by ecosystem (Python / npm / Go strong; less common ones thinner).
  • No reachability analysis: every CVE on a declared dep counts even if the vulnerable function isn't called.
  • Container image scanning is limited; prefer trivy-image or the Grype workflow in syft-generation.
  • The --sbom flag is deprecated in favour of -L (changelog, #1961 (opens in new window)); pin the scanner version in CI.

References

Reference

Snyk `.snyk` policy and false-positive triage

View source (opens in a new tab) (opens in new window)

.snyk policy file

Per Snyk's policy-file model (consult docs.snyk.io for current schema), .snyk lives at the project root and supports:

# .snyk
version: v1.0.0
ignore:
  SNYK-JS-LODASH-567746:
    - '*':
        reason: "False positive; we don't pass user input to Lodash sortBy"
        expires: '2026-12-15T00:00:00.000Z'
        created: '2026-05-15T00:00:00.000Z'
patch: {}

Per-vuln ignore can be scoped to specific paths (* > lodash, my-package > lodash) and must include an expires: field - Snyk policy validates this at scan time.

False-positive triage (MANDATORY)

Three suppression layers:

MechanismWhereUse
.snyk policy file ignore (with expiration)Repo rootPer-vuln + per-path; auditable in git history
Snyk dashboard "Ignore" actionsnyk.io/appOrg-wide; persistent; reviewer-tracked
--severity-threshold= filterCI flagScan-time noise reduction (not suppression)
--fail-on=upgradable flagCI flagOnly fail if a fix exists; soft gate

Justification template (mandatory in .snyk):

ignore:
  SNYK-JS-LODASH-567746:
    - '*':
        reason: |
          Reason: Lodash sortBy not exposed to user input;
          attack path requires admin context which is separately
          controlled. Verified in code review (PR #1234).
        approved-by: alice@example.com
        expires: '2026-12-15T00:00:00.000Z'
        created: '2026-05-15T00:00:00.000Z'
        re-review-date: '2026-09-15T00:00:00.000Z'

Cadence: every quarter, list .snyk policies grouped by re-review-date and process expired entries.

Per github.com/snyk/snyk (opens in new window), companion subcommands cover adjacent surfaces:

SubcommandSurface
snyk testSCA (open-source dependencies)
snyk code testSAST (proprietary code)
snyk container test <image>Container image scanning
snyk iac testIaC scanning (Terraform, Kubernetes)
snyk monitorContinuous monitoring with new-vuln alerts

This reference focuses on snyk test for SCA. For SAST coverage, prefer the OSS-first patterns in semgrep-rules; for container scanning, see trivy-image.

When to use

  • The team has a Snyk license and uses Snyk as primary SCA.
  • Multi-language project with mixed package managers - Snyk's auto-detect simplifies setup vs running per-ecosystem audit commands.
  • Continuous-monitoring requirement (vuln alerts on newly-disclosed CVEs against pinned dependencies).
  • Layered with osv-scanner.md (opens in new window) for cross-DB consensus signal.

Step 1 - Install + authenticate

Per sn-gh (opens in new window):

npm install -g snyk
snyk auth

The snyk auth flow opens a browser window; the credential is stored at ~/.config/configstore/snyk.json. For CI, use SNYK_TOKEN env var (no snyk auth needed):

export SNYK_TOKEN=$(cat /path/to/snyk-token)
snyk test

Step 2 - Basic SCA scan

Per sn-gh (opens in new window): "Run snyk test in a directory containing a supported package manifest (like package.json or pom.xml)."

snyk test                              # current dir; auto-detects manifest
snyk test --all-projects               # recursive multi-manifest scan
snyk test --org=my-org                 # explicit org context
snyk test --severity-threshold=high    # filter LOW + MEDIUM (less noise)
snyk test --fail-on=upgradable         # only fail if upgrade available

Common output format flags:

snyk test --json                                  # JSON for parsing
snyk test --json-file-output=snyk.json             # JSON to file
snyk test --sarif-file-output=snyk.sarif           # SARIF for GHA

The JSON / SARIF output feeds downstream aggregation for cross-tool deduplication + prioritization.

Step 3 - snyk monitor for continuous tracking

snyk monitor --org=my-org --project-name=my-app

Per sn-gh (opens in new window): "create dependency snapshots and receive alerts about newly disclosed vulnerabilities."

The snapshot lives on snyk.io/app/your-org; new CVEs disclosed against your pinned versions trigger email + Slack alerts (configured in Snyk dashboard).

Step 4 - .snyk policy + false-positive triage (MANDATORY)

Suppress false positives through a .snyk policy file at the repo root. Every per-vuln ignore must include an expires: field - Snyk validates this at scan time. Layer .snyk ignores (auditable in git) with dashboard ignores (org-wide) and the --severity-threshold= / --fail-on=upgradable CI filters, and re-review each ignore quarterly.

Full policy schema, the mandatory justification template, the suppression-layer table, and the re-review cadence: snyk-policy-and-triage.md (opens in new window).

Step 5 - CI integration

jobs:
  snyk:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: snyk/actions/setup@master
      - run: snyk test --severity-threshold=high --json-file-output=snyk.json
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
      - run: snyk monitor   # snapshot for continuous monitoring
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        if: github.ref == 'refs/heads/main'
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: snyk-report, path: snyk.json }

Step 6 - Multi-language support

Per sn-gh (opens in new window): Snyk scans "Open Source (via package managers)", "Application code vulnerabilities", "Container images and Kubernetes applications", and "Infrastructure as Code (Terraform, Kubernetes)."

Auto-detection covers package managers including npm / yarn / pnpm / pip / pipenv / poetry / Maven / Gradle / sbt / Composer / RubyGems / Go modules / Cargo / Hex / NuGet / CocoaPods / Swift Package Manager.

For unsupported / custom package managers, generate an SBOM and test it with snyk sbom test --file=sbom.cdx.json (CycloneDX 1.4-1.6 JSON or SPDX 2.3 JSON with Package URLs; Snyk Enterprise plans), per Snyk sbom test (opens in new window).

Worked example

A team with a Snyk license adds SCA to a mixed npm + Maven monorepo. In CI they run snyk test --all-projects --severity-threshold=high --json-file-output=snyk.json, which auto-detects both package.json and pom.xml and reports 3 high-severity findings. One is SNYK-JS-LODASH-567746 in a lodash path that never receives user input. A reviewer verifies the attack path is unreachable, then adds a .snyk ignore with reason:, approved-by:, expires: '2026-12-15...', and a re-review-date, so the next scan passes the --severity-threshold=high gate. snyk monitor runs on main to snapshot the dependency tree, and the team pairs the run with osv-scanner.md (opens in new window) for cross-database consensus. The .snyk ignore resurfaces in the quarterly re-review grouped by re-review-date.

Anti-patterns

Anti-patternWhy it failsFix
Run snyk test without --severity-thresholdNoise on first scan; team disablesStart --severity-threshold=high (Step 2)
.snyk ignore without expiresSnyk policy rejects; OR debt persists foreverMandatory expires: (Step 4)
Skip snyk monitor on main branchNewly-disclosed CVEs against pinned deps go undetectedAdd to main-branch CI (Step 5)
Run only Snyk; skip OSVSingle-DB blind spotsPair with osv-scanner (cross-ref)
Hardcode SNYK_TOKEN in scriptsToken leakCI secret + redact (Step 5)

Limitations

References