Confirming a Flagged Dependency Is Compromised or Clean
You will carry one flagged package release through a no-execution triage - fetch, publisher history, diff, provenance - to a defensible compromised-or-clean verdict.
A specific package version in your dependency tree just got flagged, and you have to decide before you ship or roll back: is this an actual supply-chain compromise, or noise? This guide is for engineers and engineering leads who need to carry one flagged release through a no-execution investigation and reach a verdict they can defend. It follows a real compromise end to end - the actual commands, the intermediate findings, and the wrong turns - so you can run the same path on the suspicious package in front of you.
The job here is narrow on purpose. This is not a library-quality grade or a maintenance audit. It is the security triage of one specific flagged release: publisher history, tarball diff, install-hook inspection, provenance check, verdict. Developers now do this almost weekly, and the failure modes are subtle enough that a written standard beats improvising each time.
Why one flagged release needs its own procedure
A flagged release is a fork in the road with a shipping deadline attached, and the honest answer usually lives in a diff, not in a scanner's badge. The generic advice - "use a scanner," "pin your versions" - does not tell you whether the release on your screen is malicious right now.
The threat model shifted. The dominant 2025-2026 vector is maintainer account takeover through phishing, not a new-account typosquat. On September 8, 2025 a threat actor took control of the npm account of the developer known as Qix through social engineering, then published malicious releases of highly popular packages including debug and chalk. The phishing email came from a domain, npmjs.help, registered just three days earlier. So the account you trust is exactly the account that turns on you, and "is the maintainer new?" is the wrong question.
These are not rare edge cases. Multiple large-scale compromises land per year, each spanning hundreds of packages.
The scale is why you need a procedure that works on any single release, fast, without executing anything. The rest of this guide is that procedure, carried through the keyv and node-ipc cases so you can see what each step actually turns up.
The confirmed waves you are triaging against
Recent supply-chain compromises follow a small number of shapes, and knowing the shape tells you where to look first. Table A lists the confirmed 2025-2026 npm waves from public reporting.
| Event | Date | Packages | Download volume | Vector |
|---|---|---|---|---|
| debug/chalk | 2025-09-08 | 18 | 2.6B/week | Maintainer phishing |
| Shai-Hulud | 2025-09-15 | 500+ removed | millions/week | Self-replicating worm |
| Shai-Hulud 2.0 | 2025-11-24 | 796 | 20M/week | Worm, preinstall |
| keyv wave | 2026-08 | 868+ (1,381 versions) | 2B/month | Maintainer/GitHub takeover |
Two cautions before you use these numbers. The download-volume units differ - weekly for some events, monthly for others - so they are not directly summable, and none of them count infected machines. Two billion monthly downloads is combined normal traffic for popular packages, not two billion compromises. When you scope your own exposure, the number that matters is whether you did a fresh install inside the compromise window, not the headline download count.
The shape also predicts the tell. The debug/chalk and keyv waves were trusted maintainers with hijacked accounts, so the publisher-history signal is a simultaneous multi-package publish burst. The Shai-Hulud waves were worms, and Shai-Hulud 2.0 moved execution to preinstall, which widened the blast radius across developer machines and CI pipelines. That single detail matters for your inspection: blocking postinstall alone is not enough.
Fetch and inspect without executing anything
The first rule of triage: get the published files onto disk without running a single line of them. For npm, npm pack <pkg>@<version> downloads the tarball without installing it, so you can extract and read the published files - which, importantly, can differ from the GitHub repo.
Extract the tarball with tar in a scratch directory and read package.json plus any bundled files. If you must run a command that installs, the npm CLI has an --ignore-scripts flag that prevents execution of any lifecycle hooks defined in package.json, and it works with npm install, npm ci, and npm update. But treat --ignore-scripts as a seatbelt, not a sandbox: installs can run arbitrary executables through git dependencies, so it is not hermetic. During triage, do not install at all. npm pack and read.
For PyPI, pip download --no-deps performs the same resolution and download as pip install but collects the distributions into a directory instead of installing them. One wrinkle: pip download --no-deps --no-binary :all: can still install build dependencies when resolving an sdist, so prefer wheels or a fully isolated scratch environment.
| Step | npm | PyPI |
|---|---|---|
| Fetch only | npm pack pkg@ver | pip download --no-deps |
| Block scripts | --ignore-scripts | (no lifecycle scripts on wheel install) |
| Diff versions | npm diff --diff=a --diff=b | manual tar/diff of sdists |
| Verify provenance | npm audit signatures | (no first-party equivalent) |
The no-execution triage path
- Fetchnpm pack the published tarball into a scratch dir
- Publisher historylist all versions and packages by the maintainer
- Inspectgrep for install hooks and oversized obfuscated files
- Diffcompare flagged version against last known-good
- Provenancerun npm audit signatures as corroboration, not proof
- Verdictcompromised or clean, with the diff as evidence
Capture the tarball and its hashes the moment you have them. Registry cleanup races your investigation: in the keyv case Snyk found eight malicious releases still tagged latest while three had already been removed. Your npm pack evidence can vanish mid-triage, so save it locally before anything else.
Reading the publisher's history
The load-bearing account signal is not "new maintainer" but "trusted maintainer, abnormal simultaneous multi-package publish." List every version of the flagged package and every package that maintainer owns, then look for a burst of near-simultaneous releases across their whole portfolio.
This is exactly how the keyv wave was mapped. Snyk enumerated every package returned by npm for the maintainer jaredwray and identified 11 malicious releases carrying the same two payload files. A single account, a single publishing session, a coordinated fan-out - that pattern is the signature of an account takeover, and it is far more reliable than account age.
The reason it works is mechanical. An attacker who phishes one account publishes everything at once, so timestamps cluster and payloads repeat. Which brings us to the strongest single tell.
Byte-identical payloads across releases catch a staged campaign faster than reading a single line of obfuscated code.
In the node-ipc case, the compromised node-ipc.cjs file was byte-for-byte identical across all three malicious versions, confirming a single staging operation. One obfuscated bundle dropped into several package contexts before a simultaneous publish produces identical hashes. So when you inspect the maintainer's portfolio, hash the suspicious files across releases. Matching hashes across supposedly independent versions is a near-conclusive tell of a staged campaign.
Inspecting the manifest and the changed files
Read the manifest for install hooks and the file list for one oversized obfuscated bundle. These two signals together account for most of what recent compromises look like on disk.
Grep the extracted package.json for preinstall and postinstall. Recent campaigns increasingly favor preinstall: Shai-Hulud 2.0 spreads under filenames math_init.js and Math_Symbol.js, using a preinstall script to execute during install time. The move to preinstall is what widened the CI blast radius, because the payload runs earlier in the install lifecycle.
Then look at file sizes. A malicious payload is usually one large obfuscated single-file bundle that stands out against the rest of the package. In node-ipc, the diff between the last clean release and the compromised version was a single line addition of 80,079 characters. You do not need to read that line to know it does not belong.
Now diff against your last known-good. npm ships a first-party command: npm diff --diff=pkg@good --diff=pkg@flagged prints diff patches of the files published to the registry. Add --diff-name-only to list only the changed files first, then open the ones that matter. Diff the tarballs, never the GitHub repo, because a prepublishOnly step can produce a registry artifact that differs from the source.
At this point in a real triage you can usually point to the exact added lines. That is your verdict evidence. Everything after this either corroborates it or complicates it.
Here is where the concentration of people who can run this well becomes a planning problem. In Refolk's index of professional profiles, the United States has 1,109 Application and Product Security engineers against 49 in Germany - a 22.6x gap once you divide the counts.
| Market | AppSec + ProdSec engineers | AppSec share of sample | US-to-market multiple (derived) |
|---|---|---|---|
| United States | 1,109 | 17/25 | 1.0x |
| Germany | 49 | 15/25 | 22.6x (derived) |
Non-US teams are far likelier to lack an in-house triager and to depend on a written standard like this one. If you need to build that bench, describing the specific work is the fastest way to find people who have actually done it.
Provenance and attestation: what it proves and what it does not
Provenance verifies where a package was built, not whether the source that entered the build was safe. Run the check, but read the result correctly.
Verify the provenance attestations of downloaded packages with npm audit signatures. The command fetches the signature data for each package version in the tree, validates the registry signatures against the current and historical npm public keys, and validates any provenance attestations against the sigstore certificate transparency log. To check a single package, run npm view package-name --json | jq '.dist.attestations'. If you prefer, cosign v2.4.0 verifies attestations in the bundle format used by npm provenance, GitHub Artifact Attestations, and Homebrew provenance.
Now the limitation you must not overstate. The npm signature is the registry's signature, not the publisher's, and a compromise of an individual package's publish credentials would still produce a signature that verifies cleanly. This is not theoretical. Attackers used the legitimate GitHub Actions release process to publish poisoned keyv 6.0.0, so it carried valid provenance information on npm. The signature verified where the package was built, but not whether the source code entering that process was safe.
That is why the ordering in this guide puts the diff before the provenance check. Some sources run provenance first, but because provenance can verify on a poisoned build, a green result read as "clean" is a false positive waiting to happen. Provenance is corroboration; the diff is proof.
Reading diff against provenance
The procedure
Run these eight steps in order on the single flagged release. The first seven are a triager's job and take about an hour total; the last is an engineering lead's job and takes hours if the verdict is "compromised."
Compromised-or-clean triage for one flagged release
- Scope the flagRecord the exact package name, version, and whether it is a direct or transitive dependency, plus the last version you shipped. You now have the precise spec and your last-known-good.
- Fetch without executingRun npm pack pkg@version (or pip download --no-deps) and extract the tarball in a scratch directory. Files are on disk and nothing has run.
- Check publisher historyList all versions and all packages by that maintainer and look for a simultaneous multi-package publish burst. You know whether this is one-off or a maintainer-wide event.
- Inspect manifest and filesGrep package.json for preinstall and postinstall, and scan for one oversized obfuscated single-file bundle. Hooks and suspicious bundles are enumerated.
- Diff against last known-goodRun npm diff --diff=pkg@good --diff=pkg@flagged --diff-name-only, then read the changed files. You can point to the exact added lines.
- Check provenance and attestationRun npm audit signatures and npm view pkg --json | jq '.dist.attestations'. You know whether provenance exists, treated as weak evidence.
- Reach a verdictDeclare compromised or clean with the diff and file hashes as evidence. Put the diff before provenance, because provenance verified cleanly on poisoned keyv.
- Remediate if compromisedPin or revert to last-known-good, rotate exposed secrets, search CI logs and artifact caches for install evidence, and rebuild affected hosts. No compromised version is resolvable and secrets are rotated.
Keep a copy-pasteable command block next to the procedure so you are not reconstructing flags under pressure.
mkdir triage && cd triage npm pack pkg@FLAGGED # download tarball, run nothing tar -xzf pkg-FLAGGED.tgz # extract to ./package grep -E "preinstall|postinstall" package/package.json find package -type f -size +50k # flag oversized bundles npm view pkg versions --json # publisher history / version list npm diff --diff=pkg@GOOD --diff=pkg@FLAGGED --diff-name-only npm diff --diff=pkg@GOOD --diff=pkg@FLAGGED # read the changed files npm audit signatures # corroboration only, not proof npm view pkg --json | jq '.dist.attestations' sha256sum package/<suspicious-file> # hash for cross-release comparison
Replace pkg, GOOD, and FLAGGED with your package name and versions. Run in a scratch directory.
How this goes wrong
Most bad verdicts come from a small set of predictable mistakes. Each one below has a real-world false positive or false negative and a specific check that defeats it.
- Reading a provenance pass as "clean." keyv 6.0.0 verified because it was built by the real GitHub Actions workflow. Check: diff the source; do not stop at a green
npm audit signatures. - Assuming
--ignore-scriptsis hermetic. A git dependency can still run a system binary. Check: do not install git deps during triage; inspect the tarball only. - Trusting the "new maintainer" heuristic. The 2025-2026 events were trusted maintainers with hijacked accounts. Check: look for simultaneous multi-package publish bursts, not account age.
- Diffing the GitHub repo instead of the tarball. A
prepublishOnlystep can make the registry tarball differ from the repo. Check: alwaysnpm packthe published artifact and diff that. - "Upgrade to newest" as remediation. No clean successor may exist; there was none after poisoned keyv 6.0.0. Check: confirm the target version predates the compromise window before pinning.
- Reading download volume as infections. Two billion monthly downloads is combined normal traffic, not compromised machines. Check: scope to your install window and lockfile timestamps.
- Treating
pip downloadas inert. It can trigger build steps for sdists. Check: use wheels or a fully isolated scratch environment.
The two that catch experienced engineers most often are the provenance false positive and the repo-versus-tarball trap. Both let a compromised release read as clean, which is the expensive direction to be wrong in. If you internalize only two habits from this guide, make them: diff the tarball, and never let a green attestation end the investigation.
Remediating once the verdict is "compromised"
The documented order is pin or revert first, then rotate, then hunt. Do them in that sequence, because rotating credentials while a compromised version can still resolve leaves you re-exposed on the next install.
First, pin or downgrade to a last-known-good release. Confirm that the target version predates the compromise window - do not reach for "newest," because a clean successor may not exist yet, as with keyv 6.0.0.
Then, following Singapore's CSA advisory, treat affected systems as potentially compromised and rebuild them if the malicious packages were installed. Rotate exposed credentials, including cloud credentials, GitHub tokens, SSH keys, Kubernetes configurations, and Terraform credentials. Review cloud and source-code environments for unauthorized access following the package installation.
Before you assume the worst, scope your actual exposure. From the debug/chalk case: most apps were only at risk if they performed a fresh install during the compromise window, if the lockfile was created during that window, and if a compromised package was included directly or transitively. Check your lockfile timestamps against the known compromise window. If you did not install fresh during that window, your blast radius is far smaller than the headlines imply.
Scoping exposure in the debug/chalk case
- 18Packages compromised
2.6 billion weekly downloads combined
- subsetYour tree includes one
direct or transitive
- subsetFresh install in window
lockfile created during the window
- smallestActually exposed
rotate, rebuild, and hunt here
Finally, close out the job with a verification pass before you call it done.
Before you close the triage
- The tarball and file hashes are saved locally in case the registry pulls the release.
- You can point to the exact added lines in the diff between good and flagged versions.
- You checked for a simultaneous multi-package publish burst, not just account age.
- You diffed the published tarball, not the GitHub repo.
- You treated provenance as corroboration, not as proof of clean source.
- If compromised, the pinned target version predates the compromise window.
- If installed, exposed credentials are rotated and affected hosts rebuilt.
- You scoped exposure to your lockfile timestamps, not to download counts.
Keeping this current
The mechanics of this triage are stable, but the ecosystem's defaults are moving in your favor, so re-check two things periodically. npm v12 shipped with dependency lifecycle scripts off by default, and npm began scanning newly published packages for malware - though that scan applies to new publishes only, not the existing back catalog. Neither replaces the procedure; both shrink the window in which a fresh compromise reaches you.
Do not read the tooling improvements as a reason to skip the diff. The load-bearing evidence is still the changed files in the published tarball, and the reusable tells - simultaneous multi-package bursts and byte-identical payloads across releases - will outlast any specific CLI flag. When a new wave lands, confirm the vector, capture the tarballs before cleanup races you, and run the eight steps. The verdict is only as defensible as the evidence you saved to produce it.
Questions practitioners ask
How do I check an npm package before installing it?
Use npm pack pkg@version to download the published tarball without installing, then extract it with tar in a scratch directory and read the files. This runs no lifecycle scripts. Grep the package.json for preinstall and postinstall hooks and scan for a single oversized obfuscated file. To compare against a known-good release, run npm diff --diff=pkg@good --diff=pkg@flagged --diff-name-only and read the changed files.
Does valid npm provenance mean a package is safe?
No. Provenance verifies where a package was built, not whether the source that entered the build was safe. Poisoned keyv 6.0.0 was published through the legitimate GitHub Actions workflow and carried valid provenance that verified cleanly on npm. Treat a green npm audit signatures as necessary but not sufficient, and make the tarball diff your load-bearing evidence.
Is --ignore-scripts enough to inspect a package safely?
Not entirely. --ignore-scripts prevents npm lifecycle hooks from running, but installs can still run arbitrary executables through git dependencies, so it is not a hermetic sandbox. For triage, do not install at all: npm pack the tarball and read it on disk. Avoid resolving git dependencies during the investigation, and inspect only the extracted artifact.
What is the fastest reliable tell that a release is a staged compromise?
Byte-identical payloads across releases. In the node-ipc case the malicious node-ipc.cjs was byte-for-byte identical across three simultaneous versions, and Snyk found 11 keyv-family releases sharing the same two payload files. One obfuscated bundle dropped into several packages before a simultaneous publish produces identical hashes, so cross-release hashing catches campaigns faster than reading obfuscated code.
Should I just upgrade to the newest version to fix a compromised dependency?
Not automatically. A clean successor may not exist yet: no clean version was published after poisoned keyv 6.0.0, so upgrading would have kept you on a bad release. Pin or revert to a version you confirm predates the compromise window, then rotate exposed credentials and rebuild affected hosts if the malicious package was installed.
How can I reduce exposure to these attacks in the first place?
Add a publish delay. Setting pnpm's minimumReleaseAge to 1440 refuses versions published in the last 24 hours, and most malicious versions are caught and pulled within hours, so the delay converts most compromises into non-events. Also block install-time scripts by default and scope any incident to your lockfile timestamps rather than to headline download counts.
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.