Dependency Vulnerability Scanning in CI with Grype, Trivy, and OSV-Scanner

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: false

Grype 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.sarif

The 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:latest

Trivy 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:latest

Trivy 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-5678

Trivy 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.sarif

Caching 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:latest

OSV-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

  1. Ecosystem-native advisories: OSV aggregates from the source — PyPI advisories, npm security advisories, Go vulnerability database — rather than just NVD
  2. Lock file analysis: First-class support for scanning lock files (package-lock.json, go.sum, Pipfile.lock, etc.) rather than just installed packages
  3. SBOM scanning: Accepts CycloneDX and SPDX
  4. 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-scanner

Basic 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.lock

OSV-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.sarif

OSV-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
        fi

Establishing 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?

Read more