Git, CI/CD & GitOpsXCVI · Incident: Malicious DependencyIncidentResponse
Improve dependency controls — lockfile enforcement, scan-on-PR
What you'll learn
- Identify the four layers of malicious-dependency prevention: lockfile enforcement, scan-on-PR, registry allowlists, SBOM baseline diff
- Configure `npm ci` over `npm install` and `pip install --require-hashes` so the lockfile is the source of truth
- Wire `npm audit`, `pip-audit`, and `trivy fs --security-checks vuln .` into the PR pipeline so a malicious advisory fails the build
- Diff every SBOM against a known-good baseline so a new transitive dependency is detected before it ships
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
The prevention controls for a malicious-dependency incident are layered: lockfile enforcement, scan-on-PR, registry allowlists, and SBOM baseline diff. Each layer catches a compromise path the previous layer did not address. A team that implements one layer is not safe.
The four prevention layers
A permissive resolver is a resolver the attacker can trick. An unscanned PR is a PR the attacker can use. An unrestricted registry is a registry the attacker can substitute:
flowchart LR
A["compromise prevention"] --> B["lockfile enforcement"]
A --> C["scan-on-PR"]
A --> D["registry allowlists"]
A --> E["SBOM baseline diff"]
B --> F["no resolver drift"]
C --> G["no unscanned transitive"]
D --> H["no registry substitution"]
E --> I["no undetected transitive"]
- Lockfile enforcement. The install command resolves against the lockfile exactly.
- Scan-on-PR. Every PR runs a vulnerability scan.
- Registry allowlists. The build pulls only from vetted registries.
- SBOM baseline diff. Every build produces an SBOM compared against a baseline.
Lockfile enforcement
A lockfile pins the team’s own resolution; the lockfile does not prevent the registry from serving a different package under the same name and version. The discipline is to use an install command that resolves against the lockfile exactly:
npm ci
pip install --require-hashes -r requirements.lock
npm ci reinstalls from the lockfile exactly; npm install may update the lockfile. The CI command is npm ci; the local development command is npm install.
Scan-on-PR
Every PR that changes a lockfile runs a vulnerability scanner:
npm audit
pip-audit -r requirements.lock
trivy fs --security-checks vuln .
The scan runs as a required check on the PR; a malicious advisory fails the merge.
Registry allowlists
The build is configured to pull only from registries the team controls:
# .npmrc
registry=https://registry.npmjs.org/
# pip.conf
[global]
index-url = https://pypi.org/simple/
The allowlist must be checked into version control and reviewed on every change.
SBOM baseline diff
Every build produces an SBOM; the SBOM is compared against a known-good baseline:
trivy fs --format cyclonedx --output sbom-current.json .
diff sbom-baseline.json sbom-current.json
A divergence is an alert: a new transitive, a version bump, or a removed transitive.
Operational discipline
npm ciin CI, nevernpm install. The lockfile is the source of truth.- Scan as a required check. The scan fails the merge.
- Registry allowlists in version control.
- SBOM diff on every build. A new transitive is an alert.
Cross-course references
- Git, CI/CD & GitOps — Part LXVII-06 (Package Pinning and Lockfiles) covers the lockfile shape.
- Git, CI/CD & GitOps — Part LXVI-02 (Action and Plugin Pinning) covers the SHA-pinning pattern.
- Container Security for Production Sysadmins — Part V (Image Scanning) covers the registry-side validation patterns.
Quiz
Knowledge check · 4 questions
Q1. A team runs `npm install` in CI instead of `npm ci`. A new transitive dependency with a malicious advisory is published. The team's lockfile pins to the previous version, but the CI command resolves against `package.json` and picks up the malicious version. Which prevention layer failed?
Q2. A team that runs `npm audit` as an informational log step but does not fail the merge on advisories has implemented scan-on-PR.
Q3. Name the four prevention layers for a malicious-dependency incident and state what each layer addresses.
Q4. A production team uses range constraints in `package.json`, commits the lockfile, runs `npm install` in CI, and does not scan PRs. After a malicious-dependency incident, the team wants to prevent recurrence. Design the prevention layers and identify the failure mode of skipping the registry allowlist.
The current setup uses range constraints (`^1.2.0`, `~3.6`) in `package.json`. The lockfile is committed. The CI pipeline runs `npm install` instead of `npm ci`. The PR pipeline runs `npm audit` as an informational log step. No registry allowlist is configured. No SBOM baseline exists. A malicious transitive was introduced via a resolver drift between two consecutive builds.
Passing score: 75%. Answers are checked in this browser.