Joyfill npm Supply-Chain Compromise — Import-Time RAT on Developer Workstations
@joyfill/components and @joyfill/layouts ship a five-stage chain: obfuscated in-bundle loader → blockchain C2 resolver → two staged downloaders → Socket.IO RAT with worm-like self-propagation → Python credential stealer. The payload runs on import, so npm install --ignore-scripts does not stop it.Executive Summary
On 2026-07-28 an attacker published malicious prerelease builds of two Joyfill npm packages — @joyfill/components (4.0.0-rc24-2773-beta.4/.5/.6) and @joyfill/layouts (0.1.2-2773.beta.0/.1/.2). Both carry the same obfuscated implant welded into the distribution bundles, not the source tree. The attacker's objective is not the CI runner: it is the developer workstation, where a Socket.IO remote-access trojan opens an interactive shell and a Python stealer harvests browser credentials, browser-extension wallets and password managers, Git and GitHub CLI credentials, and OS keychains.
The defining characteristic — and the reason most package-security controls miss it — is that execution is triggered by import, not by install. There is no postinstall hook anywhere in package.json, so npm install --ignore-scripts, install-hook sandboxes, and lifecycle-script scanners all pass the package clean. Any unit test, bundler run, dev server, SSR render, or production deploy that loads the module is sufficient to start the chain.
Stage 1 is also deliberately invisible to network-reputation controls: its only outbound contacts are legitimate public blockchain APIs. The implant reads the latest outbound transaction from a fixed Tron address, follows the pointer to a BNB Smart Chain transaction, XOR-decrypts the transaction input, and evaluates it. Aptos is the fallback pointer channel. That means the operator can rotate the live C2 without republishing anything, and blocking a C2 domain does not break the resolution step.
The highest-value defensive angle is therefore endpoint behavior on the developer workstation, not registry scanning. Four behaviors are near-unmistakable: a detached node -e child process carrying an inline campaign tag; a Node process shelling out to python, Get-Clipboard, pbpaste, xclip or tasklist; a runtime npm --prefix "<tmp>" install socket.io-client; and modification of the global npm CLI (lib/cli.js), GitHub Desktop's main.js, the Discord desktop core module, or the @vscode/deviceid module used by VS Code, Cursor and Antigravity.
Patching the global npm CLI is the worm loop and the reason this is not a contained incident: once npm/lib/cli.js is injected, every subsequent npm invocation on that machine re-executes the loader, and anything built or published from that workstation can carry the implant onward.
Defender priority: treat any host that imported a 2773 prerelease as interactively compromised, not merely exposed. Isolate it, then rotate every credential reachable from that Node process — npm tokens, Git/GitHub PATs and gh CLI config, browser-saved passwords and cookies, wallet and password-manager browser extensions, cloud keys — from a separate clean machine. Reinstalling the package does not remove persistence written into VS Code, Discord, GitHub Desktop or the global npm CLI.
Source Review & Web Hunter Notes
Five sources fetched and snapshotted verbatim on 2026-08-02 into the pack's -sources/ directory. Every atomic indicator shipped below traces to at least one of them. Two are primary vendor analyses with independent code recovery; the remaining three are secondary reporting used only for corroboration, not for new indicators.
| Tier | Source | Key finding | Carry forward |
|---|---|---|---|
| Primary | StepSecurity — Threat Intel blog (2026-07-28) | Full five-stage chain deobfuscated: loader, blockchain resolver, campaign-gated C2 selection, Socket.IO RAT command verbs, injection targets and sentinel tags. Confirmed three ways (OSS AI scan verdict, Harden-Runner detonation, clean-vs-malicious bundle diff). | Yes |
| Primary | Socket Research Team (2026-07-28) | Independent static recovery. Adds tarball and per-bundle SHA-256s, intermediate stage hashes, exact XOR keys, on-chain transaction hashes, publish timestamps, CI/sandbox evasion hostnames, and the family assessment (PolinRider loader → DEV#POPPER RAT → probable OmniStealer). | Yes |
| Secondary | Cybersecurity News (2026-07-28) | Reproduces the full IOC set — four C2 IPs, blockchain endpoints, Tron addresses, both headline hashes, sentinel tags. Independent restatement, no new indicators. | Partial |
| Secondary | GBHackers (2026-07-28) | Confirms C2 IPs and blockchain lookup endpoints; frames the worm/self-propagation angle. No new indicators. | Partial |
| Secondary | The Hacker News (2026-07-28) | Names the DEV#POPPER family and the import-time trigger. Narrative confirmation only. | No |
Research decisions
- Blockchain endpoints are hunt-only, never block.
api.trongrid.io,fullnode.mainnet.aptoslabs.com,bsc-dataseed.binance.org,bsc-rpc.publicnode.comandip-api.comare legitimate public services. They are excluded from the Falcon import CSV and routed to a hunt query instead. Blocking them would break real web3 development and would not stop the implant, which reaches them over allowlisted HTTPS. - Two vendors disagree on scope in one place. StepSecurity lists all six 2773 prereleases as carrying the payload; Socket analysed
@joyfill/layouts@0.1.2-2773.beta.0and@joyfill/components@4.0.0-rc24-2773-beta.4in depth and notes the components Rollup bundle ships a throwing dynamic-requireshim that limits execution in that specific artifact. This pack treats all six as compromised — the safe read — and flags the components artifact as lower-yield for live-execution telemetry. - Attribution is family-level, not actor-level. Socket assesses the loader as PolinRider-family and the RAT as DEV#POPPER on code overlap, and rates the Python stealer as an OmniStealer iteration at medium likelihood. No group attribution is claimed and none is carried into this pack.
- Detonation caveat. StepSecurity's sandbox runs produced no outbound C2 callout, most plausibly because the on-chain configuration was taken down within hours of disclosure. Live C2 telemetry may therefore be absent even on genuinely infected hosts — which is exactly why the behavioral queries in section 8 outrank the IP list in section 10.
Hunt Brief & Attack Chain
Attack chain
| # | Step | Telemetry | Hunt angle |
|---|---|---|---|
| 0 | Maintainer or publishing-credential compromise. Six 2773 prereleases published 2026-07-28 by one npm identity (Node 18.20.0 / npm 10.5.0), layouts at 10:54:57Z and components at 11:03:59Z. Implant present at bundle time — it appears in the emitted source maps — but absent from the project source tree. | Registry metadata; lockfiles; package cache | Lockfile and cache sweep for any 2773 prerelease; registry-proxy audit log |
| 1 | Import-time loader. Sets global["!"] = "9-0135-3", exposes require/module as global.r/global.m via a seeded string-shuffle decoder, then a two-step Function-constructor ladder plus a dictionary decompressor produce the ~3.8 KB stage-1 script in memory only. A 30-second re-entry guard (_p_t) defeats repeat detonation and a self-integrity check silently neuters execution if the decoder is instrumented. | ProcessRollup2 (node/npm/npx trees) | Node process trees on hosts whose lockfiles reference the packages; script-content scanning of node_modules dist bundles |
| 2 | Blockchain C2 resolution. Latest outbound Tron transaction → reversed hex → BSC transaction hash → eth_getTransactionByHash → input split on ?.? → repeating-key XOR → eval. Aptos account is the fallback pointer. Runs twice: branch A evaluates in-process; branch B spawns a detached node -e child (detached: true, stdio: "ignore", windowsHide: true, then unref()). | DnsRequest; NetworkConnectIP4; ProcessRollup2 | Q2 blockchain RPC lookups from node/npm; Q3 detached node -e child carrying the campaign tag |
| 3 | Campaign-gated C2 selection. The tag _V = "A9-0135-3" selects the npm-vector infrastructure: Socket.IO endpoint 166.88.134.62:443, upload host 166.88.134.62. Alternate profiles exist for 198.105.127.210 and 23.27.202.27 (including port 27017). The detached branch requests /$/boot from 23.27.13.43 with a Sec-V: A9-0135-3 header and a hardcoded Windows Chrome user-agent, then XOR-decrypts and evaluates the response. | NetworkConnectIP4; proxy/firewall logs | Q1 direct C2 contact; proxy hunt for the Sec-V header and the /$/boot path |
| 4 | Socket.IO RAT (77,276 bytes, version 260605). Beacons identify with a client UUID, PID, OS fingerprint and the campaign tag. Verbs: ss_info, ss_ip, ss_cb (clipboard), ss_upf/ss_upd (upload to /u/f), ss_dir, ss_eval/ss_eval64, ss_inz/ss_inzx (inject), ss_connect, **0, ~node, ~py. Self-heals dependencies with npm --prefix "<tmp>" install socket.io-client. Avoids github-runner, buildbot, buildkitsandbox, sandbox-pool-, cloudchamber and microsoft-standard-WSL2 hosts. | ProcessRollup2; NetworkConnectIP4 | Q4 runtime dependency install; Q6 clipboard readers; Q7 tasklist recon; Q5 python child |
| 5 | Persistence and worm propagation. A self-reloading loader stub is injected, idempotently and behind comment sentinels, into @vscode/deviceid (VS Code / Cursor / Antigravity), the Discord desktop core module, GitHub Desktop resources/app/main.js, and the global npm CLI lib/cli.js resolved via npm root -g. Patching the npm CLI closes the loop: every later npm run re-executes the loader. | File-write / file-integrity telemetry | Q8 developer-tool file modification; native sentinel-tag grep (section 7) |
| 6 | Python credential stealer (82,457 bytes). Provisioned via /d/python.zip, /d/7zr.exe, /d/python.7z, payload fetched from /$/{id} with the same Sec-V header. Collects Chromium and Firefox data, browser-extension wallet and password-manager storage, Git credentials, GitHub CLI config, VS Code storage, GitHub Desktop logs, Windows Credential Manager, macOS Keychain, Linux Secret Service / KWallet. Stages under %USERPROFILE%\.npm or /tmp/.npm, packs an AES ZIP with pyzipper, registers at /u/e and uploads to /u/f. Optional Telegram exfil channel. | ProcessRollup2; ZipFileWritten | Q5 node → python; Q11 archive staged under .npm |
Hunt hypotheses (ordered by fidelity)
| # | Hypothesis | ATT&CK | Falcon events | Expected FP sources | Conf |
|---|---|---|---|---|---|
| H1 | An endpoint contacted one of the four campaign C2 addresses. | T1071.001, T1105 | NetworkConnectIP4 | None credible — these are dedicated attacker hosts. | High |
| H2 | A Node process spawned a detached -e child whose inline script carries the campaign tag. | T1059.007, T1027 | ProcessRollup2 | Rare. Some CLI tools use node -e, but not with an inline global['_V'] assignment. | High |
| H3 | A Node process performed a runtime npm install of socket.io-client/axios into a scratch prefix. | T1105 | ProcessRollup2 | Package scaffolders and some test harnesses install at runtime. | High |
| H4 | A Node process read the clipboard via the platform helper. | T1115 | ProcessRollup2 | Electron/CLI tools that offer "copy to clipboard" usually write, not read. | High |
| H5 | A Node process enumerated running processes with tasklist. | T1057, T1082 | ProcessRollup2 | A few port-checking and process-manager npm modules. | High |
| H6 | Developer-tool JavaScript (npm CLI, GitHub Desktop, Discord core, @vscode/deviceid) was modified outside a vendor update. | T1554 | CriticalFileModified | Genuine application self-updates; npm install -g npm. | Medium |
| H7 | A developer workstation or build agent resolved blockchain RPC endpoints from a Node process. | T1102.001 | DnsRequest | Real web3 development; crypto wallet extensions. | Medium |
| H8 | A Node process spawned a Python interpreter (stealer staging). | T1059.006 | ProcessRollup2 | node-gyp and native-module builds legitimately call Python. | Medium |
| H9 | An encrypted archive was staged under a .npm directory. | T1560.001, T1005 | ZipFileWritten | The npm cache lives there but does not contain .zip/.7z files. | Medium |
| H10 | A package manager was invoked against a Joyfill 2773 prerelease. | T1195.002 | ProcessRollup2 | Deliberate reproduction or research activity. | Medium |
Known gap carried into the coverage map: stage 0 (how the publishing credential was compromised) has no endpoint telemetry — it happened at the registry. Stage 1's in-memory decoding likewise leaves no artifact on disk. Both are covered only by lockfile hygiene and registry-side controls, not by CQL.
Consolidated IOC Table
| Type | Value | Conf | Action | Context | Expiry |
|---|---|---|---|---|---|
| package | @joyfill/components 4.0.0-rc24-2773-beta.4 / .5 / .6 | High | detect | Payload appended to dist/index.js, dist/index.esm.js, dist/joyfill.min.js | 2027-02-02 |
| package | @joyfill/layouts 0.1.2-2773.beta.0 / .1 / .2 | High | detect | Payload spliced into dist/index.cjs.js and dist/index.es.js | 2027-02-02 |
| ipv4 | 166.88.134.62 (443 socket, 80 upload) | High | detect | Primary C2 for the npm campaign tag A9-0135-3 | 2027-02-02 |
| ipv4 | 23.27.13.43 | High | detect | Serves the detached branch's /$/boot downloader | 2027-02-02 |
| ipv4 | 198.105.127.210 | High | detect | Alternate campaign profile (numeric tags) | 2027-02-02 |
| ipv4 | 23.27.202.27 (443, 27017) | High | detect | Alternate campaign profile | 2027-02-02 |
| sha256 | 26351aed0397158d3a3b8cc8fd3047d4c015d264c9895f10f20f1521b974ed18 | High | detect | Final Socket.IO RAT clientCode payload (77,276 bytes) | 2027-02-02 |
| sha256 | 36ff00b45e67baa7e3674b0c80f48e88737264c61e5c6b3b091200972de8157c | High | detect | Python credential stealer (82,457 bytes) | 2027-02-02 |
| sha256 | cb46f12d70824ea24ed1f8bcf45bf3f86680e02a9089aafc03b27f691be57be3 | High | hunt | In-process first-stage loader (5,849 bytes) | 2026-11-02 |
| sha256 | 78f0de8682e0e894a5784eb7e95db4da6088f528918ca3107dd1e76f80a561d8 | High | hunt | Detached second-stage bootstrap (3,525 bytes) | 2026-11-02 |
| sha256 | adc4af90540d33cd1e98f44b51482ae9250fbeb97d6f8d7841c81b618cb2c6e6 | High | detect | @joyfill/layouts malicious tarball | 2027-02-02 |
| sha256 | bcc93dc55bc7daedf4ca57254f0e7a7f1c40e09851eab98fe10cde801982db17 | High | detect | @joyfill/components malicious tarball | 2027-02-02 |
| url-path | /$/boot · /u/e · /u/f · /0x/js · /verify-human/ · /snv · /$/{id} · /d/python.zip · /d/7zr.exe · /d/python.7z | High | hunt | C2 request paths — pivot on proxy and web-gateway logs | 2026-11-02 |
| http-header | Sec-V: A9-0135-3 | High | hunt | Campaign tag phoned home on the /$/boot request | 2026-11-02 |
| marker | C250617A · C250618A · C250619A · C250620A · C260511A · C260512A · RS260605 | High | hunt | Comment sentinels wrapping the injected loader stub in developer-tool files | 2027-02-02 |
| marker | 9-0135-3 · A9-0135-3 · _V · _p_t · _t_s · _t_u | High | hunt | Campaign fragment and global variable names planted by the loader | 2027-02-02 |
| tron-address | TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP | High | pivot | Branch A pointer — latest outbound tx carries the BSC hash | 2026-09-02 |
| tron-address | TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG | High | pivot | Branch B pointer (no outbound txs — Aptos fallback fires) | 2026-09-02 |
| tron-address | TA48dct6rFW8BXsiLAtjFaVFoSuryMjD3v | High | pivot | Tier-two pointer set by stage 2 | 2026-09-02 |
| domain | api.trongrid.io · fullnode.mainnet.aptoslabs.com · bsc-dataseed.binance.org · bsc-rpc.publicnode.com · ip-api.com | Medium | hunt | Legitimate public services — do not block. Hunt only, scoped to node/npm as the requesting process | 2026-11-02 |
| xor-key | 2[gWfGj;<:-93Z^C · m6:tTh^D)cBz?NM] · ThZG+0jfXE6VAGOJ | High | enrich | Branch A / branch B / /$/boot response keys — use for sample triage | 2026-09-02 |
| path | %USERPROFILE%\.npm · /tmp/.npm | High | hunt | Credential staging directory for the encrypted exfil archive | 2027-02-02 |
Affected Surface & Telemetry Matrix
| Surface | Required telemetry | Priority | Gap risk |
|---|---|---|---|
| Developer workstations (Windows / macOS / Linux) — the actual target | ProcessRollup2, SyntheticProcessRollup2, NetworkConnectIP4, DnsRequest | Critical | Low — standard Falcon sensor coverage |
| Developer-tool application files (npm CLI, VS Code / Cursor / Antigravity, Discord, GitHub Desktop) | CriticalFileModified or equivalent file-integrity telemetry | Critical | High — write visibility for non-PE script files is sensor-policy dependent; validate before relying on Q8 |
| CI/CD build agents and ephemeral runners | ProcessRollup2, NetworkConnectIP4, egress/proxy logs | High | Medium — ephemeral runners often have no sensor; egress logs may be the only source. The RAT also skips several runner hostnames by design |
| Egress path (proxy / firewall / web gateway) | Destination IP, port, URL path, request headers | High | Medium — the Sec-V header and /$/boot path need header/URL logging, not just IP |
| Source repositories and lockfiles | SCM search / dependency inventory | High | Low — a simple grep suffices |
| Internal registry proxy / mirror / build image cache | Registry audit logs; cached tarball inventory | Medium | Medium — cached tarballs can re-serve a removed version |
| Credential stores reachable from the workstation (browser vaults, wallet extensions, Git/GitHub, cloud keys, OS keychains) | IdP sign-in logs, token-use audit, VCS audit logs | Critical | High — theft is silent; only post-theft token use is observable |
| Server-side rendering and production deploy paths that import the package | ProcessRollup2, NetworkConnectIP4 on app servers | Medium | Medium — containerised runtimes may lack sensor coverage |
ATT&CK Mapping
| Tactic | Technique | Name | Observed behavior | Query / control |
|---|---|---|---|---|
| Initial Access | T1195.002 | Compromise Software Supply Chain | Malicious 2773 prereleases published to npm with the implant welded into dist bundles | Q10 · registry denylist |
| Execution | T1059.007 | Command and Scripting Interpreter: JavaScript | Loader executes on module import; Function-constructor ladder; eval of decrypted stages | Q3, Q4 |
| Execution | T1059.006 | Command and Scripting Interpreter: Python | ~py verb spawns a detached Python interpreter for the stealer | Q5 |
| Execution | T1059.001 | Command and Scripting Interpreter: PowerShell | powershell -NoProfile -Command "Get-Clipboard" for clipboard theft | Q6 |
| Execution | T1059.004 | Command and Scripting Interpreter: Unix Shell | pbpaste, xclip, xsel on macOS/Linux; shell execution verbs | Q6 |
| Persistence | T1554 | Compromise Host Software Binary | Loader stub injected into npm CLI, GitHub Desktop, Discord core, @vscode/deviceid | Q8 · native sentinel grep |
| Defense Evasion | T1027 | Obfuscated Files or Information | Seeded string shuffle, dictionary decompression, LZ-String table of 337 entries, control-flow flattening | Behavioral queries; script scanning |
| Defense Evasion | T1027.013 | Encrypted/Encoded File | Repeating-key XOR on every retrieved stage; AES ZIP for the exfil archive | Q11 |
| Defense Evasion | T1497.001 | Virtualization/Sandbox Evasion: System Checks | Skips github-runner, buildbot, buildkitsandbox, sandbox-pool-, cloudchamber, WSL2; 30-second re-entry guard; decoder self-integrity check | Workstation-first hunting |
| Discovery | T1082 | System Information Discovery | ss_info reports OS, hostname, user, Node path and version, startup path | Q7 |
| Discovery | T1057 | Process Discovery | tasklist /FO CSV /NH run hidden on Windows | Q7 |
| Discovery | T1614 | System Location Discovery | ss_ip geolocates the host through ip-api.com | Q2 |
| Collection | T1115 | Clipboard Data | ss_cb reads the clipboard per platform | Q6 |
| Collection | T1005 | Data from Local System | ss_upf/ss_upd upload files and whole directories | Q11 |
| Collection | T1560.001 | Archive via Utility | pyzipper AES ZIP staged under .npm | Q11 |
| Credential Access | T1555.003 | Credentials from Web Browsers | Chromium and Firefox stores decrypted via DPAPI, macOS Keychain, Secret Service / KWallet | Q5 · credential rotation |
| Credential Access | T1552.001 | Unsecured Credentials: Credentials In Files | Git credentials, GitHub CLI config, VS Code storage, GitHub Desktop logs, .npmrc tokens | Hardening §12 |
| Command and Control | T1102.001 | Web Service: Dead Drop Resolver | Tron → BSC → Aptos on-chain transactions carry the encrypted next stage | Q2 |
| Command and Control | T1071.001 | Application Layer Protocol: Web Protocols | Socket.IO over plain HTTP to 166.88.134.62:443; /$/boot GET with the Sec-V header | Q1 |
| Command and Control | T1105 | Ingress Tool Transfer | Runtime npm install of axios/socket.io-client; Python runtime provisioned from /d/ paths | Q4, Q9 |
| Exfiltration | T1041 | Exfiltration Over C2 Channel | Multipart POST of the encrypted archive to /u/f after registering metadata at /u/e | Q1, Q11 |
| Exfiltration | T1567 | Exfiltration Over Web Service | Optional Telegram document channel when C2 supplies a bot token and chat ID | Egress hunting |
Native Audit-Log Hunts (non-CQL)
Run these first — they are cheap, need no EDR, and answer "am I affected" faster than any query. All are read-only inventory checks. Nothing here installs, downloads, or executes any part of the malicious package.
1 · Lockfile and dependency-tree sweep (all repos, all build images)
grep -rEn 'joyfill.*2773' package-lock.json yarn.lock pnpm-lock.yaml # recursive across a repo tree grep -rEn --include='*lock*' 'joyfill.*2773' . # resolved tree, per project npm ls @joyfill/components @joyfill/layouts --all 2>/dev/null
2 · Injection-sentinel scan on developer workstations
The loader stub is wrapped in fixed comment sentinels. Scanning for those tags is the single highest-yield workstation check, and it is what tells you the machine is persistently infected rather than merely exposed.
# macOS / Linux grep -rIl -E 'C25061[789]A|C250620A|C26051[12]A|RS260605|9-0135-3' \ ~/.npm-global ~/.nvm /usr/local/lib/node_modules /usr/lib/node_modules \ "/Applications/Visual Studio Code.app/Contents/Resources/app/node_modules/@vscode/deviceid" \ "/Applications/Discord.app/Contents/Resources" \ "/Applications/GitHub Desktop.app/Contents/Resources/app" 2>/dev/null # resolve the global npm CLI first, then inspect only that file npm root -g grep -c -E 'C25061[789]A|RS260605' "$(npm root -g)/npm/lib/cli.js"
# Windows (PowerShell) $paths = @( "$env:APPDATA\npm\node_modules\npm\lib\cli.js", "$env:LOCALAPPDATA\Programs\Microsoft VS Code\resources\app\node_modules\@vscode\deviceid", "$env:LOCALAPPDATA\Discord", "$env:LOCALAPPDATA\GitHubDesktop" ) Select-String -Path $paths -Include *.js -Recurse -ErrorAction SilentlyContinue ` -Pattern 'C25061[789]A|C250620A|C26051[12]A|RS260605|9-0135-3' | Select-Object Path, LineNumber | Format-Table -AutoSize
3 · Credential staging directory check
# the npm cache lives here legitimately, but it holds no archives ls -la ~/.npm/*.zip ~/.npm/*.7z /tmp/.npm 2>/dev/null # Windows Get-ChildItem "$env:USERPROFILE\.npm" -Include *.zip,*.7z -Recurse -ErrorAction SilentlyContinue
4 · Egress / proxy log hunts
- Any request carrying the header
Sec-V:— this header is not part of any standard and is a single-source signal for this family. - Requests to the paths
/$/boot,/u/e,/u/f,/0x/js,/verify-human/,/snv,/d/python.zip,/d/7zr.exe,/d/python.7zagainst any destination, not just the known IPs — the operator rotates hosts on-chain. - Plain-HTTP traffic to port 443 (the malware writes
http://166.88.134.62:443) — a protocol/port mismatch that most gateways can flag generically. - Outbound to port 27017 from a workstation or build agent.
- Blockchain RPC lookups from CI runners: per the Socket analysis, blockchain RPC traffic from a build agent is a high-fidelity signal on its own, because build agents have no legitimate reason to talk to Tron, Aptos or BSC.
5 · Registry and identity-side checks
- Internal registry proxy / mirror audit log: did any client fetch a
2773prerelease tarball, and is a copy still cached? - npm audit log for tokens belonging to users on affected machines — publishes, token creations, 2FA changes since 2026-07-28.
- GitHub/GitLab audit log: new PATs, new SSH keys, new OAuth grants, or pushes from unfamiliar IPs by users on affected machines.
- IdP sign-in logs for the affected users: impossible-travel or new-device sign-ins using browser-stored session cookies.
CrowdStrike LogScale CQL Hunt Queries
No query below carries an in-query time filter. Set the lookback with the console time picker; each query records its intended window as a // LOOKBACK: comment. Start at 30 days — the packages were published 2026-07-28.
Looks for: any endpoint process connecting to one of the four dedicated campaign C2 addresses. This is the single unambiguous indicator in the pack — these are not shared hosting or CDN addresses. Project ContextBaseFileName to confirm the initiating process was node. Note: the operator rotates C2 on-chain, so a clean result does not clear a host — pair with Q3 and Q8.
// HUNT: Joyfill / DEV#POPPER campaign C2 contact from any process // MITRE: T1071.001, T1105, T1041 | CONF: high FP: low COST: low | REQUIRES: NetworkConnectIP4 // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^NetworkConnectIP4$/ | in(RemoteAddressIP4, values=["166.88.134.62", "23.27.13.43", "198.105.127.210", "23.27.202.27"]) | table([@timestamp, aid, ComputerName, UserName, ContextBaseFileName, ContextProcessId, RemoteAddressIP4, RemotePort, LocalAddressIP4])
Looks for: the stage-1 dead-drop resolution step — node/npm/npx/yarn/pnpm resolving Tron, Aptos or BSC RPC endpoints, plus the ip-api.com geolocation lookup used by the ss_ip verb. These are legitimate public services, so this is a hunt, never a block. FP: genuine web3 engineering, blockchain SDK test suites, and crypto wallet browser extensions all reach these hosts — the discriminator is the requesting process and whether the host has ever done this before.
// HUNT: dead-drop C2 resolution - blockchain RPC lookups from node/npm // MITRE: T1102.001, T1614 | CONF: medium FP: medium COST: low | REQUIRES: DnsRequest // LOOKBACK: 30d (set in the console time picker) // FALSE POSITIVES: real web3 development, blockchain SDK tests, wallet browser extensions. // TUNING: exclude the asset group that legitimately does web3 work, then alert only on hosts // outside it. On a build agent this needs no tuning at all - a CI runner has no reason to // resolve Tron/Aptos/BSC, so scope this query to the CI asset group for a high-fidelity variant. #event_simpleName=/^DnsRequest$/ | DomainName=/^(api\.trongrid\.io|fullnode\.mainnet\.aptoslabs\.com|bsc-dataseed\.binance\.org|bsc-rpc\.publicnode\.com|ip-api\.com)$/i | ContextBaseFileName=/^(node|npm|npx|yarn|pnpm)(\.exe|\.cmd)?$/i | groupBy([aid, ComputerName, ContextBaseFileName, DomainName], function=count(as=Lookups)) | sort(Lookups, order=desc)
node -e child carrying the campaign tag
Looks for: loader branch B — child_process.spawn("node", ["-e", "global['_V']='A9-0135-3';<payload>"]) with detached: true, stdio: "ignore" and windowsHide: true. The inline global['_V'] assignment and the literal campaign tag are both distinctive. This child survives the parent build or test run, so it can appear long after the import that spawned it.
// HUNT: detached "node -e" child carrying the A9-0135-3 campaign tag (loader branch B) // MITRE: T1059.007, T1027 | CONF: high FP: low COST: low | REQUIRES: ProcessRollup2 // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | FileName=/^node(\.exe)?$/i | CommandLine=/\s-e\s/ | CommandLine=/global\[|_V|A9-0135-3|_p_t/i | table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, GrandparentBaseFileName, FileName, CommandLine, TargetProcessId, ParentProcessId])
npm --prefix install of socket.io-client / axios)
Looks for: the RAT installing its own missing dependencies at runtime into a scratch prefix — npm --prefix "<tmp>" install socket.io-client (also axios and form-data). The combination of --prefix pointing outside a project and these three package names is the discriminator. FP: some scaffolding tools and integration-test harnesses install at runtime; they normally target the project directory, not a temp path.
// HUNT: RAT dependency self-healing - runtime npm install into a scratch prefix // MITRE: T1105, T1059.007 | CONF: high FP: medium COST: low | REQUIRES: ProcessRollup2 // LOOKBACK: 30d (set in the console time picker) // FALSE POSITIVES: project scaffolders and e2e test harnesses that install packages at runtime. // TUNING: exclude command lines whose --prefix target sits inside a known repo root, and exclude // your CI image build steps by ParentBaseFileName. What remains - an install into a temp or // profile-local path, parented by node rather than a shell - is the signal. #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | CommandLine=/\bnpm\b/i | CommandLine=/\binstall\b|\bi\s/i | CommandLine=/--prefix/i | CommandLine=/socket\.io-client|form-data|\baxios\b/i | table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine, TargetProcessId])
Looks for: the ~py verb — the RAT spawning a detached Python process, which is how the credential stealer is staged. On a developer workstation with no native-module build activity this is a strong signal. FP: node-gyp, prebuild, and native modules such as sharp or canvas legitimately call Python during install; those cluster tightly on a handful of repos and build steps.
// HUNT: node spawning python - DEV#POPPER "~py" verb staging the credential stealer // MITRE: T1059.006, T1555.003 | CONF: medium FP: medium COST: low | REQUIRES: ProcessRollup2 // LOOKBACK: 30d (set in the console time picker) // FALSE POSITIVES: node-gyp and native-module builds (sharp, canvas, prebuild) call python legitimately. // TUNING: exclude ParentCommandLine matching node-gyp, gyp, prebuild, node-pre-gyp and your own // build wrappers. Prioritise rows where GrandparentBaseFileName is node - i.e. the parent node was // itself spawned by another node process, which is the detached "-e" child shape from Q3. #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | ParentBaseFileName=/^node(\.exe)?$/i | FileName=/^(python3?|pythonw|py)(\.exe)?$/i | table([@timestamp, aid, ComputerName, UserName, GrandparentBaseFileName, ParentBaseFileName, FileName, CommandLine, TargetProcessId, ParentProcessId])
ss_cb)
Looks for: the clipboard-theft verb, which shells out per platform: powershell -NoProfile -Command "Get-Clipboard" on Windows, pbpaste on macOS, xclip -selection clipboard -o or xsel --clipboard --output on Linux. Developer tooling overwhelmingly writes the clipboard rather than reading it, which makes a Node-parented clipboard read unusual on its own.
// HUNT: clipboard theft - node-parented clipboard READ (ss_cb verb) // MITRE: T1115, T1059.001, T1059.004 | CONF: high FP: low COST: low | REQUIRES: ProcessRollup2 // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | ParentBaseFileName=/^(node|npm|npx)(\.exe|\.cmd)?$/i | FileName=/^(powershell|pwsh|pbpaste|xclip|xsel)(\.exe)?$/i | CommandLine=/Get-Clipboard|pbpaste|selection\s+clipboard|--clipboard|--output/i | table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine, TargetProcessId, ParentProcessId])
tasklist, ss_info)
Looks for: the RAT's Windows reconnaissance step — tasklist /FO CSV /NH run hidden — plus the equivalent enumeration helpers on other platforms. A Node process enumerating running processes is rare outside of a handful of port-checking modules, and those run interactively rather than from a detached child.
// HUNT: node-parented host/process reconnaissance (ss_info fingerprinting) // MITRE: T1057, T1082 | CONF: high FP: low COST: low | REQUIRES: ProcessRollup2 // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | ParentBaseFileName=/^(node|npm|npx)(\.exe|\.cmd)?$/i | FileName=/^(tasklist|systeminfo|whoami|hostname|wmic)(\.exe)?$/i | table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine, TargetProcessId, ParentProcessId])
Looks for: writes to the four files the RAT injects its self-reloading stub into — the @vscode/deviceid module (VS Code, Cursor, Antigravity), the Discord desktop core module, GitHub Desktop resources/app/main.js, and the global npm CLI lib/cli.js. Patching the npm CLI is the worm loop, so this is the persistence query that matters most. FP: the applications' own updaters rewrite these files during a version bump — exclude the vendor updater as the writing process first. ⚠ Validate in tenant — write visibility for non-PE script files depends on sensor file-monitoring policy; confirm this event fires for a test write before relying on the query, and fall back to the section 7 sentinel grep if it does not.
// HUNT: worm re-injection - modification of npm CLI / GitHub Desktop / Discord core / @vscode/deviceid // MITRE: T1554 | CONF: medium FP: medium COST: medium | REQUIRES: CriticalFileModified // (script-file write visibility is sensor-policy dependent - validate in tenant before relying on this) // LOOKBACK: 30d (set in the console time picker) // FALSE POSITIVES: vendor self-updates (VS Code, Cursor, Discord, GitHub Desktop) and "npm install -g npm". // TUNING: exclude ContextBaseFileName matching the vendor updaters - Code, Cursor, Update, Squirrel, // GitHubDesktop, Discord - and exclude node when its command line is a deliberate global npm upgrade. // Anything left, especially a write by a node process with no interactive parent, is the signal. #event_simpleName=/^CriticalFileModified$/ | TargetFileName=/(deviceid.{0,60}index\.js|discord_desktop_core.{0,60}index\.js|resources.{0,20}app.{0,20}main\.js|npm.{0,20}lib.{0,10}cli\.js)$/i | table([@timestamp, aid, ComputerName, UserName, ContextBaseFileName, ContextProcessId, TargetFileName])
Looks for: any event carrying one of the recovered payload or tarball hashes. Confidence is rated medium rather than high on purpose: every artifact here is a script or archive, not a PE, so Falcon's process-hash telemetry will not routinely surface them. Treat this as a free cross-check and put the same hashes into Falcon IOC Management (section 10), where file-scanning coverage is better.
// HUNT: recovered Joyfill / DEV#POPPER payload and tarball hashes // MITRE: T1195.002, T1105 | CONF: medium FP: low COST: low // REQUIRES: ProcessRollup2, PeFileWritten, NewExecutableWritten, ZipFileWritten // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2|PeFileWritten|NewExecutableWritten|ZipFileWritten)$/ | in(SHA256HashData, values=[ "26351aed0397158d3a3b8cc8fd3047d4c015d264c9895f10f20f1521b974ed18", "36ff00b45e67baa7e3674b0c80f48e88737264c61e5c6b3b091200972de8157c", "cb46f12d70824ea24ed1f8bcf45bf3f86680e02a9089aafc03b27f691be57be3", "78f0de8682e0e894a5784eb7e95db4da6088f528918ca3107dd1e76f80a561d8", "adc4af90540d33cd1e98f44b51482ae9250fbeb97d6f8d7841c81b618cb2c6e6", "bcc93dc55bc7daedf4ca57254f0e7a7f1c40e09851eab98fe10cde801982db17"]) | table([@timestamp, aid, ComputerName, UserName, FileName, ImageFileName, TargetFileName, SHA256HashData, CommandLine])
Looks for: the exposure moment itself — a package manager command line naming a Joyfill package and the 2773 build marker. This finds explicit installs; it will not find a transitive resolve from a lockfile, so run the section 7 lockfile grep alongside it.
// HUNT: package manager invoked against a compromised Joyfill 2773 prerelease // MITRE: T1195.002 | CONF: medium FP: low COST: low | REQUIRES: ProcessRollup2 // LOOKBACK: 30d (set in the console time picker) #event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/ | FileName=/^(npm|npx|yarn|pnpm|node)(\.exe|\.cmd)?$/i | CommandLine=/joyfill/i | CommandLine=/2773/ | table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine, TargetProcessId])
.npm
Looks for: the exfiltration packaging step — the Python stealer stages collected credentials under %USERPROFILE%\.npm or /tmp/.npm and writes an AES-encrypted ZIP with pyzipper before POSTing it to /u/f. The .npm directory legitimately holds the npm cache, but that cache contains _cacache content-addressed blobs and _logs, never archives. FP: a developer manually zipping something into their home cache directory, and tooling that snapshots the npm cache.
// HUNT: credential exfil staging - encrypted archive written under a .npm directory // MITRE: T1560.001, T1005, T1041 | CONF: medium FP: medium COST: low | REQUIRES: ZipFileWritten // LOOKBACK: 30d (set in the console time picker) // FALSE POSITIVES: manual archiving in the home directory; cache-snapshot tooling. // TUNING: exclude TargetFileName containing _cacache or _logs (the legitimate npm cache layout), and // exclude your backup or cache-warming agents by ContextBaseFileName. A python or node process // writing an archive there is the signal - the npm cache is never a .zip or .7z. #event_simpleName=/^ZipFileWritten$/ | TargetFileName=/\.npm.{0,120}\.(zip|7z)$/i | ContextBaseFileName=/^(python3?|pythonw|py|node)(\.exe)?$/i | table([@timestamp, aid, ComputerName, UserName, ContextBaseFileName, ContextProcessId, TargetFileName, FileSize])
CrowdStrike Custom IOA Recommendations
Three queries are strong enough to promote to Custom IOAs. The rest stay Investigate-only hunts. Deploy every IOA in Detect first on a pilot host group, review a full week of hits, then move to Prevent only where noted.
IOA-1 — Detached node -e with an inline campaign tag Promote
- Rule group: Process Creation (Windows / Mac / Linux).
- Image filename:
.*\\node\.exe(Windows) /.*/node(Mac, Linux). - Command line:
.*\s-e\s.*global\[.*_V.* - Action: Detect (Critical). Move to Prevent once a clean week is confirmed — the pattern has no known benign use.
- Exclusions: none expected. If a homegrown tool matches, exclude by parent image path rather than loosening the command-line pattern.
- Deployment path: Endpoint Security → Custom IOA Rule Groups → new Process Creation rule → assign to a Prevention Policy → pilot host group first.
IOA-2 — Node-parented clipboard read Promote
- Rule group: Process Creation, all platforms.
- Parent image filename:
.*(\\|/)(node|npm|npx)(\.exe|\.cmd)? - Image filename:
.*(\\|/)(powershell\.exe|pwsh|pbpaste|xclip|xsel) - Command line:
.*(Get-Clipboard|pbpaste|selection\s+clipboard|--clipboard).* - Action: Detect (High). Keep at Detect — a rare Electron helper could legitimately read the clipboard.
- Exclusions: exclude any approved internal CLI that offers a "paste from clipboard" feature, by parent image path.
IOA-3 — Runtime npm --prefix install of RAT dependencies Promote with tuning
- Rule group: Process Creation, all platforms.
- Command line:
.*npm.*--prefix.*(socket\.io-client|axios|form-data).* - Action: Detect (High). Do not set Prevent — blocking an
npm installon a developer machine causes real disruption. - Exclusions: exclude prefixes inside known repository roots and your CI image build steps before enabling.
Investigate-only (do not promote)
- Q2 — blockchain RPC lookups: too environment-dependent for an IOA. Run as a Scheduled Search scoped to the CI asset group, where it is genuinely high fidelity.
- Q5 — node → python:
node-gypmakes this too noisy for a prevention rule. Scheduled Search with the node-gyp exclusion applied. - Q8 — developer-tool file modification: depends on file-monitoring policy and on excluding vendor updaters; keep as a weekly Scheduled Search until the FP profile is measured.
- Q1 / Q9 — atomic indicators: these belong in Falcon IOC Management (section 10), not in a Custom IOA. IOC Management gives you retroactive matching and expiry handling that an IOA does not.
Scheduled Search recommendations
| Search name | Source | Cadence | Notify |
|---|---|---|---|
| DETECT — Detached node -e with campaign tag (npm supply chain) | Q3 | Hourly | SOC on-call, Critical |
| DETECT — Node-parented clipboard read | Q6 | Hourly | SOC on-call, High |
| DETECT — Campaign C2 contact (npm supply chain) | Q1 | Every 15 minutes | SOC on-call, Critical |
| HUNT — Blockchain RPC from build agents | Q2 (CI scope) | Daily | Detection engineering |
| HUNT — Developer-tool JavaScript modified | Q8 | Weekly | Detection engineering |
| HUNT — Joyfill 2773 prerelease invoked | Q10 | Daily | AppSec / platform |
Machine-Readable IOC Appendix
Four copy-ready blocks. The CSV is the only block intended for bulk import into Falcon IOC Management; everything else is hunt, pivot, or audit material. Blockchain RPC endpoints are deliberately excluded from the CSV — they are legitimate public services and blocking them breaks real development without stopping the implant.
type,value,action,severity,expiration,description,tags ipv4,166.88.134.62,detect,critical,2027-02-02,Socket.IO C2 and upload host for campaign A9-0135-3,campaign:Joyfill-npm ipv4,23.27.13.43,detect,critical,2027-02-02,Serves the detached branch /$/boot downloader,campaign:Joyfill-npm ipv4,198.105.127.210,detect,high,2027-02-02,Alternate campaign C2 profile,campaign:Joyfill-npm ipv4,23.27.202.27,detect,high,2027-02-02,Alternate campaign C2 profile including port 27017,campaign:Joyfill-npm sha256,26351aed0397158d3a3b8cc8fd3047d4c015d264c9895f10f20f1521b974ed18,prevent,critical,2027-02-02,Final Socket.IO RAT clientCode payload,campaign:Joyfill-npm sha256,36ff00b45e67baa7e3674b0c80f48e88737264c61e5c6b3b091200972de8157c,prevent,critical,2027-02-02,Python credential stealer,campaign:Joyfill-npm sha256,cb46f12d70824ea24ed1f8bcf45bf3f86680e02a9089aafc03b27f691be57be3,detect,high,2027-02-02,In-process first-stage loader,campaign:Joyfill-npm sha256,78f0de8682e0e894a5784eb7e95db4da6088f528918ca3107dd1e76f80a561d8,detect,high,2027-02-02,Detached second-stage bootstrap,campaign:Joyfill-npm sha256,f452f9cfa539f4a1fe25187a99a484391290d5dbaa422ba455edf6b04f81b7d1,detect,medium,2027-02-02,Decoded tier-two blockchain resolver,campaign:Joyfill-npm sha256,ae7565109fd01b88d82acf7f73ab20709cbc2c9f26fdea13e429ccc87a55d4fb,detect,medium,2027-02-02,Decoded detached bootstrap,campaign:Joyfill-npm sha256,26e679eaf1e9baeb7c55eb48db482301171d4d26e1728544b23734a90dc70e1b,detect,medium,2027-02-02,Preserved boot response capture,campaign:Joyfill-npm sha256,2cfede38fb121a71a2f3607474aa8cd588a99f51b37e5e6f0d8cb789fa275032,detect,medium,2027-02-02,Preserved boot response capture,campaign:Joyfill-npm sha256,adc4af90540d33cd1e98f44b51482ae9250fbeb97d6f8d7841c81b618cb2c6e6,detect,high,2027-02-02,joyfill layouts malicious tarball,campaign:Joyfill-npm sha256,8e8b90dedd456ded0c5748119836e1ca1066112bc569c1b41ca70eb931d1d4dc,detect,high,2027-02-02,joyfill layouts CommonJS bundle,campaign:Joyfill-npm sha256,5f6a92006ca2ea4b464d66fb41af777edce7296939a7c6ee491e2b3cbfe09848,detect,high,2027-02-02,joyfill layouts ESM bundle,campaign:Joyfill-npm sha256,bcc93dc55bc7daedf4ca57254f0e7a7f1c40e09851eab98fe10cde801982db17,detect,high,2027-02-02,joyfill components malicious tarball,campaign:Joyfill-npm sha256,1352ad22c99983d91e600348b7cbf58235131b1ee34cea9f09623206d5b7dea7,detect,high,2027-02-02,joyfill components dist index js,campaign:Joyfill-npm sha256,67c6ef602cc850f10d935fee53fa40440df841adf081563bf4fc2631a71249ce,detect,high,2027-02-02,joyfill components dist index esm js,campaign:Joyfill-npm sha256,c5742ea1875ecd2360022624149994909cd0546e221e4203dffd01f48de45469,detect,high,2027-02-02,joyfill components dist joyfill min js,campaign:Joyfill-npm # NOT FOR BLOCKING - legitimate public services used as the dead-drop channel. # Hunt these with Q2 scoped to node/npm; never add them to a blocklist: # api.trongrid.io fullnode.mainnet.aptoslabs.com bsc-dataseed.binance.org # bsc-rpc.publicnode.com ip-api.com
# Behavioral signatures - portable across EDR platforms.
# These survive C2 rotation; the IP list does not.
BEH-01 node spawns a detached "-e" child whose inline script assigns global['_V']
BEH-02 node/npm resolves Tron, Aptos or BSC RPC hosts on a machine that never had before
BEH-03 runtime "npm --prefix <temp> install socket.io-client|axios|form-data"
BEH-04 node parents a clipboard READ (Get-Clipboard | pbpaste | xclip -o | xsel --output)
BEH-05 node parents tasklist / systeminfo / whoami on a developer workstation
BEH-06 node parents python3 outside a node-gyp or native-module build
BEH-07 npm global CLI lib/cli.js modified by anything other than an npm self-upgrade
BEH-08 GitHub Desktop main.js, Discord core index.js or @vscode/deviceid index.js modified
outside a signed vendor update
BEH-09 .zip or .7z archive written under ~/.npm or /tmp/.npm
BEH-10 outbound HTTP (not TLS) to port 443, or any traffic to port 27017, from a workstation
BEH-11 any outbound request carrying an HTTP header named "Sec-V"
BEH-12 requests to paths /$/boot /u/e /u/f /0x/js /verify-human/ /snv against ANY host
# Malware families (family-level assessment, no actor attribution claimed) loader PolinRider-family multi-chain blockchain loader rat DEV#POPPER Socket.IO RAT, VERSION 260605, 77276 bytes stealer OmniStealer iteration (assessed medium likelihood), 82457 bytes # Campaign markers campaign_fragment 9-0135-3 campaign_tag A9-0135-3 http_header Sec-V: A9-0135-3 globals _V _p_t _t_s _t_u _t_1 _t_2 _t_c global.r global.m loader_seed rmcej%otb% injection_tags C250617A C250618A C250619A C250620A C260511A C260512A RS260605 archive_password ,./,./,./ # XOR keys (for sample triage, not for blocking) branch_A 2[gWfGj;<:-93Z^C branch_B m6:tTh^D)cBz?NM] boot_resp ThZG+0jfXE6VAGOJ # Dead-drop pointers - Tron addresses TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP branch A pointer TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG branch B pointer TA48dct6rFW8BXsiLAtjFaVFoSuryMjD3v tier-two pointer # Aptos fallback accounts 0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e 0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3 0x533b2dbcaeff19cd1f799234a27b578d713d8fcaa341b7501e4526106483e0b1 # BNB Smart Chain payload transactions 0x18a8420f727f2405f9d1805ad887b31029b584b2ff5a7ec0f57c72635183e99d 0x7ffb4efddd96e20aec90724be2ac9a71c138a9af697b9fb8224bbf80ea4f22be 0xb6c725890be6890fd2c735eedc47e24b85a350301f6c19a3864e43c35e470968 # Sandbox / CI hostnames the RAT refuses to run on (why workstations matter most) github-runner buildbot buildkitsandbox sandbox-pool- cloudchamber microsoft-standard-WSL2 root
# 1. Compromised versions - deny in registry proxy AND remove from lockfiles @joyfill/components@4.0.0-rc24-2773-beta.4 @joyfill/components@4.0.0-rc24-2773-beta.5 @joyfill/components@4.0.0-rc24-2773-beta.6 @joyfill/layouts@0.1.2-2773.beta.0 @joyfill/layouts@0.1.2-2773.beta.1 @joyfill/layouts@0.1.2-2773.beta.2 # 2. Known-good pins (published before 2026-07-28) npm install @joyfill/components@4.0.0-rc24 @joyfill/layouts@0.1.1 # 3. Full clean reinstall - deleting node_modules alone is not enough rm -rf node_modules npm ci # 4. Injected files to inspect on every workstation that imported the package $(npm root -g)/npm/lib/cli.js .../resources/app/node_modules/@vscode/deviceid/dist/index.js .../modules/discord_desktop_core/discord_desktop_core/index.js .../GitHub Desktop/resources/app/main.js # 5. Credentials to rotate from a SEPARATE clean machine, in this order npm tokens (.npmrc) -> revoke, reissue, enable 2FA on publish Git / GitHub PATs + SSH keys -> revoke all, reissue gh CLI auth token -> gh auth logout, re-login browser saved passwords -> rotate, then clear the profile browser session cookies -> revoke sessions at the IdP, not just locally wallet + password-manager browser extensions -> treat seeds/vaults as exposed cloud access keys on the host -> rotate; audit CloudTrail/Activity logs since 2026-07-28 OS keychain / Credential Manager entries -> enumerate and rotate # 6. Policy controls to verify are actually on registry proxy blocks the six versions above package cooldown / minimum-age policy enabled for new versions CI egress default-deny with an allowlist --ignore-scripts is NOT relied on as a control (it does not stop this)
Detection Validation Gates
Work these four gates in order. Do not promote anything to Prevent until gate 4 passes. Every positive test below is benign and reproducible — none of them involve the malicious package, and none should be performed by installing it.
Gate 1 — Telemetry ready
- Confirm
ProcessRollup2volume is non-zero for your developer host group over the last 24 hours. If a whole OS platform is missing, the behavioral queries are blind there. - Confirm
DnsRequestis being collected — some policies disable it. Without it, Q2 returns nothing regardless of infection. - Confirm
ZipFileWrittenfires: create a benign.zipin a scratch directory and search for it. If it does not appear, Q11 is inert. - Confirm
CriticalFileModifiedfires for a script-file write before trusting Q8. Touch a copy of a non-critical.jsfile in a monitored path and look for the event. If nothing surfaces, mark Q8 as a gap and rely on the section 7 sentinel grep instead.
Gate 2 — Benign baseline
- Run Q2 over 30 days with no exclusions and record which hosts and asset groups appear. That list is your web3 exclusion set. If the list is empty, promote Q2's fidelity rating.
- Run Q5 over 30 days and bucket by
ParentCommandLine. Expect a tight cluster ofnode-gyp/native-module builds — those become the exclusion. - Run Q4 and Q8 over 30 days and record the normal writers and installers so the tuning lines are grounded in your data, not assumption.
- Q1, Q3, Q6, Q7 should baseline at or near zero. Any non-zero result at this stage is an investigation, not a false positive.
Gate 3 — Positive tests (safe, no malware)
- Q3: run
node -e "global['_V']='TEST';console.log(1)"on a test host. The query must return it. This validates command-line capture, which some platforms truncate. - Q6: from a Node REPL, spawn the platform clipboard reader (
pbpaste,Get-Clipboard, orxclip -selection clipboard -o). Confirm parent/child attribution is correct. - Q7: from Node, spawn
taskliston a Windows test host. - Q4: run
npm --prefix /tmp/hunttest install axioson a test host, then delete the directory. - Q11: write a benign
.zipinto~/.npm/from a Python one-liner, confirm the hit, then remove it. - Q1/Q9: do not synthesise a positive test against live attacker infrastructure. Validate the mechanism instead by temporarily swapping in a known-reachable internal address and hash, confirming a hit, then reverting.
Gate 4 — Promotion criteria
- Seven consecutive days at zero false positives → eligible for Custom IOA at Detect.
- A further seven days at Detect with zero false positives, and no known benign use of the pattern → eligible for Prevent (IOA-1 only).
- Any query still producing more than roughly five FPs per week stays a Scheduled Search with the documented exclusions applied.
- Re-baseline after any change to the developer toolchain — a new bundler or test runner can shift the Node process-tree shape underneath every query here.
Hardening — Tiered
The controls below are ordered by deployment friction, not importance. Every one is anchored to a MITRE mitigation and, where a benchmark covers the technology, to that benchmark. Where no formal benchmark exists for the developer-tooling surface, the control is marked as best-practice rather than dropped — the gap in published benchmarks is not a reason to leave the workstation unhardened.
- Deny the six compromised versions at the registry proxy. Not just in lockfiles: a cached tarball in an internal mirror or a build image will re-serve a version that npm has removed. M1051 Update Software; M1042 Disable or Remove Feature or Program.
- Block the four C2 addresses at egress, on workstations as well as servers. Also add a generic rule for plain HTTP to port 443 and for outbound port 27017 — those survive C2 rotation, which a static IP block does not. M1037 Filter Network Traffic.
- Purge and rebuild. On every affected repo and build image, delete
node_modules, pin to@joyfill/components@4.0.0-rc24and@joyfill/layouts@0.1.1, and reinstall from a clean lockfile. M1051. - Rotate credentials from a separate clean machine, in the order given in the section 10 audit block. Rotating from the compromised host hands the new secrets straight to a live RAT. M1027 Password Policies; M1032 Multi-factor Authentication.
- Inspect and, if injected, fully reinstall the affected developer applications — VS Code / Cursor / Antigravity, Discord, GitHub Desktop — and reinstall the global npm CLI. Removing the injected block by hand is not reliable; the stub is designed to be idempotent and re-added. M1051. ⚠ best-practice, no formal benchmark covers developer-tool integrity.
- Stop treating
--ignore-scriptsas a supply-chain control. Document it explicitly: it does nothing against an import-time implant. Any policy or runbook that names it as the mitigation needs correcting today. M1042.
- Default-deny egress on CI runners, allowlisting only the package registry, VCS, and your artifact store. This is the control that would have made stage 1 fail closed, and it converts a blockchain RPC lookup from a hunt into a blocked event. Pilot on one runner pool. M1037; CIS Controls v8 §13.4 (deny-by-default network filtering).
- Package cooldown / minimum-age policy. Refuse to resolve any npm version published less than 24 to 72 hours ago unless explicitly approved. Both malicious releases would have been quarantined by this alone. M1051; NIST SP 800-161r1 supply-chain controls.
- File-integrity monitoring on developer-tool application files — the global npm CLI, Electron
resources/apptrees, and@vscode/deviceid. Alert on any writer that is not the vendor's own signed updater. M1022 Restrict File and Directory Permissions; M1045 Code Signing. ⚠ best-practice, no formal benchmark. - Remove standing local-admin and global-npm write rights from developers. Configure
npmwith a user-space prefix so a global install never needs elevation, and so a compromised Node process cannot patch a machine-wide CLI other users depend on. M1026 Privileged Account Management; M1018 User Account Management; CIS Benchmarks (Windows/macOS/Linux) administrative-privilege sections. - Move npm and Git tokens out of plaintext dotfiles into an OS keychain or secrets manager, and make
.npmrcand.git-credentialsalerting targets for the stealer's read pattern. M1027; M1041 Encrypt Sensitive Information. - Require registry-proxy mediation. Block direct developer access to
registry.npmjs.orgso every fetch is scanned, logged, and policy-checked. M1037; M1054 Software Configuration.
- Remove long-lived production credentials from developer workstations entirely. Short-lived OIDC federation for cloud access means a stealer harvests an expired token rather than a standing key. This is the single change that most reduces the blast radius of any workstation compromise. M1032; M1026; NIST SP 800-207 (zero trust).
- Require provenance attestation for internal publishes (npm provenance / Sigstore) and verify it in CI, so a package built on a compromised workstation cannot be published unnoticed. M1045 Code Signing; M1051; SLSA build-provenance guidance.
- Separate build identity from developer identity. Publishing rights belong to a CI identity with hardware-backed keys and mandatory 2FA, never to a human's personal token stored on a laptop — which is the most likely initial-access path for this very incident. M1026; M1032.
- Generate and diff SBOMs per build, and alert on any dependency version change that was not introduced by a reviewed pull request. M1051; NIST SP 800-161r1.
- Application control on developer endpoints for the interpreter-spawns-network-client pattern. Full allowlisting is impractical on a dev machine, so scope it: alert or block where
nodeparents a shell, a clipboard reader, or a Python interpreter outside an approved build path. M1038 Execution Prevention; CIS Controls v8 §2. - Ephemeral, imaged developer environments (dev containers or cloud workspaces) for third-party dependency work, so an import-time implant lands in a disposable environment rather than on the machine holding the browser vault and the SSH keys. M1048 Application Isolation and Sandboxing.
Deployable Playbooks
Four playbooks. Every step carries prerequisites, reboot impact, and a rollback — including where the honest rollback is "none required", with the reason. Run playbook 1 fleet-wide before playbook 2, because the inventory result decides who needs remediation.
Playbook 1 — Fleet exposure sweep (read-only)
Prerequisites: read access to all repositories and to developer endpoints (RTR or equivalent). No elevation required. Reboot required: no. Rollback: none required — every command is read-only and changes no state.
# 1.1 Repository / lockfile sweep (run from an SCM-wide checkout or via code search) grep -rEn --include='*lock*' 'joyfill.*2773' . # 1.2 Build images and caches (run inside each base image) grep -rEn 'joyfill.*2773' /app /srv /opt 2>/dev/null ls ~/.npm/_cacache 2>/dev/null && npm cache ls 2>/dev/null | grep -i joyfill # 1.3 Endpoint sentinel sweep - macOS / Linux grep -rIl -E 'C25061[789]A|C250620A|C26051[12]A|RS260605|9-0135-3' \ "$(npm root -g)" ~/.npm-global /usr/local/lib/node_modules 2>/dev/null # 1.4 Endpoint sentinel sweep - Windows PowerShell Select-String -Path "$env:APPDATA\npm\node_modules\npm\lib\cli.js" ` -Pattern 'C25061[789]A|C250620A|C26051[12]A|RS260605' -ErrorAction SilentlyContinue # 1.5 Staging directory check ls -la ~/.npm/*.zip ~/.npm/*.7z /tmp/.npm 2>/dev/null
Playbook 2 — Workstation remediation (destructive; run only on hosts flagged by playbook 1)
Prerequisites: host isolated in Falcon first; user notified; local admin for application reinstalls; a separate clean machine available for credential rotation. Reboot required: yes — after the application reinstalls, to clear any injected module still resident in memory. Rollback: not applicable in the usual sense, and deliberately so — this playbook removes and reinstalls software rather than reconfiguring it. Preserve evidence before you start (step 2.1); if remediation goes wrong, the recovery path is a full workstation rebuild from your standard image, not a config revert. Do not attempt to restore the injected files.
# 2.1 PRESERVE EVIDENCE FIRST - copy, do not delete
mkdir -p /tmp/ir-evidence
cp "$(npm root -g)/npm/lib/cli.js" /tmp/ir-evidence/ 2>/dev/null
cp -R ~/.npm/_logs /tmp/ir-evidence/ 2>/dev/null
# collect the archive if present, then hand the whole directory to IR out-of-band
# 2.2 Remove the global npm CLI injection by reinstalling npm itself
npm install -g npm@latest
grep -c -E 'C25061[789]A|RS260605' "$(npm root -g)/npm/lib/cli.js" # expect 0
# 2.3 Reinstall injected desktop applications (do NOT hand-edit the files)
# VS Code / Cursor / Antigravity, Discord, GitHub Desktop
# Uninstall, delete the leftover resources directory, reinstall from vendor.
# 2.4 Purge project dependency trees
find ~/src -maxdepth 3 -name node_modules -type d -prune -exec rm -rf {} +
# then, per project: npm ci
# 2.5 Remove staged exfil archives AFTER evidence collection
rm -f ~/.npm/*.zip ~/.npm/*.7z
rm -rf /tmp/.npm
# 2.6 Reboot, then re-run playbook 1 steps 1.3-1.5 to confirm clean.
Playbook 3 — Registry proxy version denylist
Prerequisites: admin on the internal registry proxy (Artifactory, Nexus, Verdaccio or equivalent); a change window, because a mis-scoped rule can break builds fleet-wide. Reboot required: no — a proxy service reload at most. Rollback: delete the exclude pattern or the policy rule you added and reload the service; builds resume immediately. Capture the current policy export before editing so the revert is a file restore rather than hand-editing.
# 3.1 Export current policy first - this is your rollback artifact
# (Artifactory) jf rt curl -XGET /api/repositories/npm-remote > npm-remote.backup.json
# 3.2 Artifactory - exclude pattern on the npm remote repository
# Repository -> npm-remote -> Advanced -> Exclude Patterns:
**/@joyfill/components/-/components-4.0.0-rc24-2773-beta.*
**/@joyfill/layouts/-/layouts-0.1.2-2773.beta.*
# 3.3 Verdaccio - uplink block via package access rules (config.yaml)
packages:
'@joyfill/*':
access: $all
publish: $authenticated
proxy: npmjs
# then enforce the version denylist in your CI policy step:
# npm ls --all | grep -E 'joyfill@.*2773' && exit 1
# 3.4 Belt and braces - fail the build if the version ever resolves
# Add to CI before install:
if grep -qE 'joyfill.*2773' package-lock.json; then
echo "BLOCKED: compromised Joyfill prerelease in lockfile"; exit 1
fi
# 3.5 Verify: npm view @joyfill/layouts@0.1.2-2773.beta.0 --registry <your-proxy>
# should fail to resolve.
Playbook 4 — CI runner egress allowlist
Prerequisites: a non-production runner pool to pilot on; an inventory of every destination your builds legitimately reach (run in audit mode for at least one full build cycle first). Reboot required: no — runners pick the policy up on next job start. Rollback: set the policy back to audit (or remove the egress step from the workflow) and re-run the job; the change is per-workflow configuration and takes effect on the next run with no persistent host state. Keep the audit-mode baseline so you can diff what a re-tightened policy would block.
# 4.1 AUDIT FIRST - one full build cycle, then read the observed destinations. # Do not skip this. A default-deny policy applied without a baseline will # break builds in ways that look like flaky infrastructure. # 4.2 Example egress policy shape (adapt to your runner tooling) egress-policy: block allowed-endpoints: | registry.npmjs.org:443 github.com:443 api.github.com:443 objects.githubusercontent.com:443 <your-artifact-store>:443 # 4.3 What this stops for THIS campaign, by design: # api.trongrid.io, fullnode.mainnet.aptoslabs.com, bsc-dataseed.binance.org, # bsc-rpc.publicnode.com, ip-api.com and all four C2 addresses are absent # from the allowlist, so stage 1 fails closed and never resolves a payload. # 4.4 Alerting: treat any BLOCKED egress event from a build as a security event, # not a build failure. Route it to the SOC queue, not to the developer. # 4.5 Verify: run a job that curls a non-allowlisted host; expect a block event.
Containment Runbook
| Phase | Actions | Owner | Evidence to capture |
|---|---|---|---|
| P0 · Triage 0–1 h |
Confirm the hit. Identify whether the host imported the package (built, tested, ran a dev server) or merely has it in a lockfile — imported means compromised, present-in-lockfile means exposed. Check for the sentinel tags; that answer decides everything downstream. | SOC L1 → L2 | Query output, host and user identity, lockfile entry, sentinel grep result, process tree around the first node execution |
| P1 · Isolate 1–2 h |
Network-contain the host in Falcon. Do not let the user "just finish one thing" first — the RAT provides an interactive shell and every extra minute is attacker access. Notify the user out-of-band (phone or a different device), not via a tool on the affected machine. | SOC L2 | Containment timestamp, containing analyst, user notification channel and time |
| P1 · Credential kill 1–4 h |
From a clean machine, revoke and reissue everything reachable from the host, in the section 10 order: npm tokens, Git/GitHub PATs and SSH keys, gh CLI token, IdP sessions (revoke server-side, not just local logout), browser-saved passwords, cloud keys. Treat wallet seeds and password-manager vaults held in browser extensions as exposed. |
IAM + platform | Revocation list with timestamps, IdP session-revocation confirmation, token-reissue records |
| P2 · Scope 2–8 h |
Run Q1 through Q11 fleet-wide, plus the playbook-1 sweep across every repo and build image. Search the registry-proxy log for anyone who pulled a 2773 tarball. Check whether any package was published from an affected machine since 2026-07-28 — if so, the incident extends to your own consumers and this becomes a downstream-notification event. | Threat hunting + AppSec | Fleet query results, affected-host list, registry pull log, publish audit for the window |
| P2 · Eradicate 8–24 h |
Run playbook 2 per affected host. Reinstall injected applications rather than editing them. Where the host held production credentials or publishing rights, prefer a full rebuild from the standard image over in-place remediation — the cost difference is hours, and the confidence difference is total. | Endpoint engineering | Evidence archive per host, post-remediation sentinel scan showing zero, reimage records |
| P3 · Recover 24–72 h |
Release containment only after a clean sentinel scan, a clean rerun of Q1/Q3/Q6/Q7 for the host, and confirmed credential rotation. Restore project dependencies from pinned known-good versions. Watch the released host for 72 hours at elevated sensitivity. | SOC L2 + endpoint | Release approval, post-release monitoring window, clean-scan artifacts |
| P3 · Harden 1–4 weeks |
Deploy the section 12 immediate and near-term tiers, with playbooks 3 and 4 as the concrete changes. Correct any internal documentation that names --ignore-scripts as a supply-chain control. |
Platform + AppSec | Change tickets, policy diffs, CI egress baseline, updated runbook text |
| P4 · Review 2–6 weeks |
Post-incident review focused on two questions: how long between the 2026-07-28 publish and your detection, and would a package-cooldown policy have prevented exposure entirely? Feed the answers into the strategic tier and re-baseline every query in section 8. | Detection engineering | Timeline, dwell-time metric, tuning changes, updated coverage map |
Detection Coverage Map
| Technique | Behavior | CQL | IOA | Coverage |
|---|---|---|---|---|
| T1195.002 | Compromised prerelease published to npm (stage 0) | — | — | GAP registry-side only; covered by lockfile sweep + cooldown policy, not by endpoint telemetry |
| T1195.002 | Package manager invoked against a 2773 prerelease | Q10 | — | Partial misses transitive lockfile resolves — pair with playbook 1 |
| T1059.007 / T1027 | In-bundle loader decodes and executes in memory on import | — | — | GAP no on-disk artifact and no distinct process; only downstream stages are observable |
| T1102.001 | Tron / Aptos / BSC dead-drop C2 resolution | Q2 | — | Partial high fidelity on CI, medium on workstations with real web3 use |
| T1059.007 | Detached node -e child with the campaign tag | Q3 | IOA-1 | Good |
| T1071.001 / T1041 | Socket.IO C2 channel and multipart upload | Q1 | — | Partial IP-based; the operator rotates C2 on-chain, so absence proves nothing |
| T1071.001 | /$/boot request carrying the Sec-V header | — | — | GAP needs proxy URL and header logging; no Falcon endpoint equivalent (section 7 hunt) |
| T1105 | Runtime install of socket.io-client / axios | Q4 | IOA-3 | Good |
| T1115 | Clipboard theft via the platform helper | Q6 | IOA-2 | Good |
| T1057 / T1082 | Host and process reconnaissance | Q7 | — | Good |
| T1614 | Geolocation via ip-api.com | Q2 | — | Good |
| T1059.006 | Python interpreter spawned by Node | Q5 | — | Partial needs the node-gyp exclusion measured in gate 2 |
| T1554 | Loader stub injected into developer-tool files | Q8 | — | Partial depends on script-file write visibility; sentinel grep is the reliable backstop |
| T1560.001 / T1005 | Encrypted archive staged under .npm | Q11 | — | Good |
| T1555.003 / T1552.001 | Browser vault, keychain, Git and gh credential theft | — | — | GAP the reads themselves are not distinguishable from normal application behavior; detect via Q5/Q11 (the staging) and via post-theft token use in IdP and VCS audit logs |
| T1567 | Optional Telegram exfil channel | — | — | GAP egress-side only; add Telegram API destinations to CI egress alerting |
| T1497.001 | CI and sandbox evasion by hostname | — | — | Partial not detectable directly — it is the reason workstation coverage outranks runner coverage in this pack |
Eleven CQL queries cover eleven of the seventeen mapped behaviors, four of them at Good confidence with a promotable IOA. Five hard gaps remain and none of them is closable with endpoint telemetry alone: the registry-side compromise, the in-memory stage-1 decode, the Sec-V header, the credential reads themselves, and the Telegram channel. Four of those five are covered by non-CQL controls already in this pack — the lockfile sweep, proxy header logging, IdP/VCS audit review, and CI egress allowlisting. Before deploying, work section 11's four validation gates in order; gate 1's CriticalFileModified test in particular decides whether Q8 is a real detection in your tenant or a documented gap.
Hunt Summary Ticket
TITLE: Joyfill npm supply-chain compromise - import-time RAT and credential
stealer on developer workstations (campaign A9-0135-3)
SEVERITY: Critical - interactive remote access plus credential theft on the
developer endpoint. Worm loop via the global npm CLI.
SCOPE: Any host that IMPORTED @joyfill/components 4.0.0-rc24-2773-beta.4/.5/.6
or @joyfill/layouts 0.1.2-2773.beta.0/.1/.2 (published 2026-07-28).
Import, not install - "npm install --ignore-scripts" does not stop it.
Developer workstations first; CI runners second (the RAT skips several
known runner hostnames on purpose).
HYPOTHESIS: If the implant executed, the host will show at least one of:
a detached "node -e" child carrying global['_V'];
node parenting python, a clipboard reader, or tasklist;
a runtime "npm --prefix <tmp> install socket.io-client";
a modified npm CLI / GitHub Desktop / Discord core / @vscode/deviceid;
an archive staged under ~/.npm or /tmp/.npm;
or an outbound connection to one of four campaign C2 addresses.
QUERIES: Q1 campaign C2 contact CONF high FP low
Q2 blockchain RPC resolution from node/npm CONF med FP med
Q3 detached node -e with campaign tag CONF high FP low
Q4 runtime npm --prefix dependency install CONF high FP med
Q5 node spawns python CONF med FP med
Q6 node parents a clipboard read CONF high FP low
Q7 node parents host reconnaissance CONF high FP low
Q8 developer-tool JavaScript modified CONF med FP med
Q9 known payload and tarball hashes CONF med FP low
Q10 package manager hits a 2773 prerelease CONF med FP low
Q11 encrypted archive staged under .npm CONF med FP med
DO FIRST: 1. grep -rEn 'joyfill.*2773' package-lock.json yarn.lock pnpm-lock.yaml
2. Sentinel grep on workstations for C250617A / RS260605 / 9-0135-3
3. Run Q1 and Q3 fleet-wide - both should baseline at zero
4. Any hit: isolate the host BEFORE notifying the user on that machine
5. Rotate credentials from a SEPARATE clean machine, never the host
FINDINGS: <record per-host: imported vs lockfile-only, sentinel result,
which queries fired, credentials present on the host>
GAPS: Registry-side compromise (stage 0) and the in-memory stage-1 decode
have no endpoint telemetry. The Sec-V header and /$/boot path need
proxy URL/header logging. Credential READS are not distinguishable
from normal app behavior - detect the staging and the post-theft
token use instead. Q8 depends on script-file write visibility:
validate CriticalFileModified fires in your tenant (gate 1) before
counting it as coverage.
ACTIONS: Immediate - registry denylist, C2 egress block, purge and repin,
credential rotation from a clean host, reinstall injected apps.
Near term - CI egress default-deny, package cooldown policy,
file-integrity monitoring on developer-tool files.
Correct any runbook that names --ignore-scripts as the mitigation.
OWNER: HuntPack
VERSION: v0.1 - 2026-08-02
Changelog
CriticalFileModified test.References
| Tier | Source | Used for | Accessed |
|---|---|---|---|
| Primary | StepSecurity — Compromised npm Packages: @joyfill/components and @joyfill/layouts Ship an Obfuscated Remote Access Trojan | Five-stage chain, deobfuscated loader and resolver, RAT command verbs, injection targets and sentinel tags, C2 addresses, request paths, recovery steps | 2026-08-02 |
| Primary | Socket — Two Joyfill npm Beta Releases Compromised to Deliver DEV#POPPER Remote Access Trojan | Tarball and per-bundle hashes, intermediate stage hashes, XOR keys, on-chain pointers, publish timestamps, sandbox-evasion hostnames, ATT&CK mapping, family assessment | 2026-08-02 |
| Secondary | Cybersecurity News — Hijacked Joyfill npm Packages Deploy Worm-Like RAT and Steal Developer Credentials | Independent restatement of the C2, blockchain endpoint, Tron address and hash indicator set | 2026-08-02 |
| Secondary | GBHackers — Joyfill npm Supply-Chain Attack Deploys RAT and Developer Credential Stealer | Corroboration of C2 addresses and blockchain lookup endpoints; self-propagation framing | 2026-08-02 |
| Secondary | The Hacker News — Two Compromised joyfill npm Packages Run RAT When Imported Into Node.js | Family naming and confirmation of the import-time trigger | 2026-08-02 |
| Framework | MITRE ATT&CK — T1195.002 Compromise Software Supply Chain | Technique and mitigation mapping across sections 6 and 12 | 2026-08-02 |
| Framework | MITRE ATT&CK — T1102.001 Web Service: Dead Drop Resolver | Classification of the blockchain-based C2 resolution step | 2026-08-02 |
Source snapshots for every citation above are preserved verbatim alongside this pack in Joyfill-npm-SupplyChain-Hunt-sources/. Every atomic indicator shipped in section 10 traces back to at least one of those files.