By falco365 · Published May 7, 2026

One toolchain, six weeks, four campaigns: mapping the npm supply-chain worm cluster

TeamPCP, Mini Shai-Hulud, Shai-Hulud, and CanisterWorm are four names for one connected operation. The shared __decodeScrambled PBKDF2 fingerprint, Bun runtime evasion, and worm propagation architecture trace to a single toolchain that upgraded its C2 resilience twice and its OPSEC once — within a single propagation cycle.

One toolchain, six weeks, four campaigns: mapping the npm supply-chain worm cluster
Analysis

Four campaign names have circulated in security research over the past six weeks: TeamPCP, Mini Shai-Hulud, Shai-Hulud, and CanisterWorm. Each was reported by a different team — Wiz, Aikido, StepSecurity, Socket — as a distinct incident. Each had its own IOC set, its own victim list, its own timeline. Researchers linked them by TTP overlap. What has not been written in one place is the single story they compose: one toolchain, iterating on its own infrastructure twice within six weeks, and upgrading its OPSEC once within a single 24-hour propagation cycle.

The linking artifact is the __decodeScrambled PBKDF2 cipher. It appears 232 times in the Shai-Hulud payload. It appears 232 times in the TeamPCP Bitwarden payload. It appears in CanisterWorm. It appears in Mini Shai-Hulud. That count and structure is not a coincidence of independent development. It is a shared build artifact — the same obfuscation library compiled into every payload across the cluster. What the individual incident reports each called a "link to TeamPCP" is, in aggregate, a single operator running one toolchain across six weeks of continuous operation.

The cluster timeline
  • March 20, 2026 — TeamPCP initial compromise. aquasecurity/trivy-action and aquasecurity/setup-trivy compromised via tag force-update. Credential-stealing logic injected into action.yaml. Exfiltration to scan.aquasecurtiy[.]org (typosquat). C2 architecture: primary domain typosquat → Cloudflare Tunnel fallback → ICP canister fallback. C2 resilience tier 3.
  • March 23–24, 2026 — TeamPCP expands. Checkmarx KICS GitHub Action compromised. litellm on PyPI (versions 1.82.7, 1.82.8) infected via credentials stolen from litellm's CI/CD pipeline running the compromised Trivy Action. The worm is propagating: each victim's credentials fuel the next compromise.
  • April 22, 2026 — TeamPCP peak + CanisterWorm parallel track. Checkmarx KICS Docker images overwritten; Bitwarden CLI @bitwarden/cli@2026.4.0 compromised; Xinference PyPI infected. Separately: CanisterWorm infects Namastex-linked npm packages. CanisterWorm adds the ICP canister (cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0[.]io) as the primary exfil channel rather than a fallback — a distinct capability choice that signals either a separate operator reusing the toolchain or an evolution of CanisterWorm specifically for resilient exfil.
  • April 29, 2026 — Mini Shai-Hulud. Four SAP CAP npm packages (@cap-js/sqlite@2.2.2, @cap-js/postgres@2.2.2, @cap-js/db-service@2.10.1, mbt@1.2.48) and the PyPI lightning package (versions 2.6.2, 2.6.3) infected. First use of GitHub Actions OIDC token theft as the propagation vector. Exfil via public GitHub repositories with the description A Mini Shai-Hulud has Appeared — a detectable public signal. C2 tier 4 (private GitHub API as exfil channel).
  • April 30, 2026 — Shai-Hulud closes the loop. OIDC tokens stolen from the April 29 SAP/Lightning victims are used to publish intercom-client@7.0.4 — roughly 2 million weekly downloads — within 24 hours. Critical change: exfil moves from public GitHub repositories to private repositories. The detectable signal from Mini Shai-Hulud (public repo creation) was removed within one propagation cycle. The operator iterated their own OPSEC in real time.
The OPSEC upgrade from public to private exfil repos happened within 24 hours of Mini Shai-Hulud. The operator observed that public repository creation was a detectable signal — it was being used by StepSecurity and others to identify compromises — and removed it before the next victim was published. That is an automated adversarial adaptation loop running faster than most incident response timelines.

Three technical artifacts link the campaigns at the payload level. These are not TTP overlaps of the "both used postinstall hooks" variety. They are code-level fingerprints in the compiled output.

1. __decodeScrambled PBKDF2 cipher (232 occurrences). The obfuscation layer used across the cluster applies a PBKDF2-based cipher with a consistent seed (0x3039). The function __decodeScrambled appears exactly 232 times in payloads from Shai-Hulud, TeamPCP Bitwarden phase, and Mini Shai-Hulud. This count is a build artifact — the number of obfuscated strings in the payload. Identical counts across independently discovered campaigns confirm a shared code base, not just shared technique.

2. Bun v1.3.13 as the second-stage runtime. Every payload in the cluster downloads Bun v1.3.13 from GitHub releases during npm install and uses it to execute the obfuscated credential stealer. The version is pinned — not "latest Bun" but specifically v1.3.13. Pinning to a specific version is a build artifact that would differ across independently-developed payloads. The consistent version across campaigns confirms they were built from the same codebase at the same point in the Bun release history.

3. Daemonization pattern (__DAEMONIZED=1). The payload forks a detached child process with the environment variable __DAEMONIZED=1 to break process-tree correlation between npm install and the credential theft that follows. This specific variable name and pattern appears consistently across the cluster and is absent from unrelated npm supply-chain payloads.

C2 architecture evolution

The cluster's command and control architecture changed twice across six weeks. Each change increased resilience — the operator moved up the C2 tier hierarchy in response to defenders' ability to block or seize prior infrastructure.

PhaseDatePrimary C2FallbackTierDefender countermeasure
TeamPCPMar 20Domain typosquat (scan.aquasecurtiy[.]org)Cloudflare Tunnel → ICP canister3Domain seizure possible on primary; Tunnel domain rotatable
CanisterWormApr 22ICP canister (cjn37-uyaaa-aaaac-qgnva-cai)Conventional HTTPS (telemetry.api-monitor[.]com)3ICP canister cannot be seized; blocking requires egress policy on raw.icp0.io
Mini Shai-HuludApr 29Public GitHub repos (victim's own account)Commit dead-drop (OhNoWhatsGoingOnWithGitHub)4Public repo creation is detectable; dead-drop requires scanning public commits
Shai-HuludApr 30Private GitHub repos (victim's own account)None needed4Traffic to api.github.com is allowlisted virtually everywhere; private repos invisible to external monitoring

The progression is not random. Each tier removes a specific defensive countermeasure. The move to private GitHub repos as the exfil channel is particularly significant: all traffic goes to api.github.com, which appears in every corporate firewall's allowlist and every CI/CD egress policy. The credential exfiltration is indistinguishable from a legitimate CI job pushing artifacts to GitHub. There is no C2 domain to block, no suspicious IP to sinkhole, no non-standard port.

The propagation architecture: how the worm works

Understanding this cluster requires understanding that it is not a one-time compromise pattern. It is a self-propagating system where each victim becomes the next attack launchpad.

  1. Initial compromise. The operator gains CI/CD access via one of three vectors across the cluster's history: GitHub Actions tag force-update (TeamPCP); maintainer account takeover via 2FA bypass (CanisterWorm); OIDC token theft from a compromised upstream pipeline (Shai-Hulud). Each method yields npm or PyPI publish credentials for the victim.
  2. Payload injection. The stolen credentials are used to publish a malicious patch version of every package the token can access. The payload is injected via a preinstall or postinstall hook. The version bump is minimal (patch release) to avoid triggering lockfile review. Package size increases substantially — 6 MB to 17.8 MB in the intercom-client case — which is a reliable detection signal.
  3. Credential sweep. When any developer or CI runner installs the malicious version, the payload executes and sweeps: npm tokens from .npmrc; GitHub OIDC tokens; AWS IMDS credentials; GCP metadata service tokens; Azure connection strings; SSH keys; Kubernetes service account tokens; secrets in environment variables.
  4. Worm propagation. Every stolen npm token is enumerated for packages it can publish. Each reachable package becomes the next infected host. The worm expands without operator intervention.

This architecture means the blast radius of any single compromise scales with the publish-token scope of the victim's CI/CD environment. The SAP CAP maintainers had tokens that could publish to @cap-js/* packages. Those packages' consumers included CI pipelines with tokens that could publish to intercom-client. The worm found the path from a niche SAP toolchain package to a 2-million-download customer success SDK in 24 hours.

What the cluster proves about automated OPSEC

The transition from public to private exfil repos between Mini Shai-Hulud (April 29) and Shai-Hulud (April 30) is the single most analytically significant observation in this cluster. It is not a planned capability that was always present. The Mini Shai-Hulud payloads created public repos. By the time the worm reached intercom-client the following day, the payloads created private repos.

This means the operator either:

  • Manually updated the payload between April 29 and April 30 after observing that public repository creation was being used as a detection signal, or
  • Had already built both variants into the payload, with the private-repo variant triggered by the victim environment profile (intercom-client's environment was detected as higher-value, warranting quieter exfil)

Either interpretation is operationally significant. In the first case: the operator monitors defensive research in near-real-time and iterates faster than incident response. In the second case: the payload has environment-aware OPSEC that adapts its behavior based on detected victim context. Both cases describe a capability that is qualitatively different from commodity malware.

Detection across the cluster

The shared toolchain means a single detection rule covers the entire cluster. Any one of the following signals catches all four campaigns:

  • Bun download during npm install. Outbound egress to github.com/oven-sh/bun/releases from a CI runner that does not legitimately use Bun is a high-confidence indicator. This signal is present in every payload across the cluster. Flag version 1.3.13 specifically for highest confidence; flag any Bun download from an install step for broad coverage.
  • Package size anomaly on patch release. A patch version bump that increases package size by more than 50% is anomalous. intercom-client@7.0.4 grew from 6 MB to 17.8 MB. This signal does not require payload analysis — it is detectable from npm registry metadata before installation.
  • SLSA provenance gap on a package with prior attestations. Every Shai-Hulud-phase payload was published manually, breaking the OIDC trusted-publisher chain that prior versions established. A package that shipped SLSA provenance in version N but not in version N+1 — especially on a patch bump — is a high-confidence indicator of account compromise or build pipeline injection.
  • Private GitHub repository creation by CI service accounts. In the Shai-Hulud phase, exfil happens via private repo creation by whatever GitHub token the CI environment holds. A GitHub audit log entry showing repository creation by a service account immediately following an install step is a behavioral indicator.
  • Detached child process with __DAEMONIZED=1 surviving beyond npm install exit. The payload breaks process-tree correlation by daemonizing. Process telemetry that shows a child process surviving after its parent (npm install) exits, carrying __DAEMONIZED=1 in its environment, is a conclusive payload indicator.
  • __decodeScrambled in any JavaScript payload. YARA/grep rule on this function name catches every variant in the cluster. Zero false positive risk — the name has no legitimate use.
Indicators of compromise

Compromised packages across the cluster (by phase):

  • TeamPCP: aquasecurity/trivy-action, aquasecurity/setup-trivy, checkmarx/kics-github-action, @bitwarden/cli@2026.4.0, xinference PyPI 2.6.0–2.6.2, litellm PyPI 1.82.7–1.82.8
  • CanisterWorm: @automagik/genie 4.260421.33–4.260421.39, pgserve 1.1.11–1.1.13, @fairwords/websocket, @fairwords/loopback-connector-es, @openwebconcept/design-tokens, @openwebconcept/theme-owc
  • Mini Shai-Hulud: @cap-js/sqlite@2.2.2, @cap-js/postgres@2.2.2, @cap-js/db-service@2.10.1, mbt@1.2.48, lightning PyPI 2.6.2–2.6.3
  • Shai-Hulud: intercom-client@7.0.4

Toolchain fingerprints (present in all phases):

  • __decodeScrambled function name (232 occurrences per payload)
  • Bun v1.3.13 download from github.com/oven-sh/bun/releases during install
  • __DAEMONIZED=1 environment variable on forked child process
  • setup.mjs + router_runtime.js or execution.js payload file names

Network indicators (defanged, by phase):

  • TeamPCP: scan.aquasecurtiy[.]org, checkmarx[.]zone, audit.checkmarx[.]cx, championships-peoples-point-cassette.trycloudflare[.]com
  • CanisterWorm: telemetry.api-monitor[.]com, cjn37-uyaaa-aaaac-qgnva-cai.raw.icp0[.]io
  • Mini Shai-Hulud: public GitHub repos with description A Mini Shai-Hulud has Appeared
  • Shai-Hulud: api.github[.]com (private repo creation — indistinguishable from legitimate traffic by URL alone; detect by behavior)

Behavioral artifacts:

  • Outbound connection to 169.254.169[.]254 (AWS IMDS) from a process spawned by npm install
  • Outbound connection to metadata.google[.]internal from same context
  • GitHub commit messages containing OhNoWhatsGoingOnWithGitHub (Mini Shai-Hulud dead-drop)
  • GitHub commits authored by claude <claude@users.noreply.github.com> touching .vscode/ or .claude/ paths (Mini Shai-Hulud persistence)
Detection and mitigation
  • Enable npm install --ignore-scripts by default in CI. The entire preinstall/postinstall execution chain across this cluster is blocked by this flag. This is the structural fix that applies to every campaign variant, present and future. Explicitly allowlist packages that require lifecycle hooks; treat every other hook as suspicious.
  • Pin GitHub Actions by SHA, not by version tag. The TeamPCP initial compromise relied on tag force-update. Pinning to a commit SHA means the tag can be moved without affecting your workflow. This is the structural fix for the GitHub Actions vector.
  • Gate on SLSA provenance attestations. Any npm package that drops provenance attestations in a patch release should trigger review before installation. Automate this check in CI. The Shai-Hulud compromise was detectable at this layer before payload analysis.
  • Monitor for Bun egress from CI runners. Add a network egress alert for outbound connections to github.com/oven-sh/bun/releases from CI runners that do not legitimately use Bun. This single rule covers the entire cluster.
  • Enforce IMDSv2 with hop limit 1 on all EC2 instances and containers. The Shai-Hulud payload queries the AWS Instance Metadata Service. IMDSv2 with a hop limit of 1 prevents container-originating queries from reaching the IMDS, blocking cloud credential theft at Stage 3.
  • Audit GitHub audit logs for private repository creation by CI service accounts. In the Shai-Hulud phase, the exfil signal moved to private repos. The GitHub audit log still records repository creation events. Any CI service account creating a private repository immediately following an install step is a high-confidence indicator.
  • Treat any host that ran an affected version as fully compromised. Rotate all credentials reachable from the environment: GitHub tokens, npm publish tokens, AWS/GCP/Azure credentials, Kubernetes service account tokens, SSH keys, and secrets in environment variables.
Attribution discipline

"TeamPCP," "Shai-Hulud," "Mini Shai-Hulud," and "CanisterWorm" are tracking names assigned by Wiz, StepSecurity, Aikido, and Socket respectively. The __decodeScrambled fingerprint, the Bun v1.3.13 pin, and the daemonization pattern link them at the toolchain level. This is sufficient to conclude that the campaigns share a codebase. It is not sufficient to conclude they share an operator.

Three scenarios are consistent with the available evidence:

  • A single operator running the entire cluster, iterating the toolchain across six weeks
  • A primary operator (TeamPCP) whose toolchain was reused by one or more additional actors (CanisterWorm, Shai-Hulud) who independently obtained or forked the code
  • A toolchain sold or shared privately within a small group, with each actor adapting the C2 infrastructure independently

The OPSEC iteration observed between Mini Shai-Hulud and Shai-Hulud — public to private exfil repos within 24 hours — is the strongest evidence for the single-operator scenario. A forked codebase used by an independent actor would not have iterated in real time in response to a detection signal from a different variant. But this inference is not conclusive: a coordinated group operating the same toolchain could also produce this pattern.

We use the individual campaign names throughout hub articles for traceability against existing reporting. We treat the toolchain overlap as a linking indicator, not a hard attribution. Defenders can act on the shared IOCs and detection rules without needing the attribution question settled.

Criminal-market signal

We ran comprehensive dark-web sweeps across 19+ live onion search engines for every campaign in this cluster. Results are consistent across all phases:

  • TeamPCP — clean negative across 18 pages (2 exploit forums, 15 marketplaces, 1 other)
  • Mini Shai-Hulud — clean negative across 19 pages (2 exploit forums, 16 marketplaces, 1 other)
  • Shai-Hulud — clean negative across 76 pages (13 exploit forums, 59 marketplaces, 3 news mirrors)
  • __decodeScrambled — pending sweep (Tier 1 artifact, zero homonym risk)
  • OhNoWhatsGoingOnWithGitHub — pending sweep (Tier 2 self-attribution marker)

Three clean negatives across the same cluster is a pattern, not a sensor failure. The interpretation is H2 (genuine absence): this operator uses the toolchain in-house. They are not selling the worm capability, not selling the stolen credentials through observable dark-web markets, and not commoditizing the attack. The absence is consistent with an operator whose business model is the credentials themselves — not the sale of the attack capability to others.

This shapes defensive monitoring posture. There is no early warning available from criminal-market surveillance for this campaign class. The detection surface is the registry layer: package metadata anomalies, SLSA provenance gaps, Bun egress from CI runners. Not .onion forums.

What six weeks of continuous operation means

This cluster has been running since March 20, 2026. As of this writing it has compromised at least ten distinct software vendors, infected packages across npm and PyPI with a combined download count exceeding 25 million weekly at peak, and stolen credentials from CI/CD pipelines across the software development supply chain. It has iterated its C2 architecture twice and its OPSEC at least once.

The operational longevity — six weeks of continuous activity with no apparent disruption — reflects the structural advantages of the campaign's design. The worm propagates without sustained operator effort. Each new victim extends the reach automatically. The C2 infrastructure, once moved to private GitHub repos, produces traffic that is functionally invisible to network perimeter controls. The only layer where this campaign is reliably detectable is the one most organizations have invested least in: npm and PyPI publish audit logs, package provenance attestation, and CI install script monitoring.

The structural defenses have not changed since the first TeamPCP compromise in March. --ignore-scripts by default. GitHub Actions pinned by SHA. SLSA provenance as a CI gate. IMDSv2 enforced. None of these are new recommendations. The campaign has persisted for six weeks because the recommendations are not widely implemented, not because defenders lack the tools to stop it.