Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXCVI · Incident: Malicious DependencyIncidentResponse

Improve dependency controls — lockfile enforcement, scan-on-PR

Advanced⏱ ~28 mingit

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

Not yet marked complete on this device.

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

  1. npm ci in CI, never npm install. The lockfile is the source of truth.
  2. Scan as a required check. The scan fails the merge.
  3. Registry allowlists in version control.
  4. 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

  1. 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?

  2. 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.

  3. Q3. Name the four prevention layers for a malicious-dependency incident and state what each layer addresses.

  4. 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.