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:
| Family | Database | Cost |
|---|---|---|
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-Scanner | OSV.dev, aggregated across ecosystems (OSV-Scanner usage (opens in new window)) | Free, open source |
| Snyk | Snyk'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
Step 1 - Pick the scanner
| Repo situation | Start with | Reference |
|---|---|---|
| One ecosystem, fastest first gate, nothing extra to install | That ecosystem's native audit | references/native-audits.md |
| Several ecosystems, open source only, or an SBOM as input | OSV-Scanner | references/osv-scanner.md |
| A Snyk license, or alerts on new CVEs against pinned versions | Snyk | references/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=highSBOM 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 code | Meaning | CI action |
|---|---|---|
| 0 | Packages found, no known vulnerabilities | Pass |
| 1 | Packages found, vulnerabilities found | Fail (or triage, Step 5) |
| 127 | General error | Fail |
| 128 | No packages found | Fail: 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:
| Scanner | Suppression | Expiry |
|---|---|---|
| Snyk | .snyk ignore: entry | expires: (required) |
| OSV-Scanner | osv-scanner.toml [[IgnoredVulns]] | ignoreUntil |
| pip-audit | --ignore-vuln <id> | Record in a committed ignore list |
| Dependency-Check | dependency-check-suppressions.xml | <suppress until="2026-12-31Z"> (suppression docs (opens in new window)) |
| cargo audit | .cargo/audit.toml [advisories] ignore | Record 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-pattern | Why it fails | Fix |
|---|---|---|
| No severity threshold on the first run | Low-severity noise; the team turns the job off | --audit-level=high / --severity-threshold=high |
| Treating any non-1 exit as green | OSV exit 128 means nothing was scanned | Fail on 128 (Step 3) |
| Suppression with no expiry | Debt never comes back for review | expires: / ignoreUntil (Step 5) |
| One scanner only | Each advisory database has gaps | Native audit plus OSV-Scanner or Snyk (Step 1) |
npm audit fix --force in CI | Silently bumps major versions | Review forced upgrades by hand |
| Token in scripts | Leaks SNYK_TOKEN | CI secret |
Limitations
References
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
| Code | Meaning |
|---|---|
| 0 | No vulnerabilities / denial criteria not triggered |
| 1 | Vulnerabilities found matching denial criteria |
| 2 | Execution 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 yankedOutput 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 = falsecargo audit fix
cargo audit fix --dry-run # preview
cargo audit fix # update Cargo.toml constraints + cargo updatecargo 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 binaryBinaries 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
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 syncedOutput flags (ba-readme (opens in new window)):
| Flag | Output |
|---|---|
--format json | JSON for multi-tool SCA triage |
--output FILE | Write to file (CI artifact) |
--gemfile-lock PATH | Non-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-1234Per-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 runAdds 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
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
| Property | Native audit | Snyk / OSV |
|---|---|---|
| Speed | <5s typical | 10s - 60s |
| DB coverage | Per-ecosystem only | Cross-ecosystem aggregated |
| False-positive triage | Per-ecosystem CLI | Unified config |
| Reachability analysis | None | None (most tools) |
| CI integration | Built into package manager | Per-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 || trueThe 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 --jsonSource: 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-zzzzSource: 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:checkThe 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 advisorySource: 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 CVESource: 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:
| Tool | Suppression |
|---|---|
npm audit | npm audit --omit dev (skip devDependencies) + package.json overrides field for forced version pin |
pip-audit | --ignore-vuln <id> CLI flag (per-CVE) |
dependency-check-maven | dependency-check-suppressions.xml (XML schema with vuln-name regex + reason) |
cargo audit | committed .cargo/audit.toml [advisories] ignore (preferred over --ignore CLI flags - see references/cargo-bundle-audit.md) |
bundle-audit | committed .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 --updateThe if: hashFiles(...) pattern auto-skips ecosystems not present in the repo.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
npm audit without --audit-level | Low-severity noise overwhelms; team disables | Start with --audit-level=high (Step 1) |
npm audit fix --force in CI | Bumps majors silently; breaks builds | Manual review for force-fix; never in CI |
Suppress without Re-review-date | Permanent debt | Mandatory template (Step 6) |
Skip --update for bundle-audit | Stale advisory DB; misses recent CVEs | Always --update (Step 5) |
Ignore bundle-audit check exit code | Findings invisible | Let exit code propagate to CI |
| Use only native audit; skip Snyk/OSV | Per-ecosystem-DB blind spots | Layered (Step 1 cross-ref) |
Limitations
References
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:
| Mechanism | Where | Use |
|---|---|---|
[[IgnoredVulns]] in osv-scanner.toml (with ignoreUntil) | Repo root | Per-CVE; auditable in git |
[[PackageOverrides]] in osv-scanner.toml | Repo root | Per-package version + ecosystem |
--severity-threshold filter (when supported by version) | CI flag | Scan-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
Step 1 - Install
Per osv-usage (opens in new window):
docker pull ghcr.io/google/osv-scanner:latestOther 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/releasesStep 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.txtStep 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:
| Format | Use |
|---|---|
--format json | For downstream aggregation |
--format sarif | GitHub Code Scanning upload |
--format markdown | PR comments |
--format vertical | Default human-readable |
--format html | Standalone 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.jsonComponents 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):
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
Limitations
References
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:
| Mechanism | Where | Use |
|---|---|---|
.snyk policy file ignore (with expiration) | Repo root | Per-vuln + per-path; auditable in git history |
| Snyk dashboard "Ignore" action | snyk.io/app | Org-wide; persistent; reviewer-tracked |
--severity-threshold= filter | CI flag | Scan-time noise reduction (not suppression) |
--fail-on=upgradable flag | CI flag | Only 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:
| Subcommand | Surface |
|---|---|
snyk test | SCA (open-source dependencies) |
snyk code test | SAST (proprietary code) |
snyk container test <image> | Container image scanning |
snyk iac test | IaC scanning (Terraform, Kubernetes) |
snyk monitor | Continuous 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
Step 1 - Install + authenticate
Per sn-gh (opens in new window):
npm install -g snyk
snyk authThe 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 testStep 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 availableCommon 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 GHAThe 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-appPer 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-pattern | Why it fails | Fix |
|---|---|---|
Run snyk test without --severity-threshold | Noise on first scan; team disables | Start --severity-threshold=high (Step 2) |
.snyk ignore without expires | Snyk policy rejects; OR debt persists forever | Mandatory expires: (Step 4) |
Skip snyk monitor on main branch | Newly-disclosed CVEs against pinned deps go undetected | Add to main-branch CI (Step 5) |
| Run only Snyk; skip OSV | Single-DB blind spots | Pair with osv-scanner (cross-ref) |
| Hardcode SNYK_TOKEN in scripts | Token leak | CI secret + redact (Step 5) |