Dependency Vulnerability Scanning in CI with Grype, Trivy, and OSV-Scanner
Dependency vulnerability scanning is table stakes for modern CI pipelines. A new critical CVE can drop at any moment, and without automated scanning, your pipeline will happily ship code with known exploitable vulnerabilities. Three tools dominate this space: Grype, Trivy, and OSV-Scanner. Each has different strengths, and the right choice depends on your stack and workflow.
Why Automated Scanning Matters
Manual security reviews can't keep up with the vulnerability disclosure rate. The NVD published over 28,000 CVEs in 2023. Your dependencies update constantly. The only scalable approach is automated scanning on every build.
The goal is to shift security left: catch vulnerabilities in the PR, not after deployment. A vulnerability found before merge costs minutes to fix. The same vulnerability found after a production incident can cost weeks.
Grype
Grype is Anchore's vulnerability scanner, designed to work natively with SBOMs. It's fast, produces clean output, and integrates tightly with Syft for SBOM-driven workflows.
What Grype Scans
- Container images (any registry)
- Local directories and filesystems
- Pre-generated SBOMs (SPDX or CycloneDX)
- Archives (tar, zip, etc.)
Language and OS Coverage
Grype detects packages from:
- OS packages: Debian/Ubuntu (apt), RHEL/CentOS (rpm), Alpine (apk), Amazon Linux
- Python: pip packages, requirements.txt, poetry.lock, setup.cfg
- JavaScript: npm, yarn, pnpm packages
- Java: Maven, Gradle, JAR/WAR/EAR archives (including nested JARs)
- Go: go.sum, Go modules in binaries
- Ruby: Bundler
- Rust: Cargo.lock
- PHP: Composer
- .NET: NuGet
Basic Usage
# Scan a container image
grype myapp:latest
<span class="hljs-comment"># Scan local directory
grype <span class="hljs-built_in">dir:.
<span class="hljs-comment"># Scan a pre-generated SBOM
grype sbom:sbom.spdx.json
<span class="hljs-comment"># Fail on high+ severity
grype myapp:latest --fail-on high
<span class="hljs-comment"># JSON output for processing
grype myapp:latest -o json <span class="hljs-pipe">| jq <span class="hljs-string">'.matches[] | {name: .artifact.name, severity: .vulnerability.severity, fixed: .vulnerability.fix.versions}'Grype Configuration
# .grype.yaml
fail-on-severity: high
only-fixed: false
# Suppress known false positives or accepted risks
ignore:
- vulnerability: CVE-2023-XXXX
package:
name: some-lib
- fix-state: not-fixed # suppress unfixable vulns
output: table
db:
auto-update: true
validate-age: true
# Max age of DB before refusing to scan (safety check)
max-allowed-built-age: "120h"
# Speed up repeated scans
check-for-app-update: falseGrype in GitHub Actions
- name: Vulnerability scan with Grype
uses: anchore/scan-action@v3
with:
image: myapp:${{ github.sha }}
fail-build: true
severity-cutoff: high
output-format: sarif # Uploads to GitHub Security tab
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results.sarifThe SARIF output integrates with GitHub's Security tab, showing vulnerabilities inline in PRs.
Trivy
Trivy from Aqua Security is the most comprehensive scanner of the three. Beyond vulnerability scanning, it handles misconfigurations, secrets, and license detection.
What Trivy Scans
Beyond the standard OS + language packages, Trivy also scans:
- Kubernetes manifests for misconfigurations
- Terraform and CloudFormation for IaC misconfigurations
- Helm charts
- Secrets (hardcoded API keys, passwords)
- License compliance
- VM images
- Git repositories (all commits)
Basic Usage
# Scan a container image
trivy image myapp:latest
<span class="hljs-comment"># Scan a local filesystem
trivy fs /path/to/project
<span class="hljs-comment"># Scan a git repository
trivy repo https://github.com/myorg/myrepo
<span class="hljs-comment"># Scan Kubernetes cluster
trivy k8s --report=summary cluster
<span class="hljs-comment"># Scan IaC configs
trivy config ./terraform
<span class="hljs-comment"># Fail on critical only
trivy image --exit-code 1 --severity CRITICAL myapp:latest
<span class="hljs-comment"># Fail on high+
trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:latestTrivy Flags for CI
# Scan with SARIF output
trivy image --format sarif --output trivy-results.sarif myapp:latest
<span class="hljs-comment"># CycloneDX SBOM output
trivy image --format cyclonedx --output sbom.cdx.json myapp:latest
<span class="hljs-comment"># Skip update check (faster in offline CI)
trivy image --skip-update myapp:latest
<span class="hljs-comment"># Use cached DB (pre-download in CI)
trivy image --cache-dir /opt/trivy-db myapp:latest
<span class="hljs-comment"># Ignore unfixed vulnerabilities
trivy image --ignore-unfixed myapp:latest
<span class="hljs-comment"># Only scan specific vulnerability types
trivy image --vuln-type os,library myapp:latestTrivy Configuration File
# trivy.yaml
severity:
- HIGH
- CRITICAL
exit-code: 1
ignore-unfixed: true
# Ignore specific CVEs
ignorefile: .trivyignore
vulnerability:
type:
- os
- library
scan:
security-checks:
- vuln
- secret
- config.trivyignore
# .trivyignore
# CVE-2022-1234 - False positive, not affected by this code path
CVE-2022-1234
# CVE-2023-5678 - Accepted risk, no fix available, mitigated by WAF
CVE-2023-5678Trivy in GitHub Actions
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: sarif
output: trivy-results.sarif
exit-code: 1
severity: HIGH,CRITICAL
ignore-unfixed: true
- name: Upload Trivy SARIF
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarifCaching the Trivy DB in CI
Trivy downloads a ~300MB vulnerability database. Cache it to avoid repeated downloads:
- name: Cache Trivy DB
uses: actions/cache@v4
with:
path: ~/.cache/trivy
key: trivy-db-${{ github.run_id }}
restore-keys: trivy-db-
- name: Update Trivy DB
run: trivy image --download-db-only
- name: Scan image
run: trivy image --skip-update myapp:latestOSV-Scanner
OSV-Scanner from Google queries the OSV (Open Source Vulnerabilities) database, which aggregates advisories from GitHub Advisory Database, NVD, and ecosystem-specific databases (PyPI, npm, crates.io, etc.).
What Makes OSV-Scanner Different
- Ecosystem-native advisories: OSV aggregates from the source — PyPI advisories, npm security advisories, Go vulnerability database — rather than just NVD
- Lock file analysis: First-class support for scanning lock files (package-lock.json, go.sum, Pipfile.lock, etc.) rather than just installed packages
- SBOM scanning: Accepts CycloneDX and SPDX
- Commit-level resolution: For some ecosystems, can identify the exact commit that introduced a vulnerability
Installation
# Go install
go install github.com/google/osv-scanner/cmd/osv-scanner@latest
<span class="hljs-comment"># Homebrew
brew install osv-scanner
<span class="hljs-comment"># Docker
docker pull ghcr.io/google/osv-scannerBasic Usage
# Scan current directory (finds lock files automatically)
osv-scanner .
<span class="hljs-comment"># Scan specific lock file
osv-scanner --lockfile package-lock.json
<span class="hljs-comment"># Scan multiple lock files
osv-scanner --lockfile package-lock.json --lockfile requirements.txt
<span class="hljs-comment"># Scan container image
osv-scanner --docker myapp:latest
<span class="hljs-comment"># Scan git repository (including commit history)
osv-scanner -r .
<span class="hljs-comment"># Scan SBOM
osv-scanner --sbom sbom.cdx.json
<span class="hljs-comment"># JSON output
osv-scanner --format json . <span class="hljs-pipe">| jq .Lock File Support
# Go
osv-scanner --lockfile go.sum
<span class="hljs-comment"># Node.js
osv-scanner --lockfile package-lock.json
osv-scanner --lockfile yarn.lock
osv-scanner --lockfile pnpm-lock.yaml
<span class="hljs-comment"># Python
osv-scanner --lockfile requirements.txt
osv-scanner --lockfile Pipfile.lock
osv-scanner --lockfile poetry.lock
<span class="hljs-comment"># Ruby
osv-scanner --lockfile Gemfile.lock
<span class="hljs-comment"># Rust
osv-scanner --lockfile Cargo.lock
<span class="hljs-comment"># PHP
osv-scanner --lockfile composer.lockOSV-Scanner in CI
- name: Run OSV-Scanner
uses: google/osv-scanner-action@v1
with:
scan-args: |-
--lockfile=package-lock.json
--lockfile=requirements.txt
--format=sarif
--output=osv-results.sarif
- name: Upload OSV SARIF
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: osv-results.sarifOSV-Scanner for Dependency Review
OSV-Scanner has a "guided remediation" mode that suggests specific upgrades:
# Get remediation suggestions
osv-scanner --format table --experimental-guided-remediation .Head-to-Head Comparison
| Capability | Grype | Trivy | OSV-Scanner |
|---|---|---|---|
| Container images | ✓ | ✓ | ✓ (docker) |
| Lock files | Limited | ✓ | ✓ (primary) |
| SBOM input | ✓ (primary) | ✓ | ✓ |
| IaC misconfiguration | ✗ | ✓ | ✗ |
| Secret scanning | ✗ | ✓ | ✗ |
| License scanning | ✗ | ✓ | ✗ |
| Kubernetes scanning | ✗ | ✓ | ✗ |
| VEX support | ✗ | ✗ | ✗ |
| SARIF output | ✓ | ✓ | ✓ |
| DB size | ~100MB | ~300MB | API-based |
| Scan speed | Fast | Medium | Fast |
| SBOM generation | Via Syft | ✓ | ✗ |
Vulnerability Database Comparison
Each tool uses different (partially overlapping) data sources:
| Source | Grype | Trivy | OSV-Scanner |
|---|---|---|---|
| NVD | ✓ | ✓ | ✓ (via OSV) |
| GitHub Advisory | ✓ | ✓ | ✓ (native) |
| Go Vuln DB | ✓ | ✓ | ✓ (native) |
| PyPI Advisory | ✓ | ✓ | ✓ (native) |
| npm Advisory | ✓ | ✓ | ✓ (native) |
| Ubuntu USN | ✓ | ✓ | ✗ |
| RedHat CSAF | ✓ | ✓ | ✗ |
| EPSS scores | ✗ | ✗ | ✗ |
OSV-Scanner gets ecosystem advisories directly from the source, which means it can catch vulnerabilities that NVD hasn't processed yet.
Running Multiple Scanners
Different scanners have different detection rates. For critical systems, run multiple:
scan-all:
runs-on: ubuntu-latest
steps:
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Grype scan
uses: anchore/scan-action@v3
with:
image: myapp:${{ github.sha }}
severity-cutoff: critical
fail-build: false # Collect all results, fail later
output-format: sarif
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
severity: CRITICAL
exit-code: 0
format: sarif
output: trivy-results.sarif
- name: OSV-Scanner
run: |
osv-scanner --docker myapp:${{ github.sha }} \
--format sarif \
--output osv-results.sarif || true
# Now fail if any scanner found criticals
- name: Check results
run: |
GRYPE_CRITICALS=$(jq '[.runs[].results[] | select(.level == "error")] | length' grype-results.sarif)
TRIVY_CRITICALS=$(jq '[.runs[].results[] | select(.level == "error")] | length' trivy-results.sarif)
if [ "$GRYPE_CRITICALS" -gt 0 ] || [ "$TRIVY_CRITICALS" -gt 0 ]; then
echo "Critical vulnerabilities found"
exit 1
fiEstablishing a Baseline
New projects often have existing vulnerabilities. Block the build from day one, or you'll never turn scanning on. Instead, generate a baseline and only fail on new vulnerabilities:
# Generate baseline
grype myapp:latest -o json > baseline.json
<span class="hljs-comment"># In CI: fail only if new vulnerabilities vs baseline
grype myapp:latest -o json > current.json
python3 check-new-vulns.py baseline.json current.json# check-new-vulns.py
import json
import sys
def get_vuln_ids(data):
return {m['vulnerability']['id'] for m in data.get('matches', [])}
with open(sys.argv[1]) as f:
baseline = get_vuln_ids(json.load(f))
with open(sys.argv[2]) as f:
current = get_vuln_ids(json.load(f))
new_vulns = current - baseline
if new_vulns:
print(f"New vulnerabilities introduced: {new_vulns}")
sys.exit(1)
print("No new vulnerabilities")Practical Recommendations
For container-heavy shops: Start with Trivy — it covers containers, IaC, secrets, and licenses in one tool. Then add Grype for SBOM-driven workflows once you're generating SBOMs.
For application security teams: OSV-Scanner's lock file scanning is excellent for detecting vulnerabilities in application dependencies. Add it to your PR checks alongside existing SAST tools.
For compliance-driven environments: Grype + Syft for SBOM generation and vuln scanning, with SARIF uploads to your security dashboard.
Fail-on severity: Start with --fail-on critical to avoid overwhelming teams with noise. Tighten to high once teams have processed the backlog.
Don't scan without a plan for remediation: Alerts that nobody acts on are noise. For every scanner you add, define the SLA for remediation (critical: 72 hours, high: 30 days, etc.).
The tooling is mature and freely available. The remaining question is process: what do you do with the findings?