Skip to main content
Security for Application Engineers

Dependency and Supply Chain

Ravinder··6 min read
SecurityAppSecSupply ChainSBOMDependencies
Share:
Dependency and Supply Chain

The SolarWinds breach in 2020 and the XZ Utils backdoor in 2024 made supply chain attacks front-page news. But most supply chain risks engineering teams face are not sophisticated nation-state operations — they are mundane: a popular npm package abandoned and then re-registered by a malicious actor, a transitive dependency pulling in a version with a known CVE, a Docker base image built from a vulnerable OS layer that was never updated.

The dependency graph of a modern application is enormous and mostly invisible to the team that ships it. This post is about making it visible and controllable.

Your Actual Attack Surface

When you npm install express, you are not installing one package. You are installing a tree of packages, each of which has its own maintainers, its own release process, and its own potential for compromise.

graph TD App["Your Application"] App --> A["express@4.x\n(direct dependency)"] App --> B["lodash@4.x\n(direct dependency)"] A --> C["accepts@1.x"] A --> D["body-parser@1.x"] D --> E["bytes@3.x"] D --> F["iconv-lite@0.4.x"] C --> G["mime-types@2.x"] G --> H["mime-db@1.x"] B -.->|"no transitive deps"| B style App fill:#4a9eff,color:#fff style A fill:#7bc8a4,color:#000 style B fill:#7bc8a4,color:#000 style C fill:#f0c040,color:#000 style D fill:#f0c040,color:#000 style E fill:#e88,color:#000 style F fill:#e88,color:#000 style G fill:#e88,color:#000 style H fill:#e88,color:#000

You directly control two packages. You transitively depend on six more. Each transitive package can introduce vulnerabilities, and you may not notice until a scanner surfaces a CVE weeks later.

Lockfiles Are Not Optional

Lockfiles (package-lock.json, yarn.lock, poetry.lock, Cargo.lock) pin the exact resolved version of every direct and transitive dependency. Without them, npm install on CI may resolve a different (potentially vulnerable or compromised) version than the one your developer tested.

# npm — always commit the lockfile, always use it in CI
npm ci         # installs from lockfile exactly; fails if lockfile is out of date
 
# Python — Poetry lockfile
poetry install --no-root  # uses poetry.lock exactly

Do not delete or ignore the lockfile in .gitignore. It is part of your security posture.

Automated CVE Scanning

Integrate scanning in CI so CVEs in dependencies block merges before they reach production.

# GitHub Actions — using Trivy for dependency and container scanning
name: Security Scan
 
on: [push, pull_request]
 
jobs:
  trivy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Scan filesystem (dependencies)
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          scan-ref: .
          severity: HIGH,CRITICAL
          exit-code: 1          # fail the build on HIGH/CRITICAL
          ignore-unfixed: true  # skip CVEs with no fix available yet
 
      - name: Scan container image
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: image
          image-ref: myapp:${{ github.sha }}
          severity: HIGH,CRITICAL
          exit-code: 1

Scanning at PR time catches new vulnerabilities introduced by a dependency update. Also run scans on a schedule (nightly) to catch CVEs disclosed after the last PR.

Generating and Consuming SBOMs

A Software Bill of Materials (SBOM) is a machine-readable inventory of every component in your software. It lets consumers of your software (or your own security team) quickly answer "are we affected by CVE-XXXX-YYYY?"

SBOM formats: SPDX (Linux Foundation) and CycloneDX (OWASP) are the two standards. Pick either; most tools support both.

# Generate an SBOM for a Node project with Syft
syft packages . -o cyclonedx-json > sbom.cyclonedx.json
 
# Generate an SBOM for a Python project
syft packages . -o spdx-json > sbom.spdx.json
 
# Generate an SBOM for a container image
syft packages myapp:latest -o cyclonedx-json > sbom-container.cyclonedx.json
 
# Scan an SBOM for vulnerabilities with Grype
grype sbom:./sbom.cyclonedx.json --fail-on high

Publish your SBOM as a build artifact. Attach it to your container image with cosign attest. When a new CVE is disclosed, you can query your published SBOMs to immediately know which services are affected.

Artifact Signing with Sigstore/cosign

Signing build artifacts creates a verifiable chain from source code to deployed artifact. Anyone who pulls your container image can verify it was built by your CI pipeline and not tampered with in transit.

# In CI — sign the container image after push (keyless, using OIDC)
cosign sign --yes myregistry.io/myapp:${GITHUB_SHA}
 
# Sign the SBOM attestation
cosign attest --yes --predicate sbom.cyclonedx.json \
  --type cyclonedx \
  myregistry.io/myapp:${GITHUB_SHA}
 
# In deployment — verify before pulling
cosign verify \
  --certificate-identity "https://github.com/myorg/myapp/.github/workflows/build.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  myregistry.io/myapp:${GITHUB_SHA}

Sigstore keyless signing uses the OIDC token from GitHub Actions (or other CI) as the signing identity — no long-lived signing keys to manage or rotate.

Typosquatting and Dependency Confusion

Two supply chain attack patterns that scanners will not catch because the malicious package has no known CVE:

Typosquatting: reqeusts instead of requests, colourama instead of colorama. Always verify the package name before adding a dependency.

Dependency confusion: Your internal package registry has a package named myorg-utils. If your package manager is configured to check the public registry first, an attacker can publish myorg-utils to npm/PyPI with a higher version number — your CI installs the attacker's version.

# npm — scoped packages prevent confusion for internal packages
# Use @myorg/ prefix; npm.js does not serve scoped packages to non-org registries
npm install @myorg/utils
 
# .npmrc — configure private registry for specific scope
@myorg:registry=https://nexus.internal.example.com/repository/npm/

For Python, configure pip to use your internal index exclusively for internal packages:

# pip.conf
[global]
index-url = https://nexus.internal.example.com/repository/pypi/simple/
extra-index-url = https://pypi.org/simple/
 
# Or use --no-index for fully air-gapped environments

Pinning Base Images

A Docker image built FROM node:20 may pull a different OS layer next week than it did today. Pin to a specific digest:

# BEFORE — version tag can change
FROM node:20-alpine
 
# AFTER — pinned to a specific immutable image digest
FROM node:20-alpine@sha256:a1b2c3d4e5f6...
 
# Or use a specific image version that maps to a digest
FROM node:20.11.1-alpine3.19

Automate base image updates with Dependabot or Renovate so your pinned version is updated regularly with security patches while still being deterministic:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: docker
    directory: "/"
    schedule:
      interval: weekly
  - package-ecosystem: npm
    directory: "/"
    schedule:
      interval: weekly
    open-pull-requests-limit: 10

Key Takeaways

  • Your transitive dependency tree is your real attack surface — direct dependencies are a small fraction of the packages you run in production.
  • Lockfiles are a security control: commit them, use npm ci or equivalent in CI, and treat lockfile changes as code changes requiring review.
  • Automated CVE scanning in CI on both filesystem dependencies and container images is the minimum baseline; add nightly scheduled scans to catch disclosures after merge.
  • SBOMs let you answer "are we affected by this CVE?" across your entire fleet in minutes rather than days — generate and publish them as standard build artifacts.
  • Sigstore/cosign keyless signing creates a verifiable chain from source to deployed artifact without managing long-lived signing keys.
  • Typosquatting and dependency confusion attacks bypass CVE scanners — use scoped packages, configure registry routing explicitly, and review new dependency additions carefully.
Share: