Skip to main content

All weeks · Worksheet · Overview · Supply-chain notes

Week 12 · Lecture slides

Week 12

Contents21 sections

Software Supply-Chain Security

Software Security · Nutthakorn Chalaemwongwan


Today

  • Why the supply chain is now top-tier
  • Dependency confusion & typosquatting
  • SBOMs, SLSA provenance
  • Signing with Sigstore/Cosign
  • 🎮 Game: Dependency Confusion Heist

The new #1 design risk

  • Your code is ~10% yours, ~90% dependencies
  • One bad package → thousands of victims (xz, event-stream, SolarWinds)
  • OWASP A03:2025 Software Supply Chain Failures

Most of your attack surface is other people's code

Why the supply chain is now the top-tier risk: you write about 10% of what you ship and dependencies are the other ~90%, so every bug in that 90% ships under your name; one poisoned package fans out to thousands of downstream victims (xz-utils 2024, event-stream 2018, SolarWinds 2020); and OWASP promoted this up the list to A03:2025 Software Supply Chain Failures. You can write perfect code and still be owned through a dependency you never read.


Real supply-chain attacks

CaseWhat happened
SolarWinds (2020)trojanized vendor update → 18k orgs
Log4Shell (2021)RCE in a ubiquitous logging dep
event-stream (npm)malicious dep stole crypto-wallet keys
XZ Utils (2024)backdoor planted in liblzma upstream
CircleCI (2023)stolen CI tokens → customer secrets

Attacks are shifting upstream: registry → maintainer → CI/CD.


Attack vectors

  • Typosquatting — reqeusts vs requests; the malicious package's setup.py runs at install time, before any of your code runs — you don't have to import it to be owned
  • Dependency confusion (CWE-1357) — public pkg shadows internal name
  • Malicious updates — compromised maintainer
  • Transitive risk — deps of deps you never chose

Four ways a dependency turns malicious

The four supply-chain attack vectors as a taxonomy: typosquatting (a malicious package named like one you use — reqeusts for requests — whose setup.py runs at install time, before any import, so a single typo is enough to be owned); dependency confusion (CWE-1357, a public package with a higher version shadows your internal name and is pulled instead); malicious updates (a hijacked or compromised maintainer ships a bad version of a package you already trust); and transitive risk (the dependencies of your dependencies, code you never chose or reviewed but still ship). The trust boundary is bigger than your requirements.txt.


Try it — which package actually installs?

Set both versions and the index mode. The resolver's real rule decides.

Which package actually installs? Computed live (Week 12) — open full size

SCA — find vulnerable deps

docker run --rm -v "$PWD:/src" aquasec/trivy fs /src   # this lab's Python deps
pip-audit                                                          # vs PyPI advisories
  • Produces CVEs + fix versions
  • CWE-1104 (unmaintained), CWE-829 (untrusted inclusion), CWE-1395 (known-vulnerable 3rd-party dependency)

Software Composition Analysis turns requirements.txt into a remediation worklist: the pinned-but-outdated deps (Flask 1.1.4, Werkzeug 1.0.1, urllib3 1.26.4, requests 2.25.1) feed two scanners — trivy fs (matches versions to the CVE database) and pip-audit (checks PyPI advisories) — neither of which reads your code, both read your dependency list. The output is one row per vulnerable package with the version that fixes it: Flask → 2.3.2 (CVE-2023-30861, HIGH), Werkzeug → 3.0.3 (CVE-2024-34069, HIGH), urllib3 → 1.26.17 (CVE-2023-43804, HIGH). The contrast: SAST reads the code you wrote and finds your bugs; SCA reads the versions you depend on and finds other people's known, already-patched bugs, so the fix is a version bump, not a code change.


Integrity: prove what you shipped

  • SBOM (CycloneDX/SPDX) — ingredient list of the build
  • SLSA — levels of build provenance & tamper-resistance
  • A08:2025 Software/Data Integrity Failures

Know what's in it, prove how it was built

Two integrity controls side by side. Left, an SBOM (CycloneDX / SPDX): the build's itemized ingredient list — every component and version — so when the next Log4Shell drops you search it and answer "are we affected?" in seconds, not weeks. Right, SLSA: a ladder of build-provenance levels from L0 to L3 — L0 no guarantees, L1 provenance exists but is forgeable, L2 hosted build and signed (tamper-evident), L3 hardened builds whose provenance cannot be forged. SBOM knows what is in it; SLSA proves how it was built. Both map to OWASP A08:2025 Software or Data Integrity Failures.


Signing with Cosign (keyless)

IMG=week12-supplychain:lab
docker run --rm -v "$PWD:/src" -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy:latest image --format cyclonedx \
  --output /src/sbom.cdx.json "$IMG"                          # SBOM
docker run --rm -e COSIGN_EXPERIMENTAL=1 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gcr.io/projectsigstore/cosign:latest sign --yes "$IMG"       # sign (OIDC)
docker run --rm -e COSIGN_EXPERIMENTAL=1 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gcr.io/projectsigstore/cosign:latest verify \
  --certificate-identity-regexp ".*" \
  --certificate-oidc-issuer-regexp ".*" "$IMG"                 # verify
  • Unsigned/tampered image → verification fails
  • Every step needs the docker-socket mount (trivy has to reach the local image, not just a local dir) — the SBOM step is a common copy-paste failure if it's dropped
  • cosign verify needs the two --certificate-*-regexp flags in keyless mode — without them it errors, it doesn't just "fail closed"
  • Keyless signing is backed by Fulcio (short-lived cert authority) + Rekor (public transparency log) — no long-lived private key sitting on disk to leak (CWE-321)

Keyless signing — the flow

How Sigstore keyless signing works with no long-lived private key: cosign sign authenticates via an OIDC identity (GitHub/Google), Fulcio issues a short-lived (~10 min) certificate bound to that identity, cosign signs with an ephemeral key then discards it, and the signature + certificate are recorded in Rekor's public transparency log — so nothing long-lived is ever written to disk and there is no private key to leak (avoiding CWE-321). At deploy, cosign verify checks the signature against the expected identity and the Rekor log: a valid, matching signature is allowed to deploy; an unsigned or tampered image returns "no signatures found" and the gate blocks it.


Verify the signature — does it deploy?

Signed, unsigned, tampered, or signed by someone else. Run the gate and watch.

Does this image get to deploy? Signed, tampered, or wrong signer (Week 12) — open full size

Tooling — GitHub Advanced Security (GHAS)

  • Secret scanning + push protection — block secrets at push time (before they reach the remote)
  • CodeQL code scanning — semantic SAST queries
  • Dependabot — alerts + auto-PRs for vulnerable deps
  • Native in the repo → results in the Security tab

Defenses

  • pip install --require-hashes — a substituted package's hash won't match; install refuses
  • Single --index-url, not --extra-index-url — one trusted index; the resolver won't "shop around" and pick a higher-versioned public package over your internal one
  • Verify signatures before deploy (admission policy)
  • Generate + store SBOMs per release
  • 2FA/MFA on dev/CI/cloud accounts; least privilege
  • Automate SCA in CI (next week)

The whole journey, one dependency

Six hops a dependency crosses from a public registry into production, each with its own attack and the control that answers it: registry (typosquatting → one trusted index), resolver (dependency confusion → scope the namespace), fetch (compromised maintainer → lockfile with hashes), build (stale pins → SCA in CI), image (no inventory → SBOM), deploy (unsigned image → Sigstore keyless gate). "We scanned our code" covers none of this — SAST reads the code you wrote, all six hops are code you didn't.


📦 Game — Dependency Confusion Heist

  1. Attack: no live registry in this lab — use the resolver simulation to watch a higher-version public pkg beat the private one
  2. Defend: pin + scope; generate SBOM; sign & verify with Cosign; add a provenance gate

Deliverable

📋 Worksheet 12 — labs/week12-supply-chain/worksheet.md (Parts 1–4) · kickoff: bash sca_scan.sh (trivy fs + pip-audit)

  • SCA report + remediation plan (Part 3)
  • SBOM file + sign/verify transcript (Part 3)
  • One-paragraph SLSA self-assessment + XZ Utils case analysis (Part 4)
  • + Audit the AI / EiPE / Prompt Problem (required, after Part 4 — see worksheet)

Key takeaways

  • Most of your attack surface is other people's code
  • Know your ingredients (SBOM), prove your build (SLSA), sign your artifacts
  • Verify before you trust

Questions?

Next week: Cloud & container security

All weeks in Software Security 2 (KOSEN)