Software Supply Chain Security Guide#
A defender’s reference for software supply chain risks — threat model across the SDLC, package-registry attack patterns, CI/CD hardening, artifact provenance and signing, SBOMs, dependency scanning, case studies, and a checklist. Compiled from 54 research articles, advisories, and incident writeups in raw/Supply Chain/.
Table of Contents#
- Fundamentals
- Threat Model Across the SDLC
- Package Registry Risks
- Dependency Confusion, Typosquatting, Slopsquatting
- Maintainer Account Compromise
- CI/CD Pipeline Hardening
- Container Image Provenance & Verification
- SLSA Framework
- Sigstore, Cosign, in-toto
- SBOMs (SPDX, CycloneDX)
- Dependency Scanning Tooling
- Developer Host Hardening
- Admission Control & Runtime Verification
- Case Studies — Defensive Lessons
- Detection Signals & IOCs
- Defender Checklist
- Reference Configurations
1. Fundamentals#
A software supply chain attack compromises a dependency, tool, build system, or distribution channel that the target trusts, rather than attacking the target directly. The malicious payload rides in on a routine npm install, pip install, docker pull, or CI build — bypassing perimeter defenses because the artifact appears legitimate.
Why the category exploded:
| Factor | Effect |
|---|---|
| Modular package ecosystems | One compromised transitive dep can reach thousands of downstream apps |
| ~85% of enterprises use OSS | Huge shared attack surface |
| Registries default to “latest” | Window between publish and detection is the blast radius (Chalk attack: 16 min from account access to 18 packages weaponized; 2 hrs live before removal, yet reached 10% of cloud environments) |
Lifecycle scripts (postinstall, prepare) | Arbitrary code executes during install, before any runtime control |
| AI-suggested dependencies | ~20% of LLM-suggested packages do not exist — creates slopsquatting openings |
| CI/CD secrets co-located with builds | Stealing them grants further publishing rights → worm behavior |
OWASP Top 10 2025 — A03 Software Supply Chain Failures:
| Metric | Value |
|---|---|
| Community ranking | #1 (50% of respondents) |
| Avg incidence rate | 5.19% (highest in Top 10) |
| Mapped CWEs | 477, 1035, 1104, 1329, 1357, 1395 |
Growth: Supply chain attacks on package registries surged 73% in 2025–2026. Sonatype tracked a 742% increase between 2019 and 2022. OSSF and Bastion 2026 reports attribute the spike to nation-state actors (notably DPRK clusters publishing ~1,700 malicious packages across npm, PyPI, Go, and Rust), credential-stealing worms, and industrialized typosquatting.
2. Threat Model Across the SDLC#
Map threats to lifecycle stages. Each stage has distinct actors, assets, and controls.
| Stage | Assets | Representative Threats | Primary Controls |
|---|---|---|---|
| Develop | Source code, IDEs, dev workstations, SSH/GPG keys | Stolen creds, IDE extensions (Glassworm, OpenVSX), malicious packages on dev host, AI slopsquatting | MFA, commit signing, dev-host hardening, release-age gates, disabled install scripts |
| Source | Git repo, branches, PRs, code review | Force-pushed tags, compromised reviewer, self-merge, secret commit | Branch protection, required reviews, signed commits/tags, CODEOWNERS, secret scanning |
| Build | CI runners, build tools, cache, ephemeral creds | Poisoned runner, compromised action/plugin, cache poisoning, post-build provenance forgery | Ephemeral isolated runners, pinned action SHAs, SLSA L3 hosted builds, OIDC federation, egress allowlist |
| Package | Artifacts, signatures, SBOMs, attestations | Unsigned release, forged provenance, hidden transitive dep injection | cosign sign, in-toto attestations, SLSA provenance, SBOM attached |
| Distribute | Registries (npm/PyPI/GHCR/Docker Hub), mirrors, CDN | Account takeover, typosquat, dep confusion, registry abuse, force-push image tag | Scoped tokens, 2FA, publisher policies, repository firewall, signed pulls |
| Deploy | Kubernetes, cloud, admission policies | Unsigned image rollout, drift, sidecar injection | Admission controllers (Kyverno/OPA/Gatekeeper), signature verification, image allowlists |
| Runtime | Running containers, nodes, secrets stores | Backdoor C2, token exfil via metadata, lateral movement | eBPF runtime, egress filtering, metadata service IMDSv2, least-priv IAM |
STRIDE per stage: Every stage maps to Spoofing (forged provenance), Tampering (injected code), Repudiation (missing attestations), Information disclosure (leaked secrets via install scripts), DoS (dependency deletion à la left-pad), and Elevation (signing-key theft).
Blast-radius formula:
blast_radius = trust_score(package) × downstream_installs × window_before_detection × privilege(install_context)
Reduce any factor. Release-age gates attack window_before_detection; ignore-scripts attacks privilege(install_context); pinning attacks trust_score drift.
3. Package Registry Risks#
Registry-by-registry quick reference#
| Registry | Language | Notable attack patterns (2025–2026) | Key defender features |
|---|---|---|---|
| npm | JavaScript/TypeScript | Chalk/Debug, Shai-Hulud/Shai-Hulud 2.0, Axios, Nx/s1ngularity, CanisterWorm, Glassworm | min-release-age, ignore-scripts, 2FA, scoped publish tokens, trusted publishers via OIDC |
| PyPI | Python | LiteLLM, Telnyx, Ultralytics (GH Actions script injection → coinminer), DPRK ghost-package swarms, wallet stealers | 2FA mandatory for top projects, Trusted Publishers (OIDC), --require-hashes, revoke unused API tokens alongside Trusted Publishers |
| Maven Central | Java | Typosquats of groupIds, unsigned POMs | GPG signing mandatory, namespace verification |
| RubyGems | Ruby | Strong_password-style hijacks | MFA, bundle config set --global frozen true |
| Go proxy | Go | Typosquat import paths, DPRK-seeded modules | Module proxy immutability, go.sum verification, GOSUMDB |
| crates.io | Rust | Typosquats, DPRK-seeded crates | Lockfile enforcement, cargo vet, cargo-audit |
| Docker Hub / GHCR | Containers | Hijacked image tags, base-image tampering, Trivy v0.69.4 force-push | Signed images, content trust, immutable digests |
| VS Code Marketplace / OpenVSX | IDE extensions | Glassworm, TeamPCP OpenVSX | Extension signing (preview), allowlists |
| GitHub Actions | CI actions | Tag repoint, compromised action → secret dump, tj-actions/changed-files + reviewdog cascading compromise (CVE-2025-30066/CVE-2025-30154), SpotBugs → reviewdog → tj-actions chain, log-based exfil | Pin by SHA, permissions: read-all, OIDC, allowlisted actions, audit contributor team privileges, avoid pull_request_target with unsanitized inputs |
Attack pattern taxonomy#
| Pattern | Description | Defender signal |
|---|---|---|
| Direct malware upload | Net-new package with malicious payload | Behavioral scanning (Socket.dev), new-publisher heuristics |
| Typosquat | Name differs by 1–2 chars from popular package | Levenshtein monitoring, registry deny-list |
| Dependency confusion | Public package with same name as private internal | Namespace private scopes (@org/*), lockfile + private-registry priority |
| Transitive injection | Legitimate package quietly pulls new malicious dep | Lockfile diffs, SBOM diff alerts |
| Account takeover | Phishing maintainer → legit publish | Publisher-change alerts, 2FA enforcement |
| Worm / self-propagation | Payload steals tokens → republishes | Anomalous publish events, new runner names, post-install egress |
| Force-push tag repoint | Git tag moved to malicious commit | Signed tag verification, tag immutability |
| Install-time execution | postinstall / setup.py runs payload on install | ignore-scripts, sandboxed installs, CI egress allowlist |
| GH Actions script injection | Unsanitized pull_request_target input (e.g., PR title) → arbitrary code in build | zizmor workflow linter, avoid pull_request_target, sanitize all user-controlled inputs in workflow expressions |
| Cascading action compromise | Compromise action A → steal creds → compromise action B that depends on A | Pin all composite action deps by SHA, audit transitive action dependencies |
| Log-based exfiltration | Secrets dumped to CI workflow logs instead of external C2 | Audit log scrubbing, restrict public repo workflow log visibility |
| Dead man’s switch / destructive fallback | Worm deletes user home directory if exfil + propagation fail | EDR alerting on rm -rf ~, filesystem monitoring |
| AI-tool exploitation | Malware invokes local AI CLI tools (Claude, Gemini, Q) with --dangerously-skip-permissions to enumerate secrets | Review AI tool permissions, disable --yolo flags |
| Protestware / maintainer sabotage | Maintainer introduces destructive logic | Behavioral scanning, diff review of minor versions |
| Slopsquatting | LLM hallucinates package name → attacker registers it | Verify AI-suggested packages before install |
4. Dependency Confusion, Typosquatting, Slopsquatting#
Dependency confusion (Birsan 2021)#
Occurs when a package manager prefers public registries over private ones for a name used both internally and publicly.
| Root cause | Fix |
|---|---|
| Package manager falls back to public when private lookup fails or private registry is unreachable | Configure scoped registries; hard-fail if internal package not found in private index |
Internal package names leaked in package.json / requirements.txt | Use @org/name scopes on npm; prefix on PyPI; private Maven groupIds |
| Higher version on public wins | Pre-register internal names on public registry as “security holder” stubs |
npm mitigation:
# .npmrc
@myorg:registry=https://nexus.myorg.local/repository/npm-private/
registry=https://registry.npmjs.org/
always-auth=true
pip / uv mitigation:
# pip.conf — single index, no extra-index-url fallback
[global]
index-url = https://artifactory.myorg.local/artifactory/api/pypi/pypi/simple/
Avoid --extra-index-url — it merges indices and picks highest version. Use --index-url to a repository-manager virtual registry that proxies public content behind policy.
Typosquatting#
| Detection lever | Tool / technique |
|---|---|
| Name similarity to top-1000 packages | Socket.dev, Phylum, Sonatype Repository Firewall |
| New package + suspicious postinstall | Aikido, GitGuardian |
| Namespace deny-list in repo manager | Nexus / Artifactory policies |
| Lockfile diff alerts | GitHub Actions: compare package-lock.json PR diffs |
Slopsquatting (AI-coined)#
LLMs regularly hallucinate plausible-sounding package names. Attackers squat those names. Defensive verification before installing any AI-suggested package:
# npm
npm view <pkg> time.created versions maintainers --json
# PyPI
curl -s https://pypi.org/pypi/<pkg>/json | jq '{name:.info.name,author:.info.author,home_page:.info.home_page,releases:(.releases|keys|length)}'
Red flags: created <30 days ago, single maintainer, no linked repo, download count in tens, no release history.
5. Maintainer Account Compromise#
Phishing an npm/PyPI maintainer became the single most prolific entry vector in 2025–2026 (Chalk, Debug, Axios, LiteLLM, Telnyx, Shai-Hulud, Shai-Hulud 2.0).
| Phase | Attacker action | Defender control |
|---|---|---|
| Recon | Identify maintainers of high-traffic packages | Public — accept |
| Phishing | Lookalike npm/PyPI login page, OAuth consent phishing | Hardware-backed 2FA (WebAuthn), phishing-resistant MFA, allowlisted OAuth apps |
| Publish | Use legitimate account to push malicious version | Trusted Publishers (OIDC from CI only), publish-alerts, velocity anomaly detection |
| Propagate | Stolen tokens from infected victims used to publish new packages (worm) | Short-lived tokens, token scoping per package, detect new runner names (e.g. SHA1HULUD) |
Key principle: a legitimate publisher account with a valid 2FA bypass is indistinguishable from the real maintainer at the artifact level. The only durable defense is post-publish attestation verification (SLSA provenance + Sigstore identity) tied to a CI workflow identity rather than a human account.
6. CI/CD Pipeline Hardening#
CI/CD is high-value: it holds secrets, has network egress, and signs artifacts. Compromising it turns it into a factory for malicious releases.
Control baseline#
| Control | Rationale |
|---|---|
| Ephemeral runners | No state between builds; no cache contamination |
| Isolated builds (SLSA L3) | User build steps cannot touch signing material |
| OIDC federation (no long-lived cloud creds) | Revokes secret exfil value; token valid minutes |
| Pin GitHub Actions by SHA | uses: actions/checkout@b4ffde65... not @v4; defeats tag repoint |
Minimal permissions: | Default contents: read; grant id-token: write only when signing |
| Read-only filesystem | Limits payload persistence |
| Egress allowlist on runners | Blocks curl-to-C2, anomalous registries |
| Secret masking + short-lived tokens | Shrinks exfil window |
| Separation of duties | No single human pushes code → prod |
| Signed commits/tags enforced by branch protection | Attacker cannot force-push malicious tag without signing key |
| Runner fingerprinting | Detect rogue self-hosted runners (e.g., SHA1HULUD) |
| Audit log export + alerting | Detect anomalous workflow_dispatch, new deploy keys |
Restrict pull_request_target | Never pass unsanitized PR titles/branch names into run: expressions; use zizmor to lint workflows (Ultralytics root cause) |
| Audit composite action dependencies | Composite actions inherit transitive deps — a compromised reviewdog/action-setup infected tj-actions/eslint-changed-files which infected tj-actions/changed-files |
| Contributor team privilege review | Automated invitations to maintainer teams enabled the reviewdog compromise; review team membership and write access regularly |
| Tag immutability enforcement | The tj-actions attacker re-pointed all version tags (v1–v44) to a single malicious commit; consider GitHub tag protection rules |
| Workflow log access control | Log-based exfiltration worked because public repos expose workflow logs; restrict log retention and visibility |
GitHub Actions hardened job skeleton#
name: build-and-release
on:
push:
tags: ['v*']
permissions:
contents: read
id-token: write # for keyless cosign + OIDC to cloud
packages: write # scoped; only if publishing to GHCR
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 pinned SHA
with:
persist-credentials: false
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
cache: 'npm'
- run: npm ci --ignore-scripts
- run: npm run build
- name: Generate SBOM
uses: anchore/sbom-action@d94f46e13c6c62f59525ac9a1e147a99dc0b9bf5
with:
format: cyclonedx-json
output-file: sbom.cdx.json
- name: Scan SBOM
uses: anchore/scan-action@be7a22da4f22dde4c7c9e0b14dea0d0f7a4a16a3
with:
sbom: sbom.cdx.json
fail-build: true
severity-cutoff: high
- name: Build & push image
id: build
run: |
IMAGE=ghcr.io/${{ github.repository }}:${{ github.ref_name }}
docker build -t "$IMAGE" .
docker push "$IMAGE"
echo "digest=$(docker inspect --format='{{index .RepoDigests 0}}' $IMAGE)" >> "$GITHUB_OUTPUT"
- name: Sign image (keyless)
env:
COSIGN_EXPERIMENTAL: "1"
run: cosign sign --yes ${{ steps.build.outputs.digest }}
- name: Attest SBOM
run: |
cosign attest --yes --predicate sbom.cdx.json \
--type cyclonedx ${{ steps.build.outputs.digest }}
- name: Attest SLSA provenance
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
with:
image: ${{ steps.build.outputs.digest }}
Runner egress allowlist (example)#
registry.npmjs.org
pypi.org
files.pythonhosted.org
ghcr.io
*.actions.githubusercontent.com
fulcio.sigstore.dev
rekor.sigstore.dev
oauth2.sigstore.dev
Block everything else. Most supply-chain malware phones home on install — an allowlist surfaces it immediately.
7. Container Image Provenance & Verification#
Container images are the delivery unit and must be identified by digest, not tag.
| Bad | Good |
|---|---|
FROM node:20 | FROM node:20.11.1-alpine3.19@sha256:7c3... |
docker pull myimage:latest | docker pull myimage@sha256:… |
| Tag-based Kubernetes rollout | Digest-pinned manifests + admission verification |
Base image hygiene#
| Practice | Why |
|---|---|
| Prefer distroless / chainguard / wolfi base images | Smaller attack surface, signed by producer |
| Rebuild on base CVE, not periodically | Provenance ties to cause |
Pin base by digest in FROM | Defeats silent tag repoint |
| Scan final image with Grype/Trivy | Catches inherited CVEs |
| Verify base image signature in CI | cosign verify cgr.dev/chainguard/static@sha256:… |
cosign image sign + verify#
# Keyless sign (uses OIDC → Fulcio → Rekor)
COSIGN_EXPERIMENTAL=1 cosign sign --yes \
ghcr.io/myorg/app@sha256:abcd...
# Verify against expected CI workflow identity
cosign verify \
--certificate-identity-regexp="^https://github.com/myorg/app/\.github/workflows/release\.yml@refs/tags/v.*$" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
ghcr.io/myorg/app@sha256:abcd...
The identity regex is the critical verification step — it binds the signature to a specific workflow file in a specific repo, so a compromise of a different repo cannot produce a valid signature for this image.
8. SLSA Framework#
SLSA (Supply-chain Levels for Software Artifacts, “salsa”) gives a vocabulary and progressive maturity levels for build integrity. Developed originally from Google’s Binary Authorization for Borg, donated to OpenSSF in 2021, now at v1.1.
Build track levels#
| Level | Name | Requirements | Protects against |
|---|---|---|---|
| L0 | No SLSA | Nothing | Nothing |
| L1 | Provenance exists | Scripted build, auto-generated provenance | Accidental wrong artifact; incident traceability |
| L2 | Signed provenance | L1 + cryptographically signed by hosted build platform | Post-build forgery; builds from dev workstations |
| L3 | Hardened builds | L2 + isolated, ephemeral runners; signing keys inaccessible to build user code; non-forgeable provenance | Cross-build contamination, insider tampering, signing-key theft by build step |
Note: SLSA v1.0+ consolidated former L4 — hermetic and reproducible builds are now recommended practices, not strict requirements.
Provenance attack vectors addressed#
| Vector | Without SLSA | With SLSA L3 |
|---|---|---|
| Source tampering | Hidden in commit; hard to detect | Provenance binds artifact to commit digest |
| Build tampering | Undetectable mid-build | Hardened runner isolates build from signer |
| Dependency poisoning | Installed silently | Provenance lists dependencies used |
| Provenance forgery | Trivial | Non-forgeable; signed by platform, not user |
| Compromised credentials | Used to publish | OIDC-scoped short-lived, verifiable |
Minimal in-toto / SLSA provenance document#
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{
"name": "ghcr.io/myorg/app",
"digest": {"sha256": "abcd1234..."}
}],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://github.com/slsa-framework/slsa-github-generator/container@v1",
"externalParameters": {
"repository": "https://github.com/myorg/app",
"ref": "refs/tags/v1.2.3"
},
"resolvedDependencies": [
{"uri": "git+https://github.com/myorg/app", "digest": {"gitCommit": "f00..."}}
]
},
"runDetails": {
"builder": {
"id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.0.0"
},
"metadata": {
"invocationId": "https://github.com/myorg/app/actions/runs/1234567890/attempts/1",
"startedOn": "2026-04-01T12:00:00Z"
}
}
}
}
SLSA 3 for Go modules (GitHub Actions + Sigstore)#
GitHub provides a reusable workflow (slsa-framework/slsa-github-generator-go) that automates SLSA L3 for Go projects. The workflow uses the Actions OIDC token to bind provenance to the exact repository, branch, commit, and workflow file. Cosign + Fulcio issue a short-lived certificate from the OIDC identity, and Rekor logs the signing event. No long-lived signing keys are needed. This pattern is extensible to other ecosystems via the generic slsa-github-generator.
Consumer verification workflow#
- Download artifact + provenance bundle.
- Verify Sigstore signature against certificate transparency log (Rekor).
- Check
builder.idmatches allowlisted trusted builder. - Match artifact digest against
subject.digest. - Match
externalParameters.repositoryandrefagainst expected. - Policy engine (Kyverno / Conftest / OPA) approves or rejects.
9. Sigstore, Cosign, in-toto#
Component map#
| Component | Role |
|---|---|
| cosign | CLI for signing/verifying container images, blobs, SBOMs, attestations |
| Fulcio | Short-lived code signing CA; issues certs from OIDC identity |
| Rekor | Immutable transparency log of signing events |
| policy-controller / cosign verify | Enforcement at admission or deploy time |
| in-toto attestations | Statement format (subject, predicateType, predicate) |
| SLSA provenance | A predicateType inside in-toto that describes build facts |
Keyless signing flow#
dev/CI → OIDC (GitHub, Google, etc.) → Fulcio (issues 10-min cert bound to identity)
→ cosign signs artifact with ephemeral key
→ signature + cert logged in Rekor transparency log
→ verifiers check cert identity + Rekor inclusion proof
Benefit: no long-lived signing keys to manage or exfil.
Common cosign operations#
# Keyless sign a blob
cosign sign-blob --yes --bundle release.cosign.bundle release.tar.gz
# Verify blob using expected identity
cosign verify-blob \
--bundle release.cosign.bundle \
--certificate-identity-regexp="^https://github.com/myorg/app/.*$" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
release.tar.gz
# Attach an SBOM attestation
cosign attest --yes --predicate sbom.cdx.json --type cyclonedx \
ghcr.io/myorg/app@sha256:abcd...
# Download + inspect attestation
cosign download attestation ghcr.io/myorg/app@sha256:abcd... | \
jq -r '.payload' | base64 -d | jq .
# Verify SLSA provenance predicate
cosign verify-attestation --type slsaprovenance \
--certificate-identity-regexp="^https://github.com/slsa-framework/.*$" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
ghcr.io/myorg/app@sha256:abcd...
cosign verification of ecosystem provenance bundles#
Since cosign v2.4.0, you can verify attestations in the bundle format used by npm provenance, GitHub Artifact Attestations, and Homebrew provenance. Over 16,000 npm packages publish with provenance.
# Verify npm provenance (e.g., semver@7.6.3)
curl https://registry.npmjs.org/semver/-/semver-7.6.3.tgz > semver-7.6.3.tgz
curl https://registry.npmjs.org/-/npm/v1/attestations/semver@7.6.3 | \
jq '.attestations[]|select(.predicateType=="https://slsa.dev/provenance/v1").bundle' > npm-provenance.sigstore.json
cosign verify-blob-attestation --bundle npm-provenance.sigstore.json --new-bundle-format \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
--certificate-identity-regexp="^https://github.com/npm/node-semver/.github/workflows/release-integration.yml.?" \
semver-7.6.3.tgz
# Verify GitHub Artifact Attestation
gh attestation verify gh_2.54.0_linux_armv6.tar.gz --owner cli
The Ultralytics incident demonstrated the value: PyPI staff used Sigstore transparency logs and publish attestations to determine the first malicious releases came through the legitimate GitHub Actions workflow (build-phase injection), while the second round had no attestations at all (stolen API token). Once tooling enforces attestation presence at install time, tokenless publishes become detectable.
Key-based signing (fallback for air-gapped builds)#
cosign generate-key-pair # cosign.key / cosign.pub
cosign sign --key cosign.key ghcr.io/app:1.0.0
cosign verify --key cosign.pub ghcr.io/app:1.0.0
Store private keys in HSM or KMS (--key awskms://…, --key gcpkms://…); never embed in CI secrets if OIDC is available.
10. SBOMs (SPDX, CycloneDX)#
An SBOM is a machine-readable inventory of components, versions, licenses, and relationships in an artifact. It answers “is package X at version Y present?” in seconds when the next Log4Shell drops.
| Format | Body | Typical use |
|---|---|---|
| SPDX | Linux Foundation | License compliance, regulatory (NTIA, EO 14028) |
| CycloneDX | OWASP | AppSec, vuln scanning, VEX, SaaSBOM, ML-BOM |
CISA 2025 Minimum Elements update#
CISA updated its SBOM guidance (originally NTIA 2021) to reflect current maturity. Key additions: machine-processable formats are now required (not just recommended), SBOMs must integrate into broader cybersecurity practices (not standalone compliance artifacts), and the guidance emphasizes scalable implementation across both public and private sectors. Organizations should align SBOM programs to this updated baseline.
Minimum viable SBOM practice#
- Generate at build time (not post-hoc from a registry).
- Attach to the artifact (
cosign attest --type cyclonedx). - Sign the SBOM.
- Store versioned alongside the release.
- Feed to a vuln scanner on every pull request and on new CVE disclosure.
- Diff SBOMs between releases to surface new transitive deps.
Generation commands#
# Syft — filesystem
syft dir:. -o cyclonedx-json=sbom.cdx.json -o spdx-json=sbom.spdx.json
# Syft — container image
syft ghcr.io/myorg/app@sha256:abcd... -o cyclonedx-json=sbom.cdx.json
# cdxgen (language-native)
cdxgen -r -t javascript -o sbom.cdx.json
# Trivy
trivy image --format cyclonedx --output sbom.cdx.json ghcr.io/myorg/app@sha256:abcd...
# Docker buildx native
docker buildx build --sbom=true --provenance=mode=max -t myimage .
SBOM vulnerability scan#
# Grype against the SBOM (consistent regardless of runtime env)
grype sbom:sbom.cdx.json --fail-on high
# OSV-Scanner against the SBOM
osv-scanner --sbom sbom.cdx.json
# Dependency-Track upload
curl -X POST "https://dtrack.myorg.local/api/v1/bom" \
-H "X-API-Key: $DTRACK_TOKEN" \
-F "autoCreate=true" \
-F "projectName=myorg/app" \
-F "projectVersion=1.2.3" \
-F "bom=@sbom.cdx.json"
SBOM diff for transitive-dep injection detection#
# Naive: detect new direct or transitive deps between tags
diff <(jq -r '.components[] | "\(.name)@\(.version)"' old.cdx.json | sort) \
<(jq -r '.components[] | "\(.name)@\(.version)"' new.cdx.json | sort)
Run in PR CI — alert a human on any new dependency added by a minor/patch version bump of an existing dependency. This is the signal that would have caught the Axios → plain-crypto-js injection.
11. Dependency Scanning Tooling#
| Tool | Kind | Strength | Limitation |
|---|---|---|---|
| Dependabot | Auto-PR bumps + advisories | Free, GitHub-native, alerts | Only known CVEs; noisy PRs |
| Renovate | Auto-PR bumps | Highly configurable grouping, schedules, merge confidence | Config complexity |
| Snyk | SCA + IaC + container | Good proprietary DB + license checks | Commercial |
| OSV-Scanner | Google OSV DB | Open DB, fast, works on SBOM, lockfiles, directories | CVE-only (but catches GHSA, PYSEC, etc.) |
| Trivy | Image + SBOM + IaC + secrets | One binary does a lot | Noisy defaults |
| Grype | SBOM-driven scanner | Works with Syft output, SPDX and CycloneDX | CVE-only |
| Dependency-Track | Continuous SBOM mgmt | Aggregates SBOMs, policy, VEX | Self-hosted |
| Socket.dev | Behavioral (not CVE) | Detects install scripts, obfuscation, network access | Commercial for private |
| Phylum | Behavioral + reputation | Pre-install blocking | Commercial |
| Sonatype Repository Firewall / Nexus IQ | Registry proxy w/ quarantine | Blocks known-malicious before cache | Commercial |
OSV-Scanner CI usage#
- name: OSV scan
uses: google/osv-scanner-action/osv-scanner-action@v1.9.0
with:
scan-args: |-
--lockfile=package-lock.json
--lockfile=poetry.lock
--recursive
--fail-on-vuln
Dependabot minimal config#
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule: { interval: "daily" }
open-pull-requests-limit: 10
groups:
production:
dependency-type: "production"
development:
dependency-type: "development"
- package-ecosystem: "github-actions"
directory: "/"
schedule: { interval: "weekly" }
- package-ecosystem: "docker"
directory: "/"
schedule: { interval: "weekly" }
Renovate minimal config#
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", ":pinAllExceptPeerDependencies"],
"minimumReleaseAge": "7 days",
"pinDigests": true,
"packageRules": [
{"matchManagers": ["github-actions"], "pinDigests": true},
{"matchUpdateTypes": ["patch"], "automerge": true, "automergeType": "pr"}
],
"vulnerabilityAlerts": {"enabled": true, "labels": ["security"]}
}
minimumReleaseAge: "7 days" is the single most effective Renovate setting: it refuses to auto-bump into a version published less than 7 days ago, which is the window most malicious releases live before takedown.
12. Developer Host Hardening#
Developer workstations and personal laptops are now prime targets (Shai-Hulud, OpenVSX compromises, Glassworm). Two host-level defenses block the majority of opportunistic install-time attacks:
- Release-age gates — refuse to install any package version published <7 days ago.
- Disable install lifecycle scripts —
postinstall,preinstall,prepare. This is the #1 execution vector for npm malware.
Per-package-manager configs (from 2026 practitioner gist)#
uv (Python) — ~/.config/uv/uv.toml
exclude-newer = "7 days"
pip (Python) — ~/.config/pip/pip.conf + shell alias
[global]
index-url = https://artifactory.myorg.local/artifactory/api/pypi/pypi/simple/
# macOS / BSD date
alias pip='pip --uploaded-prior-to $(date -u -v-7d +%Y-%m-%dT%H:%M:%SZ)'
# Linux / GNU date
alias pip='pip --uploaded-prior-to $(date -u -d "7 days ago" +%Y-%m-%dT%H:%M:%SZ)'
npm — ~/.npmrc
min-release-age=7
ignore-scripts=true
pnpm — pnpm-workspace.yaml
minimumReleaseAge: 10080 # minutes = 7 days
minimumReleaseAgeExclude:
- "@myorg/*"
- "esbuild"
pnpm config set ignore-scripts true --global
yarn — ~/.yarnrc.yml
npmMinimalAgeGate: "7d"
enableScripts: false
npmPreapprovedPackages:
- "@myorg/*"
bun — ~/.bunfig.toml
[install]
minimumReleaseAge = 10080
Bun disables lifecycle scripts by default.
Additional host hygiene#
| Control | Note |
|---|---|
| Full-disk encryption + MDM | Table stakes |
| Hardware-backed MFA (YubiKey, TouchID WebAuthn) | Resists npm/PyPI phishing |
| Separate publish identity from daily-driver | Scoped npm tokens per project |
| EDR with script execution telemetry | Catches post-install spawning shells |
| Allowlist outbound from dev VM | Block C2 traffic |
| Rotate SSH keys, move to signed-commit keys (GPG / SSH) | Prevents force-push forgery |
| Review VS Code / JetBrains extensions regularly | Glassworm / OpenVSX TeamPCP vector |
| Don’t store long-lived cloud creds on disk | Use SSO + short-lived tokens |
13. Admission Control & Runtime Verification#
CI signing is only as useful as the deploy-time verification that enforces it. Without admission control, a compromised runner or registry can push unsigned images straight into production.
Kyverno policy — require cosign-verified images#
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce
background: false
webhookTimeoutSeconds: 30
rules:
- name: check-image-signature
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*"
attestors:
- entries:
- keyless:
subject: "https://github.com/myorg/*/.github/workflows/release.yml@refs/tags/v*"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
Sigstore policy-controller — verify SLSA provenance predicate#
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-slsa-provenance
spec:
images:
- glob: "ghcr.io/myorg/**"
authorities:
- keyless:
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: "https://github.com/slsa-framework/slsa-github-generator/.*"
ctlog:
url: https://rekor.sigstore.dev
attestations:
- name: must-have-slsa
predicateType: https://slsa.dev/provenance/v1
policy:
type: cue
data: |
predicate: {
buildDefinition: {
externalParameters: {
repository: =~"^https://github.com/myorg/"
}
}
}
OPA Gatekeeper — deny unpinned image tags#
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Pod"
image := input.request.object.spec.containers[_].image
not contains(image, "@sha256:")
msg := sprintf("image %q must be pinned by digest", [image])
}
Runtime#
| Layer | Control |
|---|---|
| Node | eBPF-based runtime detection (Falco, Tetragon) for exec of shells from package dirs |
| Network | Egress NetworkPolicy; deny-by-default cluster egress; CoreDNS deny lists |
| Secrets | Vault/ExternalSecrets with short-lived leases; no baked-in secrets |
| IAM | Workload identity (no static cloud keys); IMDSv2 only |
| Image | Scan on pull; expired image quarantine |
14. Case Studies — Defensive Lessons#
Each case below summarizes what happened, how it was detected, and what defenders learned. No reproduction steps.
14.1 SolarWinds SUNBURST (2020)#
| Aspect | Detail |
|---|---|
| Vector | Build-system compromise; backdoor injected into Orion build |
| Scale | ~18,000 organizations, including US federal agencies |
| Detection | FireEye discovered it in its own environment via anomalous 2FA enrollment |
| Dwell time | Months — code was signed with SolarWinds’ legitimate cert |
| Defensive lesson | Code signing alone is insufficient; the build platform itself must be trusted and attested (SLSA L3). Provenance would have shown “built on dev workstation, not expected builder.” |
14.2 Codecov Bash Uploader (2021)#
| Aspect | Detail |
|---|---|
| Vector | Attackers modified a Bash uploader script distributed via Codecov’s Docker image creation |
| Payload | Exfiltrated CI environment variables (secrets, tokens) |
| Detection | Customer noticed checksum mismatch on the script |
| Defensive lesson | Checksum-verify any script piped from the internet. Never use production creds in CI. Use short-lived OIDC-federated creds. |
14.3 Log4Shell / CVE-2021-44228 (2021)#
| Aspect | Detail |
|---|---|
| Vector | JNDI lookup RCE in a transitive dependency most teams didn’t know they used |
| Detection | Public disclosure after Minecraft PoC |
| Defensive lesson | You cannot patch what you can’t see. SBOMs answer “am I using Log4j anywhere?” in minutes. |
14.4 Event-stream (2018)#
| Aspect | Detail |
|---|---|
| Vector | Original maintainer handed off publish rights to an unknown contributor who added a malicious transitive dep |
| Payload | Crypto wallet stealer targeted at one specific downstream user |
| Detection | Developer noticed deprecated API warning, investigated |
| Defensive lesson | Maintainer handoffs are a red flag. Registry-level notifications for ownership change. Lockfile diffs would have caught the new dep. |
14.5 Chalk / Debug / color-* (September 2025)#
| Aspect | Detail |
|---|---|
| Vector | Maintainer “qix” phished via fake npmjs[.]help domain impersonating npm support (Sept 5); within 16 minutes of account access, attacker published trojanized versions of 18+ packages: chalk, debug, ansi-styles, strip-ansi, supports-color, color-convert, color-string, color-name, ansi-regex, wrap-ansi, slice-ansi, color, simple-swizzle, supports-hyperlinks, has-ansi, chalk-template, backslash, is-arrayish; DuckDB ecosystem packages also hit |
| Reach | 2.6 billion combined weekly downloads; Wiz found malicious code reached 10% of cloud environments within the 2-hour live window |
| Payload | Browser-side interceptor hooking fetch(), XMLHttpRequest, window.ethereum, Solana signing APIs; rewrote crypto wallet addresses with look-alike substitutions before signing; targeted ETH, BTC, SOL, TRX, LTC, BCH |
| Financial impact | ~$500 direct theft, but massive industry “denial-of-service” in remediation hours |
| Detection | Aikido Security first; subsequent analysis by Wiz, Sygnia, Palo Alto Unit 42, Semgrep, Sonatype, ArmorCode, Vercel, JFrog |
| Defensive lesson | A single phished maintainer of a utility package cascades into the entire JS ecosystem. 2FA with phishing-resistant factors (WebAuthn) is now mandatory for any popular-package maintainer. Release-age gates would have stopped most installs. The 16-minute attack window demonstrates that human-speed response is insufficient — automated controls must pre-empt. |
14.6 Nx / s1ngularity (August 2025)#
| Aspect | Detail |
|---|---|
| Vector | Flawed GitHub Actions workflow using pull_request_target with unsanitized PR titles enabled code injection; malicious telemetry.js postinstall in Nx packages 20.9.0–21.8.0 and @nx/devkit, @nx/js, @nx/node, @nx/workspace, @nx/eslint, @nx/enterprise-cloud, @nx/key |
| Payload | Systematically harvested GitHub tokens, npm keys, SSH keys, crypto wallets, .env files, AI CLI tool credentials (Claude, Gemini, Q); used AI tools with --dangerously-skip-permissions / --yolo / --trust-all-tools flags to enumerate more secrets; exfiltrated via double-base64 to attacker-created s1ngularity-repository GitHub repos; appended sudo shutdown -h 0 to shell startup files for sabotage |
| Scale | 2,349 credentials harvested from 1,079 compromised systems; 1,100+ credentials remained valid; Phase 2 (Aug 28): attackers used stolen GitHub tokens to make 10,767 private repos public, exposing 82,901 additional secrets (11,168 valid); 400+ users/orgs impacted, 5,500+ repos exposed |
| Detection | StepSecurity, Wiz, GitGuardian, Aikido; root cause identified by Adnan Khan |
| Defensive lesson | Build-tool compromise executes inside the build context with full access to secrets. Egress allowlists on runners would have blocked exfil. Never use pull_request_target without input sanitization; use zizmor to lint workflows. AI CLI tools are now an attack surface — their elevated permissions make them high-value targets. GitGuardian released the open-source S1ngularity Scanner for post-compromise assessment. |
14.7 Shai-Hulud npm worm (September 2025) — “First successful self-propagating npm worm”#
| Aspect | Detail |
|---|---|
| Vector | Credential-harvesting phishing spoofing npm → maintainer account access → postinstall scripts harvest credentials → stolen npm tokens used to auto-publish malicious versions of victim’s other packages (worm propagation via NpmModule.updatePackage function) |
| Propagation | Self-spreading; 500+ package versions compromised including ngx-bootstrap (300k/wk), ng2-file-upload (100k/wk), @ctrl/tinycolor (2.2M/wk); ~3.6MB Webpack-bundled bundle.js payload |
| Payload mechanics | Installed TruffleHog for broad secret scanning (800+ secret types); targeted npm, GitHub, AWS, GCP, Azure tokens; exfiltrated to attacker-created Shai-Hulud GitHub repos with double-base64 encoding; also injected .github/workflows/shai-hulud-workflow.yml using ${{ toJSON(secrets) }} for persistent exfil; Linux/macOS only (skipped Windows) |
| Detection | ReversingLabs, StepSecurity, Trend Micro, CISA advisory (Sept 23); npm team acted |
| Detection signals | Anomalous publish events, new npm versions without corresponding release commits, outbound exfil to public GitHub repos, repos named “Shai-Hulud” with description “Shai-HuludRepository”, branches named shai-hulud |
| LLM involvement | Unit 42 assesses with moderate confidence that an LLM was used to generate the malicious bash script (based on comments and emojis in code) |
| Defensive lesson | Developer machines are now the target. ignore-scripts=true would have blocked execution. Never store long-lived npm tokens; use OIDC Trusted Publishers. Similarities with Nx/s1ngularity suggest related or copycat actors. |
14.8 Shai-Hulud 2.0 (November 2025)#
| Aspect | Detail |
|---|---|
| Scale | ~25,000 repositories hijacked, ~500 GitHub users, 796 unique npm packages (20M+ weekly downloads) — including Zapier, ENS Domains, PostHog, Postman |
| Key evolution | Switched from postinstall to preinstall — executes before any security checks; eliminates need for human interaction; bypasses static scanning tools |
| Payload | setup_bun.js + 10MB obfuscated bun_environment.js; installed Bun runtime (likely to evade Node.js monitoring); used TruffleHog to scan for 800+ secret types; harvested AWS Secrets Manager entries (SDK pagination), GCP secrets (@google-cloud/secret-manager), Azure creds; exfil to repos described “Sha1-Hulud: The Second Coming”; fake commits under “Linus Torvalds” name |
| Dead man’s switch | If the worm cannot replicate or exfiltrate, it attempts to delete the user’s home directory — destructive fallback discovered by GitLab Vulnerability Research team |
| GH Actions abuse | Registered self-hosted runners as “SHA1HULUD”; backdoored “formatter” workflows dumped toJSON(secrets); malicious discussion.yaml for remote command execution |
| Self-replication | Reads its own content to propagate — no C2 server needed for replication; automatically backdoors up to 100 packages per victim |
| Detection | Aikido, ReversingLabs, Wiz, Datadog Security Labs, Microsoft Defender, Trend Micro, GitLab via GitHub Archive telemetry; Microsoft Defender issued dedicated alert “Sha1-Hulud Campaign Detected” |
| Defensive lesson | Treat self-hosted runners as production. Audit runner registrations. Restrict permissions: blocks. Avoid toJSON(secrets) anti-patterns. Commit signature verification defeats fake persona commits. The destructive fallback means incident response must account for data loss, not just exfiltration. |
14.9 Axios / plain-crypto-js (March 2026) — DPRK-attributed#
| Aspect | Detail |
|---|---|
| Vector | Maintainer account hijack (email changed to ifstap@proton.me) → axios@1.14.1 and axios@0.30.4 published with a brand-new hidden transitive dep plain-crypto-js@4.2.1 instead of inline malware |
| Attribution | Google Threat Intelligence Group (GTIG) attributes to UNC1069, a financially motivated North Korea-nexus actor active since 2018, based on the WAVESHAPER.V2 backdoor and infrastructure overlaps |
| Reach | axios = 100M+ weekly downloads per affected version (1.14.1 + 0.30.4 combined ~183M/wk) |
| Payload | SILKBELL dropper (setup.js): custom XOR + Base64 string obfuscation; dynamically loads fs, os, execSync; OS-specific execution: Windows (copies powershell.exe to %PROGRAMDATA%\wt.exe, downloads payload via curl POST), macOS (AppleScript), Linux (Python); deploys WAVESHAPER.V2 backdoor (RAT) |
| Anti-forensics | Deleted setup.js after execution; rewrote package.json to remove postinstall hook and restore benign appearance |
| Detection | Sonatype flagged within minutes (01:04 UTC March 31); GTIG, StepSecurity reporting |
| Live window | March 31, 2026 00:21–03:20 UTC (~3 hours) |
| Defensive lesson | Attackers now inject a hidden transitive instead of modifying core code — SBOM diff is the fastest reliable signal. Repository firewalls that quarantine new packages would have blocked install. Nation-state actors (DPRK) are now directly conducting npm supply chain attacks for financial gain. |
14.10 TeamPCP campaign (March 2026, ongoing)#
| Sub-incident | Summary |
|---|---|
| Trivy v0.69.4 | Stolen Aqua Security creds used to publish malicious release + force-push 76 GitHub Action tags. Propagated to GHCR, Docker Hub, ECR Public, deb/rpm, get.trivy.dev. Actions dumped CI runner memory and exfiltrated via lookalike domain. |
| CanisterWorm npm | Self-propagating worm across 40+ packages; stole tokens, bumped patch versions, republished; Kubernetes-targeting payload |
| Checkmarx KICS GitHub Action + OpenVSX | Same credential-theft pattern; fallback exfil by creating public repo with victim GITHUB_TOKEN |
| LiteLLM (PyPI) 1.82.7/1.82.8 | Collected env vars, SSH keys, cloud creds, Kubernetes configs, Docker configs, shell history, DB creds, wallet files, CI secrets; encrypted locally and exfiltrated |
| Telnyx (PyPI) 4.87.1/4.87.2 | Same exfil payload |
Unifying defensive lesson: credential theft → automated republishing is the modern blueprint. Required controls: (1) no long-lived publish tokens — OIDC Trusted Publishers only; (2) pinned, digest-bound Action SHAs; (3) signed tag enforcement; (4) egress allowlist on runners; (5) anomaly detection on publish velocity.
14.11 Glassworm (October 2025)#
| Aspect | Detail |
|---|---|
| Vector | Self-spreading VS Code extension on OpenVSX |
| Defensive lesson | IDE extensions are code executing with user privileges. Treat extension installs like package installs — pin, review, scan. |
14.12 PhantomRaven (October 2025)#
| Aspect | Detail |
|---|---|
| Vector | 126 npm packages with stealer payloads |
| Defensive lesson | Behavioral scanners (Socket, Phylum) that look at what a package does catch these before CVE databases do. |
14.13 DPRK 1,700-package flood (2025–2026)#
| Aspect | Detail |
|---|---|
| Vector | North Korean cluster (Contagious Interview / Jackpot Panda overlaps) published across npm, PyPI, Go, Rust |
| Defensive lesson | Nation-state volume makes manual review infeasible. Automation + behavioral analysis + release-age gates are required. |
14.14 XZ Utils backdoor / CVE-2024-3094 (March 2024)#
| Aspect | Detail |
|---|---|
| Vector | Multi-year social engineering: attacker “Jia Tan” built credibility as an OSS contributor over 2+ years, gained co-maintainer status on the xz-utils project, then inserted an obfuscated backdoor into versions 5.6.0 and 5.6.1 via distribution tarballs (not visible in Git source) |
| Payload | Malicious shared object loaded by sshd via liblzma; hijacked RSA_public_decrypt to allow holders of a specific private key to execute arbitrary code pre-authentication; only activated on x86-64 DEB/RPM builds using gcc + GNU linker |
| Affected | Fedora 40/41/Rawhide, Debian testing/unstable/experimental, Alpine Edge, Kali, openSUSE Tumbleweed, Arch Linux; NOT in Ubuntu, RHEL, Amazon Linux stable branches |
| Detection | Andres Freund (Microsoft) noticed 500ms SSH latency anomaly while benchmarking; disclosed March 29, 2024 |
| Defensive lesson | The most sophisticated supply chain attack to date — code review alone is insufficient when the attacker is the maintainer. Binary/opaque files in commits are a red flag. Reproducible builds would have detected the tarball/source mismatch. Staged release pipelines (experimental → stable) limited blast radius. SBOM and provenance attestation would have surfaced the discrepancy between source and binary. |
14.15 tj-actions/changed-files + reviewdog (CVE-2025-30066 / CVE-2025-30154, March 2025)#
| Aspect | Detail |
|---|---|
| Vector | Multi-stage cascading compromise: (1) attacker exploited automated contributor invitation in reviewdog org → joined @reviewdog/actions-maintainer team → pushed malicious commit to reviewdog/action-setup, re-pointed v1 tag; (2) stolen credentials from reviewdog used to compromise tj-actions/changed-files via its dependency on tj-actions/eslint-changed-files → reviewdog/action-setup; (3) all version tags v1–v44.5.1 re-pointed to a single malicious commit. Origin traced to SpotBugs compromise in November 2024 |
| Initial target | Coinbase (agentkit project) — attacker first targeted their public CI/CD flow but failed to access Coinbase secrets; then pivoted to broader attack |
| Payload | Base64-encoded script dumped CI runner memory containing workflow secrets to workflow logs (no external C2 needed — GitHub’s own logs served as exfiltration channel) |
| Scale | 23,000+ repositories used tj-actions/changed-files; Wiz identified dozens of repos with exposed AWS keys, GitHub PATs, npm tokens, private RSA keys |
| Detection | StepSecurity (March 14), Adnan Khan (linked to reviewdog, March 16), Unit 42 traced to SpotBugs origin |
| CISA advisory | Joint CVE-2025-30066 (tj-actions) and CVE-2025-30154 (reviewdog) advisory, March 18 |
| Defensive lesson | Pin all GitHub Actions by commit SHA, not tag. Audit transitive composite action dependencies. Restrict contributor team auto-invitation. Log-based exfiltration bypasses network monitoring — restrict public workflow log visibility. ghs_ tokens are short-lived (<24h) so lower risk; custom secrets are highest priority for rotation. |
14.16 Ultralytics PyPI (December 2024)#
| Aspect | Detail |
|---|---|
| Vector | GitHub Actions script injection via pull_request_target trigger with unsanitized PR branch names (known vuln GHSA-7x29-qqmq-v6qc reported by Adnan Khan); attacker crafted malicious PR titles from forks to achieve arbitrary code execution in the build environment |
| Payload | XMRig coinminer downloaded via platform-specific dropper injected into downloads.py and model.py; affected versions 8.3.41, 8.3.42, 8.3.45, 8.3.46 |
| Compounding failure | Version 8.3.42, intended as the fix, shipped with the same malware (maintainers didn’t properly locate the compromise); versions 8.3.45/8.3.46 published via stolen PyPI API token from the initial build-env compromise |
| Scale | ultralytics: 60M+ PyPI downloads, 30,000+ GitHub stars |
| Attestation value | PyPI staff used Sigstore transparency logs and publish attestations to determine first malicious releases came through legitimate GH Actions (build-phase injection) while second round had no attestations (stolen API token) — provenance was the forensic key |
| Defensive lesson | Audit GH Actions for pull_request_target + unsanitized input patterns using zizmor. Revoke unused API tokens when using Trusted Publishers. Build environment caches are an attack surface. When remediating, verify the fix is actually clean before publishing. |
14.17 dYdX npm + PyPI (2026)#
| Aspect | Detail |
|---|---|
| Vector | Compromised dYdX packages deliver wallet stealers + RAT |
| Defensive lesson | Fintech / Web3 packages are high-value targets. Multi-registry campaigns mean defenders must unify SCA across language ecosystems. |
14.18 React2Shell CVE-2025-55182 (Nov–Dec 2025)#
Not a supply-chain injection but relevant as a dependency-triggered RCE: React Server Components 19.0.0–19.2.0 had a pre-auth RCE. Discovered by Lachlan Davidson; exploited within days by Earth Lamia, Jackpot Panda, Contagious Interview. Over 77,000 vulnerable IPs scanned by Shadowserver. CISA KEV December 17.
Defensive lesson: SBOM-driven scanning plus an emergency response playbook are load-bearing. The fastest organizations to patch had automated SBOM → KEV lookups.
14.19 Oracle EBS CVE-2025-61882 (Oct 2025) — Clop#
Oracle E-Business Suite zero-day (CVSS 9.8) exploited by Clop for mass data theft (Barts Health, Canon, GlobalLogic, LKQ, Logitech, Mazda).
Lesson: Commercial dependencies need the same patching SLAs as OSS.
14.20 ToolShell CVE-2025-53770/-53771 (July 2025)#
Chained SharePoint on-prem exploits by Linen Typhoon, Violet Typhoon, Storm-2603; ~396 systems compromised; web shells deployed.
Lesson: Internet-facing internally-run commercial software is a supply chain node — treat it with the same monitoring as your own binaries.
14.21 CitrixBleed 2 CVE-2025-5777 (June 2025)#
Out-of-bounds read bypassing MFA / hijacking session tokens in NetScaler ADC/Gateway.
Lesson: Network-appliance firmware is supply chain. SBOM your appliances.
14.22 Bybit $1.5B (Feb 2025)#
Wallet-software supply chain compromise executing only when the target wallet was active.
Lesson: Conditional payloads evade test benches. Runtime behavioral monitoring complements static analysis.
14.23 Composite: SolarWinds, Codecov, Log4Shell, XZ Utils#
Common thread: a small number of high-trust components → catastrophic downstream. The only durable defense is verifiable provenance + SBOM transparency + deploy-time enforcement.
15. Detection Signals & IOCs#
Build / CI signals#
| Signal | Possible attack |
|---|---|
New postinstall / preinstall in a minor version bump | npm dropper |
| New transitive dep introduced in a patch release | Axios-style hidden injection |
Runner egress to unrecognized domain during npm install | C2 beacon |
toJSON(secrets) in workflow diff | Shai-Hulud 2.0 pattern |
Self-hosted runner registered with anomalous name (e.g. SHA1HULUD) | Worm propagation |
| Force-push to release tag | Trivy v0.69.4 pattern |
| Publish event without corresponding release commit | Worm re-publish |
Unexpected npm publish / twine upload in CI logs | Token abuse |
| CI workflow logs containing double-base64 encoded strings | tj-actions exfil pattern |
preinstall script in a patch/minor bump referencing setup_bun.js or bun_environment.js | Shai-Hulud 2.0 |
| GitHub repo created with description containing “Shai-Hulud” or “Sha1-Hulud” | Worm exfil target |
Branch named shai-hulud with workflow file in .github/workflows/ | Persistence mechanism |
pull_request_target workflow with unsanitized ${{ github.event.pull_request.title }} | Script injection vector (Ultralytics, Nx root cause) |
| PyPI publish without corresponding Sigstore attestation when project uses Trusted Publishers | Stolen API token (Ultralytics second wave) |
| GitHub contributor auto-invitation to teams with write access | reviewdog compromise vector |
Package-level signals#
| Signal | Check |
|---|---|
| Obfuscated strings, base64/XOR decoding in install scripts | Static scan |
OS fingerprinting (process.platform, sys.platform) in install scripts | Behavioral |
Writes to /tmp + chmod +x + exec in install scripts | Behavioral |
| Network fetch during install | Behavioral |
package.json rewritten at runtime | Anti-forensics — Axios pattern |
| Single-maintainer, <30 day age, no source repo | Metadata heuristics |
Bun runtime installation (curl -fsSL bun.sh/install) in install scripts | Shai-Hulud 2.0 evasion of Node.js monitoring |
| TruffleHog binary downloaded during install | Credential harvesting (Shai-Hulud 1.0/2.0) |
rm -rf ~ or shred commands in install scripts | Dead man’s switch / destructive fallback |
| Custom XOR + Base64 string deobfuscation patterns | SILKBELL dropper (Axios/DPRK) |
Workstation / dev signals#
| Signal | Source |
|---|---|
Shell spawn from node, python, pip, npm processes | EDR |
Read of ~/.aws/credentials, ~/.ssh/id_*, ~/.kube/config, ~/.docker/config.json by package install | EDR + DLP |
New files under /tmp, %TEMP%, ~/Library correlated with package install | EDR |
| Outbound to GitHub Gists / raw.githubusercontent.com from install | Network |
AI CLI tools (Claude, Gemini, Q) invoked with --dangerously-skip-permissions, --yolo, --trust-all-tools | Nx/s1ngularity AI exploitation |
New GitHub repos named s1ngularity-repository* or Shai-Hulud in victim accounts | Exfiltration repos |
Shell startup files (~/.bashrc, ~/.zshrc) modified with shutdown commands | Nx sabotage payload |
Copied system binaries (e.g., powershell.exe → %PROGRAMDATA%\wt.exe) | SILKBELL dropper evasion |
Hunting queries (SIEM pseudocode)#
# Anomalous npm publish from CI
process.name = "npm" AND argv contains "publish"
AND runner.self_hosted = true
AND runner.name NOT IN (allowlist)
# Credential file access by package install
process.parent IN ("npm","node","pip","python","uv")
AND file.path MATCHES ("~/.aws/*","~/.ssh/*","~/.kube/*","~/.docker/*")
# New transitive dep in patch version
repo.event = "pull_request"
AND file.path IN ("package-lock.json","poetry.lock","go.sum")
AND diff.adds CONTAINS "new package" AND semver.bump = "patch"
16. Defender Checklist#
Strategy#
- Inventory every supply chain node: registries, CI systems, artifact repos, IDE extensions, base images, vendor SaaS
- Assign an owner per node with a runbook and rotation plan
- Establish patch SLAs per severity; measure MTTR to KEV
- Build an incident response playbook keyed to supply-chain scenarios (maintainer ATO, worm, hidden transitive, build poisoning)
- Table-top a Chalk/Debug-scale and an Axios-scale incident annually (include destructive Shai-Hulud 2.0 scenario with data loss)
- Align SBOM program to CISA 2025 Minimum Elements guidance
Source & Code#
- Branch protection on default branch
- Required review (>=1), with CODEOWNERS for sensitive paths
- Signed commits + signed tags enforced
- Disallow force-push to protected branches/tags
- Secret scanning enabled on all repos (GitGuardian / GitHub push protection)
- No long-lived secrets in CI — OIDC federation to cloud
Dependencies#
- Lockfiles committed for all language ecosystems
-
--require-hashesor equivalent for reproducibility where supported - Dependabot or Renovate enabled, with
minimumReleaseAge >= 7 days - OSV-Scanner or Trivy on every PR + on a nightly schedule
- Dependency-Track (or equivalent) SBOM aggregation and VEX
- Behavioral scanner (Socket, Phylum) wired into PR gate
- Private-registry virtual proxy (Nexus/Artifactory) quarantining new packages
Build / CI#
- Ephemeral runners only
- GitHub Actions pinned by commit SHA, not tag
- Minimal
permissions:per job; default deny -
ignore-scripts=truewhere possible in CI installs - Egress allowlist on runners
- No
toJSON(secrets)anywhere - No
pull_request_targetwith unsanitized user inputs; lint withzizmor - Audit composite action transitive dependencies
- Review GitHub org team auto-invitation policies (reviewdog lesson)
- Enable tag protection rules to prevent tag re-pointing
- Tamper-evident audit log export
- Separation of duties: code author != release signer
Artifacts / Packaging#
- SBOM generated at build (CycloneDX + SPDX)
- SBOM attached to artifact as attestation (
cosign attest) - Image signed keyless via cosign + Sigstore
- SLSA L3 provenance via
slsa-github-generator - Reproducible builds where feasible
- Artifacts immutable once published
Registry / Distribution#
- 2FA required on all maintainer accounts (hardware, not SMS)
- Trusted Publishers (OIDC) replacing long-lived tokens
- Publish-alert notifications to shared channel
- Namespace/scope claim for internal packages on public registries
- Repository firewall quarantining newly published or low-reputation deps
Deploy / Runtime#
- Admission controller (Kyverno / policy-controller) verifies cosign signature + SLSA predicate
- Images referenced by digest only
- Deny-by-default egress NetworkPolicy
- IMDSv2 only; workload identity; no baked creds
- Runtime detection (Falco/Tetragon) for shells-from-package dirs
- Canary / staged rollout — never deploy all systems simultaneously (OWASP A03 guidance)
Developer Host#
- Release-age gate configured in npm/pnpm/yarn/bun/pip/uv
-
ignore-scripts/enableScripts: falseglobally - Hardware MFA on registry accounts
- EDR with script-execution telemetry
- IDE extension allowlist; review extension updates
- Verify AI-suggested packages before install (slopsquatting)
- Review AI CLI tool permissions and disable dangerous auto-approve flags
- Revoke unused PyPI API tokens when using Trusted Publishers
Response#
- On suspected compromise, assume full credential exposure — rotate all secrets, cloud keys, SSH keys, tokens touched by the affected environment
- Rebuild affected workstations and CI runners from a clean image
- Pull audit logs for the suspected window
- Block the indicator at repository firewall and network egress
- Publish an internal advisory with affected package/version/IOC list
- Retrospect to close the gap that permitted the compromise
17. Reference Configurations#
17.1 Full hardened .npmrc#
registry=https://nexus.myorg.local/repository/npm-group/
@myorg:registry=https://nexus.myorg.local/repository/npm-private/
always-auth=true
audit=true
fund=false
save-exact=true
min-release-age=7
ignore-scripts=true
engine-strict=true
package-lock=true
17.2 Full hardened pip.conf#
[global]
index-url = https://artifactory.myorg.local/artifactory/api/pypi/pypi/simple/
require-virtualenv = true
disable-pip-version-check = true
no-cache-dir = false
require-hashes = true
17.3 GitHub Actions repository default permissions#
permissions:
actions: read
contents: read
deployments: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: none
repository-projects: none
security-events: none
statuses: none
Escalate per-workflow only where needed.
17.4 Trivy image + config scan#
trivy image --severity HIGH,CRITICAL --exit-code 1 \
--ignore-unfixed \
--format sarif --output trivy.sarif \
ghcr.io/myorg/app@sha256:abcd...
trivy config --severity HIGH,CRITICAL --exit-code 1 .
trivy fs --scanners secret,vuln,misconfig .
17.5 OSV-Scanner offline#
osv-scanner --experimental-offline --experimental-download-offline-databases ./
17.6 Syft + Grype end-to-end#
syft dir:. -o cyclonedx-json=sbom.cdx.json
cosign sign-blob --yes --bundle sbom.cosign.bundle sbom.cdx.json
grype sbom:sbom.cdx.json --fail-on high -o sarif > grype.sarif
17.7 Policy-as-code example (Conftest / OPA on Dockerfile)#
package main
deny[msg] {
input[i].Cmd == "from"
val := input[i].Value[0]
not contains(val, "@sha256:")
msg := sprintf("FROM %s must use @sha256 digest", [val])
}
deny[msg] {
input[i].Cmd == "run"
contains(input[i].Value[_], "curl")
contains(input[i].Value[_], "|")
contains(input[i].Value[_], "sh")
msg := "curl | sh is banned; use pinned, verified installers"
}
17.8 Dependency-Track policy example#
| Policy | Condition | Action |
|---|---|---|
| New high-severity CVE | component.vuln.severity >= HIGH | Fail build |
| Unmaintained dep | component.last_modified > 24 months | Warn |
| Unapproved license | license NOT IN allowlist | Fail |
| Dep from untrusted registry | component.purl !~ allowed_registries | Fail |
| New publisher | component.publisher NEW | Manual review |
17.9 Release playbook summary#
1. Feature branch -> PR -> required review -> merge to main
2. Tag v* -> triggers release workflow (OIDC, read-only by default)
3. Workflow: build -> SBOM -> scan -> sign -> attest (SLSA + SBOM)
4. Publish artifact + signature bundle to registry
5. Admission controller verifies at deploy time
6. Canary rollout 5% -> 25% -> 100% with automated rollback
7. Post-deploy: runtime telemetry + SBOM archived alongside release
17.10 Emergency triage on a suspected compromised dependency#
1. Identify affected package + version(s) from advisory
2. Query SBOM store: "which services ship this?"
3. For each hit:
- Pin to last-known-good version and force-rebuild
- Invalidate all secrets accessible from the build and runtime context
- Rebuild and redeploy affected workloads from clean state
- Review CI runner logs + egress traffic during vulnerable window
4. Block malicious version at repository firewall
5. Add advisory to internal KEV-equivalent list
6. Notify downstream consumers if you publish packages
7. Post-incident: determine which SLSA level / SBOM coverage / admission rule would have prevented it; file follow-up action items
Appendix A — Frameworks & Standards Quick Map#
| Standard | Body | Scope |
|---|---|---|
| SLSA v1.1 | OpenSSF | Build integrity levels, provenance format |
| in-toto | CNCF | Attestation statement format |
| Sigstore / cosign | OpenSSF | Keyless signing, transparency log |
| SPDX 2.3 / 3.0 | Linux Foundation | SBOM (compliance-oriented) |
| CycloneDX 1.5/1.6 | OWASP | SBOM (security-oriented), VEX, ML-BOM |
| NIST SSDF (SP 800-218) | NIST | Secure software development framework |
| EO 14028 | US Executive Order | Federal software supply chain baseline |
| NIST SP 800-161 Rev.1 | NIST | C-SCRM for systems and organizations |
| ISO/IEC 5230 (OpenChain) | ISO | OSS compliance program |
| OWASP SAMM / ASVS V15 | OWASP | Secure coding & architecture verification |
| CIS Software Supply Chain Security Guide | CIS | Benchmark controls |
| SAFECode Software Integrity Controls | SAFECode | Integrity control practices |
Appendix B — Key CWEs#
| CWE | Description |
|---|---|
| CWE-477 | Use of Obsolete Function |
| CWE-1035 | Using Components with Known Vulnerabilities (2017 Top 10 A9) |
| CWE-1104 | Use of Unmaintained Third-Party Components |
| CWE-1329 | Reliance on Component That Is Not Updateable |
| CWE-1357 | Reliance on Insufficiently Trustworthy Component |
| CWE-1395 | Dependency on Vulnerable Third-Party Component |
Appendix C — Sources (54 articles)#
- 12 Months That Changed Supply Chain Security (Silobreaker)
- 16 Minutes to Impact: npm Supply Chain Abuse Deploys crypto-draining malware (Sygnia)
- 2025 Minimum Elements for a Software Bill of Materials (SBOM) (CISA)
- 2026 Supply Chain Security Report — Lessons from a Year of Devastating Attacks (Bastion)
- A03 Software Supply Chain Failures (OWASP)
- Achieving SLSA 3 Compliance with GitHub Actions and Sigstore for Go modules (GitHub Blog)
- Axios Compromise on npm Introduces Hidden Malicious Package (Sonatype)
- Axios npm Package Compromised in Supply Chain Attack (InfoQ)
- Breakdown: Widespread npm Supply Chain Attack Puts Billions of Weekly Downloads at Risk (Palo Alto)
- Compromised dYdX npm and PyPI Packages Deliver Wallet Stealers and RAT Malware (The Hacker News)
- Compromised ultralytics PyPI package delivers crypto coinminer (ReversingLabs)
- cosign Verification of npm Provenance, GitHub Artifact Attestations, and Homebrew Provenance (Sigstore Blog)
- CVE-2024-3094 XZ Backdoor: All you need to know (JFrog)
- Five Key Flaws Exploited in 2025’s Major Software Supply Chain Incidents (Infosecurity Magazine)
- GitHub Actions Supply Chain Attack: A Targeted Attack on Coinbase Expanded to the Widespread tj-actions/changed-files Incident (Unit 42)
- GitHub Action tj-actions/changed-files supply chain attack: everything you need to know (Wiz)
- GitLab discovers widespread npm supply chain attack (GitLab)
- Hackers Supply Chain Attack Moves From npm to PyPI as Trivy Breach Extends into LiteLLM (Semgrep)
- How to Prevent OWASP Software Supply Chain Failures (CrossClassify)
- LiteLLM PyPI Packages Compromised in Expanding TeamPCP Supply Chain Attacks (Help Net Security)
- Malicious PyPI and npm Packages Discovered Exploiting Dependencies in Supply Chain Attacks (The Hacker News)
- N. Korean Hackers Spread 1,700 Malicious Packages Across npm, PyPI, Go, Rust (The Hacker News)
- North Korea-Nexus Threat Actor Compromises Widely Used Axios NPM Package in Supply Chain Attack (Google Threat Intelligence / GTIG)
- NPM Supply Chain Attacks Explained — Dependency Confusion, Exploits, and Defense (jsmon)
- OWASP Top 10 2025 A03 — Software Supply Chain Failures (Authgear)
- Predictions for Open Source Security in 2025 — AI, State Actors, and Supply Chains (OpenSSF)
- Protecting Your Software Supply Chain — Typosquatting and Dependency Confusion (GitGuardian)
- PyPI, npm, and the New Frontline of Software Supply Chain Attacks (RapidFort)
- s1ngularity: supply chain attack leaks secrets on GitHub: everything you need to know (Wiz)
- Securing CI/CD Pipelines After the tj-actions and reviewdog Supply Chain Attacks (OpenSSF)
- Securing Software Supply Chains — Critical Infrastructure Priorities for 2026 (Leadership Connect)
- Shai-Hulud 2.0: Guidance for detecting, investigating, and defending against the supply chain attack (Microsoft Security Blog)
- Shai-Hulud 2.0 Supply Chain Attack: 25K+ Repos Exposing Secrets (Wiz)
- Shai-hulud npm attack: What you need to know (ReversingLabs)
- Shai-Hulud: Self-Replicating Worm Compromises 500+ NPM Packages (StepSecurity)
- “Shai-Hulud” Worm Compromises npm Ecosystem in Supply Chain Attack (Unit 42)
- SLSA Framework — The Definitive Guide for Securing Your Software Supply Chain (Practical DevSecOps)
- Software Supply Chain Attacks 2025–2026 — Axios, Shai-Hulud, Chalk, TeamPCP (Cyber Army)
- Software Supply Chain Risks — 2026 Software Supply Chain Report (Sonatype)
- Supply-chain attack analysis: Ultralytics (PyPI Blog)
- Supply-Chain Attack Defense — Developer Host Machine Hardening (gist)
- Supply-chain Levels for Software Artifacts (slsa.dev)
- Supply Chain Attack — How Attackers Weaponize Software Supply Chains (Netlas)
- Supply Chain Attacks Are Exploiting Our Assumptions (Trail of Bits)
- Supply Chain Attacks in Q4 2025 — From Isolated Incidents to Systemic Failure Modes (Sygnia)
- Supply Chain Compromise of Third-Party tj-actions/changed-files (CVE-2025-30066) and reviewdog/action-setup@v1 (CVE-2025-30154) (CISA)
- Supply Chain Security in CI — SBOMs, SLSA, and Sigstore (nathanberg.io)
- The 2026 Guide to Software Supply Chain Security (Cloudsmith)
- The Next Wave of Supply Chain Attacks — NPM, PyPI, Docker Hub Set the Stage for 2026 (LinuxSecurity)
- The Nx “s1ngularity” Attack: Inside the Credential Leak (GitGuardian)
- The Shai-Hulud 2.0 npm worm: analysis, and what you need to know (Datadog Security Labs)
- The XZ Utils backdoor (CVE-2024-3094): Everything you need to know, and more (Datadog Security Labs)
- Widespread npm Supply Chain Attack: Breaking Down Impact & Scope Across Debug, Chalk, and Beyond (Wiz)
- xz Backdoor CVE-2024-3094 (OpenSSF)
This document synthesizes publicly reported research and advisories for defensive reference. It contains no reproduction steps, no working malicious code, and no attacker tooling. All code snippets are defensive configurations.