Dependency Scanning & CI Security Gates
Most application code is not written by the team that ships it — it arrives as transitive dependencies pulled in by a package manager, and any one of them can carry a known, catalogued vulnerability. A dependency scanner reads your resolved dependency graph, matches every package and version against public advisory databases, and reports the ones with a published CVE. A CI security gate turns that report into an enforcement decision: a pull request that introduces a fixable high-severity advisory fails the build before it can merge. This guide is part of Supply Chain & Dependency Security and covers scanner selection per ecosystem, severity thresholds, reachability and allowlists, handling false positives and unfixable transitive advisories, and license compliance — the full lifecycle of blocking vulnerable dependencies in continuous integration. It sits alongside the companion software bill of materials guide, which produces the machine-readable inventory a scanner consumes, and automated dependency updates, which closes the loop by shipping the fixes a scan demands. For end-to-end command-level walkthroughs, two companion guides drill into specific ecosystems: failing npm audit builds on high severity for the Node.js toolchain and gating pip-audit in Python CI for Python.
Threat Anatomy
Using a component with a known vulnerability is a distinct failure mode from writing a new bug. The flaw already exists, it is publicly documented with an exploit maturity rating, and often a proof of concept is a search away. The attacker’s job is reconnaissance, not discovery: fingerprint the target’s dependencies, cross-reference public advisories, and fire a known payload.
Attacker perspective. An adversary enumerates client-side bundle contents, error stack traces, response headers, or a leaked lockfile to identify library versions. Given a version, they consult the same advisory feeds your scanner uses — the GitHub Advisory Database, OSV, or the National Vulnerability Database — to find a matching CVE with a working exploit. Where a compromised package publishes malicious code directly, the supply chain itself becomes the delivery mechanism: the dependency executes attacker-controlled code inside your build or runtime with the privileges of the process.
MITRE ATT&CK coverage. This class maps to T1195.001 — Supply Chain Compromise: Compromise Software Dependencies and Development Tools, under the broader T1195 supply-chain technique. Exploitation of a vulnerable public-facing dependency also touches T1190 (Exploit Public-Facing Application), and malicious install-time scripts align with T1059 (Command and Scripting Interpreter).
Real-world impact. The event-stream incident saw a popular npm package hand off to a new maintainer who injected a payload targeting a specific cryptocurrency wallet — millions of weekly downloads shipped the malicious transitive dependency before it was caught. The Log4Shell vulnerability (CVE-2021-44228) in Log4j 2 turned a ubiquitous logging library into a remote-code-execution vector reachable through ordinary log messages; organisations that could not answer “where is Log4j in our estate?” spent weeks in incident response. Both cases share the lesson that motivates this guide: you cannot defend a dependency you cannot see, and you cannot prioritise a fix you have not gated.
Advisory Source & Scanner Reference
| Scanner | Ecosystem scope | Advisory source | Primary use |
|---|---|---|---|
npm audit |
npm / Node.js | GitHub Advisory Database | Fast native gate for JavaScript lockfiles |
pip-audit |
Python (PyPI) | PyPI Advisory DB + OSV | Native gate for resolved Python requirements |
osv-scanner |
Multi-ecosystem | OSV.dev | Lockfile scanning across languages |
| Trivy | Containers, IaC, lockfiles | Multiple (NVD, GHSA, OSV) | Image + filesystem + dependency scanning |
| Grype | Containers, SBOMs | Anchore feed (NVD, GHSA) | SBOM-driven scanning of built artifacts |
| GitHub Dependency Review | GitHub PRs | GitHub Advisory Database | PR-diff gate on newly introduced advisories |
Prerequisites & Scope
Before wiring a gate, confirm the following are in place:
- Committed lockfiles. A resolved lockfile (
package-lock.json,poetry.lock,requirements.txtwith pinned hashes,go.sum) must exist and be committed. Scanners resolve advisories against exact versions; an unpinned manifest produces non-reproducible, low-value scans. - A dependency inventory. Ideally a generated software bill of materials that enumerates every direct and transitive component with its version and license. Container-based scanners such as Grype can consume this artifact directly.
- A severity policy. A written, versioned decision on which severities and which fixability states block a merge. This is policy as code, not a per-repository judgement call.
- An update pathway. A configured automated dependency updates mechanism so that when a scan demands a bump, a patch pull request already exists or can be opened without manual toil.
- Trust-boundary awareness. An understanding of where third-party code executes with elevated privilege — build agents, install-time scripts, and production runtimes — informed by your trust boundaries model.
Mitigation Architecture
The gate is a decision node on the path from pull request to merge. A scanner reads the resolved dependency graph, a severity gate compares each finding against the policy, and an allowlist provides a controlled bypass for advisories that are triaged and accepted with an expiry. Fixable high or critical findings that are not on the allowlist fail the build.
The allowlist is not a hole in the gate — it is an audited, time-boxed record. Every entry names an advisory, an owner, a justification, and an expiry date, so a suppression cannot silently outlive the reason it was granted.
Step-by-Step Implementation
Step 1 — Choose Scanners per Ecosystem (OWASP ASVS V14.2.1)
Match a scanner to the artifact you are gating. A native ecosystem scanner (npm audit, pip-audit) is fastest for language lockfiles and produces the cleanest advisory mapping. A cross-ecosystem scanner (osv-scanner, Trivy, Grype) covers polyglot repositories, container images, and SBOM inputs. Most teams run both: a native gate on the language lockfile and a broader scan on the built container.
# Native lockfile gates (fast, per-ecosystem)
npm audit --audit-level=high --omit=dev
pip-audit --requirement requirements.lock
# Cross-ecosystem lockfile scan via OSV
osv-scanner scan --lockfile package-lock.json --lockfile poetry.lock
# Container image scan on the built artifact
trivy image --severity HIGH,CRITICAL --exit-code 1 registry.internal/app:${GIT_SHA}
grype registry.internal/app:${GIT_SHA} --fail-on high
For GitHub-hosted repositories, GitHub Dependency Review adds a complementary PR-diff gate that flags only advisories a pull request newly introduces, keeping the signal focused on the change under review.
Step 2 — Set Severity Thresholds and Fixability Policy (OWASP ASVS V14.2.4, NIST SSDF RV.1)
A raw severity threshold alone is too blunt. Combine severity with fixability: block on high or critical advisories that have a patched version available, and route unfixable findings to the allowlist rather than the failure path. Encode the policy as data so it is reviewable and versioned.
# .security/dependency-policy.yml
gate:
fail_on:
severities: [high, critical]
require_fix_available: true # unfixable High/Crit -> allowlist, not fail
warn_on:
severities: [low, moderate]
license:
allowed_spdx: [MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC]
fail_on_copyleft: true
fail_on_unknown: true
allowlist_path: .security/allowlist.json
max_exception_age_days: 90
Step 3 — Wire the CI Gate (SOC 2 CC7.1, PCI-DSS 6.3.1)
Run the scanner on every pull request and let its non-zero exit code fail the job. Surface findings as an uploadable report (SARIF where supported) so results appear in the code-scanning UI rather than buried in job logs.
# .github/workflows/dependency-gate.yml
name: Dependency Security Gate
on: [pull_request]
permissions:
contents: read
security-events: write
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: OSV lockfile scan
uses: google/osv-scanner-action@v1
with:
scan-args: |-
--lockfile=package-lock.json
--format=sarif
--output=osv.sarif
continue-on-error: true
- name: Upload findings to code scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: osv.sarif
- name: Enforce policy gate
run: python .security/enforce_gate.py --report osv.sarif --policy .security/dependency-policy.yml
The scan step uses continue-on-error so findings are always uploaded to the security tab; the separate enforce_gate.py step is the authoritative pass/fail decision that reads the policy and the allowlist.
Step 4 — Manage Exceptions with Expiry (OWASP ASVS V14.2.5, NIST SSDF PW.4)
An exception is a deliberate, documented decision to accept a specific advisory for a bounded period. Store exceptions as structured data, require an expiry, and fail the gate on any entry that has aged out.
{
"exceptions": [
{
"advisory": "GHSA-xxxx-yyyy-zzzz",
"package": "left-pad",
"reason": "Vulnerable code path is unreachable; no fixed version published upstream.",
"reachable": false,
"owner": "platform-security",
"created": "2026-06-01",
"expires": "2026-08-30"
}
]
}
The gate script treats an expired expires date exactly like a fresh finding: the build fails, forcing a re-triage. This prevents the most common failure of allowlists — a suppression added during an incident that quietly becomes permanent.
Edge Cases & Bypass Patterns
Unfixable Transitive Advisory
A vulnerability lives three levels deep in the graph and the direct dependency has not released a bump. Do not disable the gate. Attempt a resolution override (overrides in npm, a constraint pin in Python) to force the nested package to a patched version; if none exists, add an expiring allowlist entry and record that the path is unreachable.
Dev-Dependency Noise
Advisories in build-time tooling that never ship to production inflate the finding count and erode trust in the gate. Scope the runtime gate to production dependencies (--omit=dev, --only=main) and run a separate, lower-severity informational scan across the full graph including dev tooling.
Lockfile Drift and Phantom Resolutions
A scanner reads the lockfile, but the build may resolve differently if the lockfile is stale or ignored. Enforce npm ci / pip install --require-hashes so the installed tree matches the scanned lockfile exactly; a scan of a lockfile that does not match the build is theatre.
Reachability False Positives
A flagged advisory may sit in a code path your application never calls. Reachability analysis (available in some commercial scanners and in osv-scanner’s call-graph mode for supported languages) downgrades these. Without it, treat severity plus fixability as the gate and use the allowlist’s reachable flag to record manual triage.
Suppression Sprawl
Teams under delivery pressure accumulate dozens of ignore entries. Cap exception age in policy (max_exception_age_days), require an owner per entry, and report allowlist size as a tracked metric so the debt stays visible.
Automated Testing & CI Validation
Policy Enforcement Test
# tests/test_gate.py
import datetime
import pytest
from security.enforce_gate import evaluate, load_allowlist
def test_fixable_high_fails(sample_report):
findings = [{"id": "GHSA-aaaa", "severity": "high", "fix_available": True}]
assert evaluate(findings, allowlist=[]) == "fail"
def test_unfixable_high_without_exception_fails(sample_report):
findings = [{"id": "GHSA-bbbb", "severity": "high", "fix_available": False}]
assert evaluate(findings, allowlist=[]) == "fail"
def test_expired_exception_reopens_finding():
finding = {"id": "GHSA-cccc", "severity": "critical", "fix_available": False}
stale = [{"advisory": "GHSA-cccc", "expires": "2020-01-01"}]
assert evaluate([finding], allowlist=stale) == "fail"
CI/CD Gate: Scheduled Full Scan
# .github/workflows/nightly-audit.yml
name: Nightly Dependency Audit
on:
schedule:
- cron: "0 3 * * *"
jobs:
full-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Grype scan of built image
uses: anchore/scan-action@v4
with:
image: "registry.internal/app:latest"
fail-build: true
severity-cutoff: high
- name: Alert on new advisories
if: failure()
run: ./scripts/notify-security-channel.sh
A pull-request gate catches new advisories introduced by a change; a scheduled scan catches advisories published against dependencies you already ship. Both are required — a version that was clean at merge time can be vulnerable tomorrow.
Compliance Mapping
| Framework | Control | Satisfied By |
|---|---|---|
| SOC 2 | CC7.1 | Dependency scan on every PR plus scheduled full scans; findings routed to code scanning and tracked to resolution |
| OWASP ASVS v4.0 | V14.2.1 | Third-party components are inventoried and scanned against a maintained advisory source |
| OWASP ASVS v4.0 | V14.2.4 | Severity-and-fixability threshold policy enforced as a build gate |
| OWASP ASVS v4.0 | V14.2.5 | Exceptions recorded with owner, justification, and expiry |
| NIST SSDF | PW.4 | Reuse of secure, well-maintained components verified before integration |
| NIST SSDF | RV.1 | Continuous identification and analysis of vulnerabilities in delivered components |
| PCI-DSS v4.0 | 6.3.1 | Security vulnerabilities in bespoke and third-party software identified and risk-ranked |
| ISO 27001 | A.8.28 | Secure coding controls extended to third-party dependency management |
Common Pitfalls Checklist
Frequently Asked Questions
Should a dependency scan fail the build or only warn?
Fail on high and critical advisories that have a fixed version, and warn on lower severities or unfixable transitive advisories that are recorded in an expiring allowlist. A warn-only gate produces alert fatigue and never changes behaviour; a gate that fails on every finding gets bypassed under delivery pressure. The workable policy is a hard fail on fixable high or critical findings and a tracked, time-boxed exception for everything else. The failing npm audit builds on high severity guide shows the exact exit-code wiring for Node.js.
How do we handle a transitive CVE with no patched version available?
Confirm the vulnerable code path is unreachable or force a resolution override to a patched nested version, then add the advisory ID to an allowlist entry with an owner and an expiry date. The expiry re-surfaces the finding on a schedule so a stale suppression cannot hide a fix that later ships upstream. Pair this with a configured automated dependency updates flow so the patch lands automatically once it is published.
What is reachability analysis and do we need it?
Reachability analysis determines whether your code actually calls the vulnerable function in a flagged dependency, rather than merely depending on the package. It sharply reduces false positives on advisories that live in unused code paths. It is valuable at scale but is not a prerequisite for a useful gate — a severity plus fixability policy backed by an expiring allowlist is a sound starting point, and you can record manual reachability triage in the allowlist’s metadata.
Where do license compliance checks fit in a dependency gate?
License scanning runs in the same CI job as vulnerability scanning because both operate on the resolved dependency graph. Maintain an allowlist of permitted SPDX license identifiers and fail the build when a newly introduced dependency carries a copyleft or unknown license that conflicts with your distribution model. Consuming a generated software bill of materials makes both the license and the version data available from a single source.
Related
- Software Bill of Materials (SBOM) — generate the machine-readable component inventory a scanner consumes
- Automated Dependency Updates Policy — ship the fixes a scan demands without manual toil
- Failing npm audit Builds on High-Severity Advisories — command-level Node.js gate with an expiring allowlist
- Gating pip-audit in Python CI Pipelines — resolved-requirements scanning with SARIF output
- Supply Chain & Dependency Security — the parent guide covering the full dependency-risk lifecycle