CI dependency scanning: pip-audit, npm audit, fail fast

Lockfile scanners like pip-audit and npm audit are free and fast — the hard part is a CI failure policy strict enough to matter that your team keeps on.

Jun 12, 2025·Updated ·5 min readBeginner·By SecOpsLog · documentation-verified

A dependency gate has one enemy, and it is not the attacker. It is the developer who, after the third build blocked by an unfixable CVE in a transitive package nobody has heard of, adds allow_failure: true and moves on. Once that line is in, the scanner runs forever and protects nothing. The policy below is designed around that person: it fails only on findings with a version to upgrade to, records every exception with a reason and an expiry, and finishes in the time it takes to read the merge request.

bash — a filesystem scan that only reports what can be fixed
trivy fs --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 .
Detected package files: package-lock.json, poetry.lock
CRITICAL lodash 4.17.11 fixed in 4.17.21
HIGH axios 0.21.0 fixed in 0.21.1
Total: 2 (HIGH: 1, CRITICAL: 1) — 14 unfixed findings hidden by --ignore-unfixed
exit status 1: both have an upgrade path, so the build should fail

Two flags carry the policy

--severity HIGH,CRITICAL removes the noise floor. --ignore-unfixed removes the findings nobody can act on: a CVE with no fixed version in the distribution or registry yet. What remains always has a next step (bump the version, rebuild, re-run), which is the property that keeps the gate switched on. --scanners vuln keeps secret and misconfiguration scanning out of this job so its runtime and its failures mean one thing. Pin the scanner image; a floating latest on the tool that enforces your supply-chain policy is its own supply-chain problem.

.gitlab-ci.yml
dep-scan:
stage: test
image:
name: aquasec/trivy:0.74.0
entrypoint: [""]
variables:
TRIVY_CACHE_DIR: .trivycache/
cache:
key: trivy-db
paths: [.trivycache/]
script:
- trivy fs --scanners vuln
--severity HIGH,CRITICAL
--ignore-unfixed
--ignorefile .trivyignore.yaml
--exit-code 1 .
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"

Trivy reads lockfiles (package-lock.json, poetry.lock, go.sum, Gemfile.lock and others), so the scan is only as honest as the lockfile is committed and current. The vulnerability database is pulled as an OCI artifact, first from mirror.gcr.io/aquasec and then from ghcr.io/aquasecurity; caching .trivycache/ between jobs removes that download from every run and, on a busy runner fleet, keeps you away from registry rate limits. For a Python-only repository pip-audit -r requirements.txt applies the same policy with --ignore-vuln for exceptions; the tool matters less than the two rules.

Go deeper in a courseSecure CI/CD with GitLabWhere this gate sits among the other stages: build, scan, sign, gate, deploy.View course

Suppress with a reason and a date, never with a lower bar

A finding you have assessed as not reachable in your context is a risk acceptance, and a risk acceptance has an owner, a reason and a review date. Trivy's YAML ignore file carries all three: statement records the reason, expired_at makes the exception lapse on a date, and paths or purls limit it to the package it was assessed for. When the date passes the finding reappears and the build fails again, which is the review reminder working. Lowering the global severity to make the same finding disappear would also hide the next real one.

.trivyignore.yaml
vulnerabilities:
- id: CVE-2023-45853
purls:
- "pkg:deb/debian/zlib1g"
statement: "Only reachable through a build-time tool that never sees untrusted input. Reviewed by security."
expired_at: 2026-12-01
# The YAML form is only read when passed explicitly: --ignorefile .trivyignore.yaml
# --show-suppressed prints what this file hid, for the audit.

Write the SBOM now, answer the question later

The scan answers "is this build affected by what is known today". The question that arrives at 17:00 on a Friday is "which of our 80 services contains the library that was disclosed an hour ago", and re-scanning 80 repositories is the wrong way to find out. trivy fs --format cyclonedx --output sbom.cdx.json . on the same checkout produces a bill of materials per build; stored as an artifact and indexed by image tag, it turns that question into a search.

What this gate does and does not cover
Covers
Known CVEs in declared dependencies
Only fixable findings block the merge
Exceptions with owner, reason, expiry
A per-build inventory for later questions
Does not cover
CVEs disclosed after the build (registry or runtime scanning)
Vulnerabilities in your own code (SAST)
Malicious packages with no CVE yet
Anything not in a lockfile

The container image built from this checkout gets the same treatment one stage later, with the OS packages added: image scanning with Trivy in GitLab CI covers the build-scan-gate-push ordering, and SBOMs with Syft covers the inventory side for images rather than source trees.

Related posts

Quick reference