Supply chain attacks weaponise trust. Instead of attacking your perimeter, the adversary compromises something upstream — an npm package you depend on (event-stream, ua-parser-js), a vendor's signed installer (SolarWinds, 3CX, Codecov), your CI/CD pipeline, or a typosquatted package similar to one you already use. The malicious payload arrives via your own normal build or update path, signed and trusted.
Detection requires both proactive controls (Software Composition Analysis, signed-package enforcement, dependency pinning, SBOM tracking) and reactive hunting (process telemetry from build agents and developer workstations, network egress from CI runners, vendor advisory feeds). When upstream advisory drops — Snyk, GitHub advisory, vendor CVE — you have hours to confirm exposure and contain.
◈ Table of Contents
Background
CONTEXT| Pattern | MITRE | Key Indicator | Severity |
|---|---|---|---|
| Malicious package (npm / PyPI / NuGet / RubyGems) | T1195.001 (Compromise Software Dependencies) | Newly-published version with install/postinstall script, network egress, or obfuscated code | CRITICAL |
| Trojanized vendor update (SolarWinds, 3CX, Kaseya pattern) | T1195.002 (Compromise Software Supply Chain) | Signed legitimate binary contains backdoor; vendor advisory after the fact; specific hashes | CRITICAL |
| Dependency confusion | T1195 | Public package with same name as internal package + higher version pulled by build | CRITICAL |
| Typosquat | T1195 | Package name 1-2 chars off from a popular library (e.g. reqeusts vs requests) | HIGH |
| Build system / CI runner compromise | T1195 + T1078 | Unsigned commits in pipeline source; new self-hosted runner from foreign IP; secrets read by runner | CRITICAL |
| MSP / vendor admin abuse | T1199 (Trusted Relationship) | Vendor admin account performing actions outside scope; new vendor IP using legit creds | CRITICAL |
| Source | Table / Tool | Key Signals |
|---|---|---|
| SBOM / SCA | Snyk, Dependabot, GitHub Advisory, OSV, GitLab Dependency Scanning | Vulnerable package + version + transitive path |
| Artifact repository | Azure Artifacts / JFrog Artifactory / Nexus | Pulled-version manifests, package hashes, internal vs external source |
| CI/CD logs | GitHub Actions / Azure DevOps / GitLab CI | Pipeline runs, packages installed, network egress from runner |
| Build host / runner EDR | DeviceProcessEvents, DeviceNetworkEvents | Post-install scripts, unexpected outbound, secret-file reads |
| Vendor advisory channels | RSS / email / Slack integration | CVE drops, IOC lists, hash blocklists |
Identification
INVESTIGATE- 1Search SBOM / lockfiles: Query Snyk / Dependabot / GitHub Advisory for the affected package + version range across all repos. Don't trust developer memory — query the source-of-truth.
- 2Search artifact repository: For each artifact pulled from the upstream registry, check if the exact bad version/hash was downloaded by your builds.
- 3Search container registries: Image layers can carry the bad package even if your manifest no longer pins it. Check published images.
- 4Search production runtime: Use endpoint inventory or container manifests to confirm if the bad version is actively running anywhere.
Malicious npm packages overwhelmingly run their payload in postinstall. Any cmd.exe/powershell.exe/curl/wget child of npm.cmd on a build agent is suspicious by default.
| True-Positive Signals | False-Positive Signals |
|---|---|
| Bad hash matched in your artifact cache; package version range published in advisory | You pulled an adjacent version not on the advisory's affected list |
| Build runner egress to attacker C2 immediately after the package install step | Build runner egress to your normal artifact mirror / proxy |
| Postinstall script with obfuscated base64 / unicode escape payload | Postinstall running a known native binary build (sharp, node-sass, etc.) |
| New self-hosted runner registered from foreign IP without change record | New runner registered from your VPC IP with approved scaling event |
Containment
STOP IT- 1Block bad version in artifact repo: Add the exact version + hash to the deny-list (JFrog Block Rule / Artifactory exclusion / npm dist-tag manipulation). New builds cannot pull it.
- 2Pin to known-good version: Update lockfiles across all affected repos; create coordinated PR with explanation.
- 3If vendor binary: quarantine endpoints: Defender for Endpoint → Indicators → Add SHA-256 hash → Block + Remediate. Auto-isolates anything matching.
- 4Rollback recent production deploys known to contain the bad version. Use canary/blue-green to minimise customer impact.
- 5Disable affected CI/CD runners until rebuilt from known-clean base image; if self-hosted, treat as compromised.
- 1Rotate ALL CI/CD secrets accessible to the bad package or runner: NPM_TOKEN, GitHub PAT, cloud OIDC keys, Docker Hub tokens, internal artifact tokens.
- 2Rotate cloud credentials that the affected runtime could reach (managed identity, IAM role tokens are usually short-lived but rotate the bound secrets too).
- 3Rotate signing keys if the bad package had access to code-signing material.
- 4Audit egress logs for the period the bad version was active — flag any unfamiliar destination.
Eradication
CLEAN UP- 1Rebuild all CI runners from fresh gold images. Self-hosted runners that ran the bad code: destroy and recreate, do not patch in place.
- 2Force a clean install on developer workstations that ran the bad package — wipe node_modules / venv / equivalent, reinstall from pinned lockfile.
- 3Update SCA tooling rules to fail builds on packages newer than a quarantine period (e.g. block packages < 7 days old unless explicitly allow-listed).
- 4Enforce package source pinning to prevent dependency confusion: scope-locked npm registry, PyPI index-url + extra-index-url discipline, NuGet packageSourceMapping.
- 5Sign and verify your own internal packages with Sigstore / similar; require signed packages in production builds.
- Bad version removed from all lockfiles, container images, and runtime
- CI runners rebuilt; all secrets accessible to affected runners rotated
- SCA gate updated to prevent re-introduction
- Customer notification drafted (if applicable) and disclosure timeline agreed
- Post-incident review captures source-pinning + signed-package policy gaps
Escalation
TIERS| Tier | Trigger | Action / Handoff Bundle |
|---|---|---|
| L1 → L2 | Upstream advisory; SBOM hit; affected versions in our repos | Advisory link, package + version, affected repos, deploy footprint |
| L2 → IR + Engineering | Bad version is in production OR build agent shows post-install C2 egress | Affected services, customer scope, runner forensics, secret-rotation list |
| IR → CISO + Customer Comms | Customer-impacting compromise; signed-by-us binary contains backdoor; PII / regulated data possibly exposed | Customer notification draft, regulator clock, public-disclosure decision |
| External | Active campaign across multiple of your vendors; nation-state implications | CISA / MS-ISAC / sector ISAC, law enforcement, external IR firm |
Authorized security research & penetration testing only. Unauthorized use is illegal. Authored by Rudra Verma | Senior Security Architect | CyberHawk Threat Intel.