The Compromised-Package Runbook: From Advisory to Contained Blast Radius
You will move from a compromise advisory to a full list of exposed repos and pipelines, a contain-or-wait call per hit, and the precise secrets to rotate, inside the first hour.
A package your code depends on just got flagged as compromised. This runbook is for the engineering manager, technical founder, or on-call responder who now has to answer three questions before attackers act: where am I exposed, what executed, and which secrets do I rotate. It walks the live incident in order, so you can go from an advisory to a contained blast radius inside the first hour with the tools you already have. It does not sell you a scanner and it does not stop at "update the package."
Most repo guides cover triaging a known CVE or reading whether a compromise flag is real. This one assumes the flag is real and the clock is running. The named 2026 campaigns propagate in minutes, so the work is enumeration and rotation under time pressure, not deliberation.
What "contained blast radius" actually means here
Contained means every place that resolved a bad version is found, each is tagged contain-or-wait, and every secret reachable from an affected host is rotated in dependency order. It is a state you can prove from audit logs, not a feeling.
The trap is treating this as a dependency-update task. Removing the package cleans your local tree; it says nothing about what a payload already exfiltrated. JFrog is explicit that removal is not enough: if an affected version ran, you revoke credentials, inspect every writable repository branch, and rebuild the host or CI runner from a clean image. So the deliverable is not a green lockfile. It is a rotation set and a persistence audit.
The order of operations matters because the failure modes cluster at the boundaries: people who investigate before containing let the next build run, and people who rotate before scoping leave the cascade intact.
Confirm the advisory and pull the identifiers
The first five minutes are for turning a vague alert into a precise list of bad versions and search terms. You cannot enumerate exposure against "some npm thing is bad." You need exact package names, affected version ranges, the campaign name, and any CVE.
Cross-check four sources: GitHub Security Advisories (GHSA), the CISA Known Exploited Vulnerabilities (KEV) catalog, OSV.dev, and the registry or vendor post-mortem. GitHub now imports OSV and OpenSSF malicious-package data, and setting type=malware in advisory search pulls supply-chain malware advisories such as malicious npm packages. Search on both the CVE and the campaign name, because the two index different corners of the web.
Here are the confirmed identifiers worth keeping in your search bar.
| Incident | Identifier | Catalog |
|---|---|---|
| Nx Console | CVE-2026-48027 | CISA KEV |
| trivy-action #1 | CVE-2026-28353 (CVSS 10.0) | GHSA/NVD |
| tj-actions (2025) | CVE-2025-30066 | CISA KEV (Mar 2025) |
The campaign name is as useful as the CVE. Search terms that surface post-mortems fast: "Shai-Hulud," "Sha1-Hulud: The Second Coming," "CHAINDROP," "Mini Shai-Hulud," "TeamPCP," and "Megalodon." Knowing the campaign changes your containment. A stolen-token wave demands npm-token revocation and re-publish checks; a credential-free OIDC or CI wave demands you treat the pipeline identity itself as compromised.
Know which campaign you are fighting
The named 2026 campaigns differ by how the attacker got in, and that difference dictates what you rotate. A responder who misreads the initial-access technique rotates the wrong class of secret and misses the real foothold.
| Campaign | Date | Initial access | Scale |
|---|---|---|---|
| Shai-Hulud | Sep 2025 | Phishing-stolen npm tokens + worm | 500+ packages |
| Shai-Hulud 2.0 | Nov 2025 | Pre-install execution | 25,000+ repos / ~350 users |
| Mini Shai-Hulud (TeamPCP) | May 2026 | GitHub Actions Pwn Request + OIDC theft | 84 artifacts / 42 pkgs in 6 min |
| trivy-action (TeamPCP) | Mar 2026 | Tag force-push (credential abuse) | 76 of 77 tags |
| CHAINDROP/keyv | Aug 2026 | Maintainer takeover + worm | 444+ pkgs / 1,381 versions |
Read the table as three response shapes. Stolen-token waves (Shai-Hulud) mean revoke npm tokens and audit re-publishes. Pre-install and worm waves (Shai-Hulud 2.0, CHAINDROP) execute on virtually every build server processing the infected package and bypass static scanners that inspect code during later build stages, so "our scanner is green" is not evidence of safety. Credential-free CI waves (Mini Shai-Hulud, trivy-action) abuse the pipeline itself: Mini Shai-Hulud chained a GitHub Actions Pwn Request, cache poisoning, and OIDC token extraction from runner memory, producing the first npm attack with valid SLSA Build Level 3 attestations. For that class, treat the CI identity as compromised, not just the token.
The lesson embedded in that number: rank your response by install-time execution and reachable secrets, not by package popularity or download counts. The exposure list a human builds is always lagging the worm.
Enumerate exposure across every lockfile and pipeline
Enumeration means grepping every lockfile, manifest, and workflow file across every repo, plus checking transitive paths and tag-referenced actions. The output is a complete list of repos, services, and pipelines that resolved a bad version.
Do not stop at direct dependencies. The bad version often arrives transitively. Three complementary techniques cover the ground:
- Code search. A quick
gh search codeacross your repos locates every consumer in minutes. Grep for the exact bad version strings inpackage-lock.json,yarn.lock, andpnpm-lock.yaml. - Dependency graph. GitHub's dependency graph shows the transitive path that brought in a dependency via "Show paths," and can be filtered by relationship status: direct, transitive, or inconclusive. The GraphQL API returns a relationship field with direct, transitive, or unknown values.
- Workflow YAML. Grep every
.github/workflows/*.ymlfor the compromised action, and for each hit, resolve whether it is pinned to a tag or a SHA.
That last check is the highest-leverage move in the whole runbook. A repo can be fully exposed while its manifest is unchanged, because a Git tag maps to a commit and, by default, anyone with push access can reassign it to a different commit. In the trivy-action case the attacker force-pushed 76 of 77 version tags in aquasecurity/trivy-action and all 7 in setup-trivy; the only clean tag was 0.35.0, and every other release from 0.0.1 through 0.34.2 served a malicious entrypoint.sh. The release page did not change: same name, date, description, but the code behind each tag contained a stealer.
Narrowing from ecosystem to your rotation set
- 444+Malicious versions published
CHAINDROP reached 444+ packages across 1,381 versions
- found by gh search codeYour repos referencing the package
direct and transitive
- subsetPipelines that resolved a bad version
SHA-pinned actions excluded
- smallerHosts where install-time code ran
contain these now
- your rotation setSecrets reachable from those hosts
rotate in dependency order
Only workflows pinned to a commit hash ran the code they always ran. In two consecutive years, workflows referencing actions by 40-character commit hash were completely immune, including the March 2025 wave that compromised 23,000+ repositories. So for every tag-pinned action, verify the commit SHA against a known-good state. Do not trust GitHub's "Immutable Release" badge: it did not stop the attack, and the attacker may have locked the malicious state in place.
Building the exposure list under time pressure is exactly where a plain-English search tool earns its place. If you also need to stand up a response team fast because your on-call is a generalist, you can search for the specific experience this incident demands rather than posting a job and waiting.
The procedure, in order
This is the runbook to execute start to finish. Steps 5 and 6 run parallel to 7 once containment is done; the numbers are the order you start them, not a strict serial chain.
Advisory to contained blast radius
- Confirm the advisory and pull identifiersCross-check GHSA, CISA KEV, OSV.dev, and the vendor post-mortem. Record exact package names, version ranges, campaign name, and any CVE. Done when you have a definitive bad-version list and IOCs.
- Isolate hosts and runnersPause affected CI pipelines and quarantine build runners and dev machines where install-time code may have executed. Plan a clean-image rebuild, not just file removal. Done when no further run can install the bad version.
- Enumerate exposure everywhereGrep all lockfiles and workflow YAML for bad versions and tag-referenced actions. Use gh search code, dependency graph Show paths for transitive hits, and check tag-repointing on tag-pinned actions. Done when the list of exposed repos, services, and pipelines is complete.
- Decide contain-or-wait per hitIf bad code executed on a host with reachable secrets, contain now. If the lockfile pins a safe version and never installed the bad one, monitor. Watch the keyv exception where a ransom-style description means follow IR guidance before revoking. Done when each hit is tagged.
- Scope the blast radius before rotatingFor each affected host, list every reachable secret: npm and GitHub tokens, cloud keys, SSH, Kubernetes, Vault, CI secrets. Identify what each can access before touching it. Done when you have an explicit rotation set.
- Revoke and rotate in dependency orderRotate break-glass and admin first, then service accounts, then users. Update all consumers and invalidate tokens and sessions. Rotate GitHub secrets at repository, environment, and organization levels. Done when old credentials fail and new ones work.
- Regenerate clean dependency stateAdd overrides or resolutions or pin known-good, run npm ci from a verified lockfile, rebuild, and re-scan for IOCs. Redeploy clean builds and invalidate CDN caches if bundles were published. Done when the IOC grep is clean and builds reproduce.
- Hunt for persistenceCheck for attacker-planted workflows, new PATs, OAuth grants, deploy keys, repos flipped public, and altered branch protections. Done when audit logs show no attacker-created persistence.
On the dependency-remediation mechanics: the simplest immediate step is to delete the specific compromised package's folder within node_modules, and on the next install npm will pull the known-good version if the malicious ones were removed. For critical dependencies, manually pin to a specific known-good version in package.json to prevent automatic upgrades. Prefer npm ci, which deletes node_modules, installs exactly the lockfile versions, fails if the lockfile is inconsistent, and never modifies the lockfile. You usually do not need to delete the lockfile: only remove it if, after adding overrides or resolutions and reinstalling, the resolver still keeps the compromised version.
Which secrets to rotate, and in what order
Rotate every secret reachable from an affected host, not only the flagged token, and rotate in dependency order. The modern implants are indiscriminate harvesters, so scoping the reachable set is the difference between a one-hour incident and a one-week one.
CISA states the actor targeted GitHub PATs and cloud API keys for AWS, GCP, and Azure, exfiltrated them, and uploaded them to a public "Shai-Hulud" repo via the GitHub API. The CHAINDROP variant's harvester scans over 300 unique patterns across many credential stores on a developer machine.
| Credential class | Where it lives | Rotate when |
|---|---|---|
| GitHub PATs / session tokens | Dev machine, CI env | Reachable during any bad run |
| Cloud keys (AWS/GCP/Azure) | Env vars, config files | Reachable during any bad run |
| SSH private keys (id_rsa) | ~/.ssh, agent | Host ran install-time code |
| Vault tokens / K8s SA tokens | Runner memory, mounts | CI runner in scope |
| npm registry tokens | .npmrc | Stolen-token campaign confirmed |
CHAINDROP also targets AI-tooling credentials (Anthropic, Claude, Codex, Cursor, OpenAI, Gemini) and reads runner process memory to steal ACTIONS_ID_TOKEN_REQUEST_TOKEN. Earlier waves parsed .git/config for embedded credentials. The point of the table is not completeness; it is that "the npm token" is never the whole answer.
The sequencing rule: start by identifying what the secret can access, not by rotating it immediately. Then rotate break-glass and admin credentials first, service accounts next, users last, updating all consumers and invalidating tokens and sessions as you go. Rotate GitHub secrets at the repository, environment, and organization levels, because a secret set at only one level leaves the others live.
Rotating only the flagged token leaves the cascade that turns a one-hour incident into a one-week one.
How this goes wrong: failure modes and false positives
The costliest mistakes in this runbook are false confidence, not missing knowledge. Each of the following has burned real teams; each has a one-line check.
- Grep-by-package-name misses tag rewrites. A repo can be exposed while its manifest is unchanged. Check: is the action pinned to a 40-char SHA or a tag? Only SHA pins are clean.
- "I deleted node_modules, I'm safe" false positive. Removal ignores exfiltration that already happened; removing a JS file does not show its collected secrets were not already exported. Check: was the secret store reachable during the run window?
- Rotating the stolen token first can arm the payload. In keyv and cacheable, revoking the stolen token is exactly what arms the payload. Check: look for a ransom-style token description before revocation.
- Rotating only the flagged token leaves the cascade. If it accessed other secrets, those are compromised too. Check: enumerate every secret reachable from the host first.
- Deleting the lockfile unnecessarily can pull a newer bad version. Only remove the lockfile if overrides or resolutions plus reinstall still keep the compromised version. Check: does the lockfile already pin a safe version?
- Trusting GitHub's immutable badge. The badge did not stop the attack; the attacker may have locked the malicious state in place. Check: verify the commit SHA against a known-good, not the badge.
- Rotation without a persistence hunt. If you saw suspicious usage, rotating does not remove the leak. Check: audit logs for attacker-created PATs, workflows, and repo visibility flips.
- Assuming trusted publishing removes the risk. If an attacker compromises the workflow or the account behind the job, the publish action is still authenticated as a trusted path. Check: treat the CI identity as compromised, not just the token.
That last one is not theoretical. Red Hat's wave shipped 96 compromised package versions across 32 @redhat-cloud-services packages through GitHub Actions OIDC. Trusted publishing reduces one secret class but not the dependency on pipeline integrity. Note also the limit of automated tooling: GitHub secret scanning supports 39 token types with push protection, but custom API keys and internal tokens are not detected, so your custom credentials will not trip an alert.
Contain-or-wait decision per hit
Hunt for persistence, then verify you are done
Persistence hunting is the last step and the one teams skip when the pressure lifts. Rotation stops the attacker using stolen credentials; it does nothing about footholds they planted while they had access.
Look for attacker-created artifacts: planted workflows such as shai-hulud-workflow.yml, new PATs, OAuth grants, deploy keys, repositories flipped from private to public, and altered branch protections. Datadog observed that Shai-Hulud 2.0 backdoors up to 100 of the victim's own packages, and if it can neither replicate nor exfiltrate it attempts to delete the home directory, so also confirm your own published packages were not backdoored during the window. Removing a JavaScript file does not demonstrate that collected secrets were not already exported.
Before you call the incident contained
- Every lockfile and workflow across all repos has been grepped for the bad versions
- Every tag-pinned action is verified against a known-good commit SHA
- Each exposed repo is tagged contain or wait
- Every secret reachable from an affected host is enumerated, not just the flagged one
- Secrets are rotated in dependency order and all consumers are updated
- GitHub secrets rotated at repository, environment, and organization levels
- npm ci from a verified lockfile reproduces clean builds and the IOC grep is clean
- Audit logs show no attacker-created PATs, workflows, deploy keys, or visibility flips
- Your own published packages confirmed not backdoored during the run window
Keeping the runbook current and staffing it
The two variables that go stale are the campaign list and your team's ability to execute. Both need a re-check cadence.
For the campaign list, subscribe to CISA KEV and GitHub Security Advisory feeds and re-read your grep patterns whenever a new named wave lands. GitHub has shipped defensive defaults you should confirm are on: a three-day cooldown delaying version update PRs so findings can be addressed before code intake, and push protection for the 39 supported token types. Adopt SHA-pinning tooling so future waves cannot reach you through tag rewrites, and enforce it in review.
The harder gap is people. Refolk's index shows only 59 US practitioners matching supply-chain, incident-response, and CI/CD-security signals together, and zero under the same narrow filter in Germany, so most teams will run this playbook with generalist app-sec staff. That is precisely why a pre-written, order-of-operations runbook matters more than a specialist you do not have.
If you decide to hire ahead of the next incident, the title distribution tells you what the market actually calls this work.
| Current title | Count in 25-profile sample | Share (derived) |
|---|---|---|
| Application Security Engineer | 15 | 60% |
| Product Security Engineer | 8 | 32% |
| Senior Product Security Engineer | 1 | 4% |
| Senior Security Researcher | 1 | 4% |
The pool is heavily US-concentrated and thin, which is a planning fact, not a footnote. Stolen credentials drive 22% of incidents and take an average of 292 days to resolve, so the cost of not having this runbook ready is measured in months, not the hour it takes to execute. Write down your own IOC-grep patterns, your rotation dependency order, and your persistence-hunt queries now, while nothing is on fire. When the advisory lands at 2am, the team that already has this document open is the team that contains the blast radius before the token is harvested.
COMPROMISED DEPENDENCY - contain first, investigate second. Package + bad versions: ____ Campaign / CVE: ____ (check GHSA, CISA KEV, OSV.dev) Owner: on-call ____ | Incident lead: ____ NOW: pausing all pipelines that could install this; quarantining runners. NEXT: enumerating repos via gh search code + dependency graph Show paths; checking tag-vs-SHA on every referenced action. DO NOT revoke any token until we confirm no ransom-style description (keyv exception). Rotation set will be scoped per host before any revocation.
Paste into your incident channel and fill the four fields; it forces containment before investigation.
Questions practitioners ask
compromised npm package what to do first
Contain first, investigate second. Pause the CI pipelines and quarantine the runners and dev machines where the package could have executed install-time code, then confirm the advisory against GitHub Security Advisories, CISA KEV, and OSV.dev. Do not start by deleting node_modules and declaring victory, because that ignores any exfiltration that already happened. Only after containment do you enumerate exposure and rotate the secrets reachable from each affected host.
how do I find which repos use a compromised package version
Search lockfiles and manifests, not just direct dependencies. A quick gh search code across your repos locates direct consumers in minutes, and GitHub's dependency graph Show paths view reveals the transitive path that pulled in a bad version. For GitHub Actions, also check whether each action is pinned to a 40-character commit SHA or to a tag, since tag-force-push attacks change the code behind an unchanged tag without touching your manifest.
which secrets should I rotate after a dependency compromise
Rotate every secret reachable from the affected host, not only the flagged token. The 2026 implants target GitHub PATs, cloud API keys for AWS, GCP, and Azure, SSH private keys, Kubernetes service account tokens, HashiCorp Vault tokens, npm tokens, and AI-tooling credentials. One harvester scans over 300 patterns across many credential stores. Enumerate what each secret can access first, then rotate in dependency order: break-glass and admin, then service accounts, then users.
is deleting node_modules enough after a compromised package
No. Removal cleans your local state but does nothing about credentials the payload may have already exported during a run. If an affected version ran on a host with reachable secrets, you must revoke credentials, inspect every writable branch, and rebuild the host or CI runner from a clean image. Treat file removal as one step in dependency remediation, never as proof of safety.
why should I not revoke the stolen token immediately
Because in at least one 2026 case revoking the stolen token was exactly what armed the payload. The keyv and cacheable worm used a ransom-style dead-man's switch, so standard revoke-first muscle memory triggered destruction. Before you revoke, check for a ransom-style token description and follow the incident-response guidance for that specific campaign. This is the one documented situation where the reflex is wrong.
how fast do I need to respond to a leaked CI token
Assume minutes, not hours. Public-commit scanners can detect an exposed token within about five minutes, so by the time you notice, it may already be harvested. Worms propagate at machine speed too: one wave published 84 malicious artifacts across 42 packages in six minutes. Rank your response by which hosts ran install-time code with reachable secrets, and contain those first rather than working alphabetically or by package popularity.
Try it on the search you came here for
Stop building boolean strings. Just describe the person.
Type one sentence. I plan the search, read GitHub, public LinkedIn and Crunchbase records, and the open web as it is right now, and hand back a ranked list with the reason next to every name.
01Describe them
One plain sentence. Role, city, stack, stage, whatever matters to you.
02I read the web live
GitHub, public LinkedIn and Crunchbase records, the open web. Not a database that went stale last quarter.
03You read the shortlist
Ranked, with the reasoning under every name. Open a profile, ask a follow-up, narrow it down.
- Staff backend engineers in NYC who shipped Rust in production
- Series A fintechs in SF under 50 people, growing headcount this year
- Maintainers of fast-growing Rust web frameworks on GitHub
- No boolean, no filters, no seat to buy. One box.
- Read at search time, so a profile updated yesterday counts today.
- Every step visible as it runs, every name with its reason.
500 free credits on sign-up. No card, no demo call. See real searches.