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.
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
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) | Free, open source |
| Snyk | Snyk's own database, plus snyk monitor alerts (Snyk CLI) | Commercial license |
The workflow below is the same for all three; the per-tool detail is in the references.
| 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.
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).
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).
syft-generation produces the SBOM.
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):
| 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.
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); the container image takes the same
arguments (installation). 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.
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) |
| 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.
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); the full job is in references/snyk.md.
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-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 |
reachability-analyzer.trivy-image or the Grype flow in
syft-generation.dependabot-config and renovate-config.-L, -r, --format, --output-file, --config.scan source -L.ghcr.io/google/osv-scanner image.until expiry.snyk test, snyk monitor, subcommands.snyk sbom test --file, supported formats, Enterprise plans.syft-generation (SBOMs), reachability-analyzer,
cve-exploitability-triage, multi-tool-finding-triage,
dependabot-config, renovate-config, trivy-image.