Automated Dependency Vulnerability Scanning in CI: Stop Shipping Known CVEs
You're Shipping Vulnerabilities You Already Know About
In March 2024, CVE-2024-3094 — the xz-utils backdoor — demonstrated that even foundational dependencies can be weaponized. But here's what should concern you more than sophisticated supply chain attacks: most breaches exploit vulnerabilities that have been publicly known for months or years. The 2017 Equifax breach (CVE-2017-5638) used an Apache Struts vulnerability that had a patch available two months before the attack. 147 million records. Two months of inaction.
I audit CI pipelines regularly. The pattern is consistent: teams run unit tests, integration tests, linting, formatting checks — and skip dependency scanning entirely. They deploy applications with dozens of known critical CVEs because nobody checks. The vulnerability database knows. The CVE has been published. The patch exists. But the pipeline doesn't look.
Automated dependency scanning in CI is not optional. It's the lowest-effort, highest-impact security control you can add to a pipeline. Here's how to do it properly.
The Tools Landscape
There are three categories of dependency scanners worth considering:
| Tool | Strengths | Best For |
|---|---|---|
| Trivy | Multi-target (OS, language, IaC, containers), fast, offline DB | All-in-one scanning |
| Grype | Excellent vulnerability matching, SBOM-native | SBOM-first workflows |
| OSV-Scanner | Uses OSV.dev (Google's open-source vulnerability database) | Go, Python, Rust ecosystems |
| Snyk | Developer-friendly, fix PRs, commercial support | Teams wanting managed solution |
| GitHub Dependabot | Native GitHub integration, automatic PRs | GitHub-only workflows |
For this guide, I'll focus on Trivy and Grype — they're open source, work in any CI system, and cover the widest range of ecosystems.
Trivy: The All-in-One Scanner
Trivy scans container images, filesystem dependencies, IaC files, and Kubernetes manifests. One tool, multiple attack surfaces.
Basic Filesystem Scan
Scan your project dependencies against the NVD and other advisory databases:
# Install
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Scan the current project
trivy filesystem --severity HIGH,CRITICAL --exit-code 1 .
The --exit-code 1 flag is critical — it makes Trivy return a non-zero exit code when vulnerabilities at the specified severity are found, which fails your CI pipeline.
GitHub Actions Integration
# .github/workflows/security-scan.yml
name: Dependency Security Scan
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
trivy-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy filesystem scan
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: fs
scan-ref: .
severity: HIGH,CRITICAL
exit-code: 1
format: table
- name: Run Trivy filesystem scan (SARIF)
uses: aquasecurity/trivy-action@0.28.0
if: always()
with:
scan-type: fs
scan-ref: .
severity: HIGH,CRITICAL
format: sarif
output: trivy-results.sarif
- name: Upload SARIF to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
This runs two scans: one that fails the pipeline on HIGH/CRITICAL findings, and one that uploads SARIF results to GitHub's Security tab regardless of the outcome. You get pipeline enforcement and a persistent security dashboard.
Container Image Scanning
Don't just scan your source dependencies — scan the built image. Base images carry their own vulnerabilities:
image-scan:
runs-on: ubuntu-latest
needs: build
steps:
- name: Scan container image
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: image
image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
severity: CRITICAL
exit-code: 1
ignore-unfixed: true
format: table
The --ignore-unfixed flag is important. It suppresses vulnerabilities that have no available fix — these are still worth tracking, but you shouldn't block deployments on issues you can't resolve yet.
Grype: SBOM-Native Scanning
Grype pairs with Syft (its companion SBOM generator) for a scan-from-SBOM workflow. This is more precise than filesystem scanning because the SBOM captures exactly what's packaged, not just what's in your source tree.
Generate SBOM, Then Scan
# Install Syft and Grype
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Generate SBOM from your container image
syft ghcr.io/your-org/your-app:latest -o spdx-json > sbom.spdx.json
# Scan the SBOM for vulnerabilities
grype sbom:sbom.spdx.json --fail-on critical
GitLab CI Integration
# .gitlab-ci.yml
dependency-scan:
stage: test
image: alpine:3.20
before_script:
- apk add --no-cache curl
- curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
script:
- grype dir:. --fail-on high --output table
- grype dir:. --output json > gl-dependency-scanning-report.json
artifacts:
reports:
dependency_scanning: gl-dependency-scanning-report.json
when: always
allow_failure: false
Handling False Positives and Exceptions
Not every CVE finding requires immediate action. Some vulnerabilities are in code paths your application doesn't use. Some are disputed. Some are in dev dependencies that never ship to production. You need a structured exception process.
Trivy Ignore File
# .trivyignore.yaml
vulnerabilities:
- id: CVE-2023-44487 # HTTP/2 rapid reset — mitigated at load balancer
statement: "Not exploitable. All HTTP/2 traffic terminates at the ALB which has mitigation applied."
expires: 2026-06-22
- id: CVE-2024-21626 # runc container breakout — patched in node AMI
statement: "Nodes running containerd 1.7.13+ which includes the fix."
expires: 2026-04-22
- id: GHSA-example-1234 # Dev dependency only, not in production image
statement: "Only present in devDependencies, not included in production build."
expires: 2026-05-22
Every exception must have:
- A statement explaining why it's acceptable
- An expiration date that forces periodic review
- A human who approved it (track this in your PR)
Exceptions without expiration dates are tech debt that never gets paid.
Grype Ignore Rules
# .grype.yaml
ignore:
- vulnerability: CVE-2023-44487
reason: "Mitigated at infrastructure level"
- package:
type: npm
name: "dev-only-package"
reason: "Dev dependency, not shipped"
Setting Severity Thresholds That Make Sense
A common mistake is setting the threshold too aggressively on day one. If you block on everything, developers will ignore the scanner or add blanket exceptions. Start practical and tighten over time:
# Week 1-2: Block only CRITICAL, report HIGH
trivy filesystem --severity CRITICAL --exit-code 1 .
trivy filesystem --severity HIGH,CRITICAL --exit-code 0 .
# Week 3-4: Block HIGH and CRITICAL
trivy filesystem --severity HIGH,CRITICAL --exit-code 1 .
# Ongoing: Track MEDIUM for awareness
trivy filesystem --severity MEDIUM,HIGH,CRITICAL --exit-code 0 --format json > full-report.json
Continuous Monitoring: Don't Just Scan at Build Time
New CVEs are published daily. A container image that was clean when you built it last Tuesday might have three critical vulnerabilities by Friday. Scanning only at build time creates a window of exposure between builds.
Scheduled Rescans
# .github/workflows/scheduled-scan.yml
name: Scheduled Vulnerability Scan
on:
schedule:
- cron: '0 6 * * *' # Daily at 6 AM UTC
jobs:
scan-deployed-images:
runs-on: ubuntu-latest
strategy:
matrix:
image:
- ghcr.io/your-org/api-service:production
- ghcr.io/your-org/web-frontend:production
- ghcr.io/your-org/worker:production
steps:
- name: Scan ${{ matrix.image }}
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: image
image-ref: ${{ matrix.image }}
severity: HIGH,CRITICAL
ignore-unfixed: true
format: json
output: scan-result.json
- name: Notify on new findings
if: failure()
uses: slackapi/slack-github-action@v2.1.0
with:
webhook: ${{ secrets.SECURITY_SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
{
"text": "New vulnerabilities found in ${{ matrix.image }}. Check the scan results."
}
Generating an SBOM for Every Release
Every production artifact should have a corresponding Software Bill of Materials. When the next Log4Shell drops, you need to answer "are we affected?" in minutes, not days.
generate-sbom:
runs-on: ubuntu-latest
needs: build
steps:
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository }}:${{ github.sha }}
format: spdx-json
output-file: sbom.spdx.json
- name: Attach SBOM to release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: sbom.spdx.json
- name: Store SBOM as artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
retention-days: 365
The Full Pipeline: Putting It Together
Here's what a complete dependency security stage looks like in a CI pipeline:
security:
runs-on: ubuntu-latest
needs: build
steps:
# 1. Scan source dependencies
- uses: actions/checkout@v4
- name: Scan filesystem dependencies
run: trivy filesystem --severity HIGH,CRITICAL --exit-code 1 --ignorefile .trivyignore.yaml .
# 2. Scan the built container image
- name: Scan container image
run: trivy image --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed $IMAGE_REF
# 3. Generate SBOM for the release artifact
- name: Generate SBOM
run: syft $IMAGE_REF -o spdx-json > sbom.spdx.json
# 4. Scan SBOM with a second scanner for coverage
- name: Cross-check with Grype
run: grype sbom:sbom.spdx.json --fail-on critical
# 5. Upload results
- name: Upload scan results
if: always()
run: trivy image --format sarif -o results.sarif $IMAGE_REF
Two scanners, two databases, one SBOM. If both agree a vulnerability exists, you can be confident it's real. If one catches something the other missed, you've gotten value from the overlap.
Stop Negotiating With Known Vulnerabilities
Every day you ship an application with a known critical CVE, you're accepting a risk that someone else has already quantified and published a fix for. The vulnerability database is a gift — it tells you exactly what's wrong and how to fix it before an attacker uses it.
Dependency scanning isn't a nice-to-have. It's the baseline. Get it into your pipeline this week. Start with CRITICAL only, expand to HIGH, build the exception process, and run scheduled rescans. The window between CVE publication and exploitation is shrinking. In 2023, CISA reported that 42% of exploited vulnerabilities were weaponized within 48 hours of disclosure.
Your CI pipeline already gates on test failures. Start gating on known vulnerabilities. The cost of scanning is minutes of build time. The cost of not scanning is an incident response at 2 AM.
Related Articles
DevSecOps Lead
Security-first mindset in everything I ship. From zero-trust architectures to supply chain security, I make sure your pipeline doesn't become your weakest link.
Related Articles
Container Supply Chain Security With Sigstore and Cosign
Sign and verify your container images with Sigstore Cosign to prevent supply chain attacks — with keyless signing, SBOM attestation, and Kubernetes admission enforcement.
Container Image Scanning with Trivy: Complete Setup Guide
Set up Trivy for container image vulnerability scanning — from local development to CI/CD pipeline integration with actionable remediation.
Kubernetes Security Hardening for Production: The Complete Guide
Harden Kubernetes clusters for production with RBAC, network policies, pod security standards, secrets management, and admission controllers.