Joyfill npm Supply-Chain Compromise — Import-Time RAT on Developer Workstations

Malicious 2773 beta builds of @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.
Threat
Joyfill npm compromise (A9-0135-3)
Severity
ACTIVE SUPPLY CHAIN
Type
Package supply chain · RAT · Infostealer
Access
Import-time code execution (no install hook)
Version
v0.1 · 2026-08-02
Author
HuntPack
Confidence
High — two independent vendor analyses
01

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.

02

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.

TierSourceKey findingCarry forward
PrimaryStepSecurity — 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
PrimarySocket 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
SecondaryCybersecurity 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
SecondaryGBHackers (2026-07-28)Confirms C2 IPs and blockchain lookup endpoints; frames the worm/self-propagation angle. No new indicators.Partial
SecondaryThe 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.com and ip-api.com are 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.0 and @joyfill/components@4.0.0-rc24-2773-beta.4 in depth and notes the components Rollup bundle ships a throwing dynamic-require shim 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.
03

Hunt Brief & Attack Chain

Attack chain

#StepTelemetryHunt angle
0Maintainer 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 cacheLockfile and cache sweep for any 2773 prerelease; registry-proxy audit log
1Import-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
2Blockchain 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; ProcessRollup2Q2 blockchain RPC lookups from node/npm; Q3 detached node -e child carrying the campaign tag
3Campaign-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 logsQ1 direct C2 contact; proxy hunt for the Sec-V header and the /$/boot path
4Socket.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; NetworkConnectIP4Q4 runtime dependency install; Q6 clipboard readers; Q7 tasklist recon; Q5 python child
5Persistence 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 telemetryQ8 developer-tool file modification; native sentinel-tag grep (section 7)
6Python 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; ZipFileWrittenQ5 node → python; Q11 archive staged under .npm

Hunt hypotheses (ordered by fidelity)

#HypothesisATT&CKFalcon eventsExpected FP sourcesConf
H1An endpoint contacted one of the four campaign C2 addresses.T1071.001, T1105NetworkConnectIP4None credible — these are dedicated attacker hosts.High
H2A Node process spawned a detached -e child whose inline script carries the campaign tag.T1059.007, T1027ProcessRollup2Rare. Some CLI tools use node -e, but not with an inline global['_V'] assignment.High
H3A Node process performed a runtime npm install of socket.io-client/axios into a scratch prefix.T1105ProcessRollup2Package scaffolders and some test harnesses install at runtime.High
H4A Node process read the clipboard via the platform helper.T1115ProcessRollup2Electron/CLI tools that offer "copy to clipboard" usually write, not read.High
H5A Node process enumerated running processes with tasklist.T1057, T1082ProcessRollup2A few port-checking and process-manager npm modules.High
H6Developer-tool JavaScript (npm CLI, GitHub Desktop, Discord core, @vscode/deviceid) was modified outside a vendor update.T1554CriticalFileModifiedGenuine application self-updates; npm install -g npm.Medium
H7A developer workstation or build agent resolved blockchain RPC endpoints from a Node process.T1102.001DnsRequestReal web3 development; crypto wallet extensions.Medium
H8A Node process spawned a Python interpreter (stealer staging).T1059.006ProcessRollup2node-gyp and native-module builds legitimately call Python.Medium
H9An encrypted archive was staged under a .npm directory.T1560.001, T1005ZipFileWrittenThe npm cache lives there but does not contain .zip/.7z files.Medium
H10A package manager was invoked against a Joyfill 2773 prerelease.T1195.002ProcessRollup2Deliberate 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.

04

Consolidated IOC Table

TypeValueConfActionContextExpiry
package@joyfill/components 4.0.0-rc24-2773-beta.4 / .5 / .6HighdetectPayload appended to dist/index.js, dist/index.esm.js, dist/joyfill.min.js2027-02-02
package@joyfill/layouts 0.1.2-2773.beta.0 / .1 / .2HighdetectPayload spliced into dist/index.cjs.js and dist/index.es.js2027-02-02
ipv4166.88.134.62 (443 socket, 80 upload)HighdetectPrimary C2 for the npm campaign tag A9-0135-32027-02-02
ipv423.27.13.43HighdetectServes the detached branch's /$/boot downloader2027-02-02
ipv4198.105.127.210HighdetectAlternate campaign profile (numeric tags)2027-02-02
ipv423.27.202.27 (443, 27017)HighdetectAlternate campaign profile2027-02-02
sha25626351aed0397158d3a3b8cc8fd3047d4c015d264c9895f10f20f1521b974ed18HighdetectFinal Socket.IO RAT clientCode payload (77,276 bytes)2027-02-02
sha25636ff00b45e67baa7e3674b0c80f48e88737264c61e5c6b3b091200972de8157cHighdetectPython credential stealer (82,457 bytes)2027-02-02
sha256cb46f12d70824ea24ed1f8bcf45bf3f86680e02a9089aafc03b27f691be57be3HighhuntIn-process first-stage loader (5,849 bytes)2026-11-02
sha25678f0de8682e0e894a5784eb7e95db4da6088f528918ca3107dd1e76f80a561d8HighhuntDetached second-stage bootstrap (3,525 bytes)2026-11-02
sha256adc4af90540d33cd1e98f44b51482ae9250fbeb97d6f8d7841c81b618cb2c6e6Highdetect@joyfill/layouts malicious tarball2027-02-02
sha256bcc93dc55bc7daedf4ca57254f0e7a7f1c40e09851eab98fe10cde801982db17Highdetect@joyfill/components malicious tarball2027-02-02
url-path/$/boot · /u/e · /u/f · /0x/js · /verify-human/ · /snv · /$/{id} · /d/python.zip · /d/7zr.exe · /d/python.7zHighhuntC2 request paths — pivot on proxy and web-gateway logs2026-11-02
http-headerSec-V: A9-0135-3HighhuntCampaign tag phoned home on the /$/boot request2026-11-02
markerC250617A · C250618A · C250619A · C250620A · C260511A · C260512A · RS260605HighhuntComment sentinels wrapping the injected loader stub in developer-tool files2027-02-02
marker9-0135-3 · A9-0135-3 · _V · _p_t · _t_s · _t_uHighhuntCampaign fragment and global variable names planted by the loader2027-02-02
tron-addressTMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAPHighpivotBranch A pointer — latest outbound tx carries the BSC hash2026-09-02
tron-addressTXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcGHighpivotBranch B pointer (no outbound txs — Aptos fallback fires)2026-09-02
tron-addressTA48dct6rFW8BXsiLAtjFaVFoSuryMjD3vHighpivotTier-two pointer set by stage 22026-09-02
domainapi.trongrid.io · fullnode.mainnet.aptoslabs.com · bsc-dataseed.binance.org · bsc-rpc.publicnode.com · ip-api.comMediumhuntLegitimate public services — do not block. Hunt only, scoped to node/npm as the requesting process2026-11-02
xor-key2[gWfGj;<:-93Z^C · m6:tTh^D)cBz?NM] · ThZG+0jfXE6VAGOJHighenrichBranch A / branch B / /$/boot response keys — use for sample triage2026-09-02
path%USERPROFILE%\.npm · /tmp/.npmHighhuntCredential staging directory for the encrypted exfil archive2027-02-02
05

Affected Surface & Telemetry Matrix

SurfaceRequired telemetryPriorityGap risk
Developer workstations (Windows / macOS / Linux) — the actual targetProcessRollup2, SyntheticProcessRollup2, NetworkConnectIP4, DnsRequestCriticalLow — standard Falcon sensor coverage
Developer-tool application files (npm CLI, VS Code / Cursor / Antigravity, Discord, GitHub Desktop)CriticalFileModified or equivalent file-integrity telemetryCriticalHigh — write visibility for non-PE script files is sensor-policy dependent; validate before relying on Q8
CI/CD build agents and ephemeral runnersProcessRollup2, NetworkConnectIP4, egress/proxy logsHighMedium — 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 headersHighMedium — the Sec-V header and /$/boot path need header/URL logging, not just IP
Source repositories and lockfilesSCM search / dependency inventoryHighLow — a simple grep suffices
Internal registry proxy / mirror / build image cacheRegistry audit logs; cached tarball inventoryMediumMedium — 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 logsCriticalHigh — theft is silent; only post-theft token use is observable
Server-side rendering and production deploy paths that import the packageProcessRollup2, NetworkConnectIP4 on app serversMediumMedium — containerised runtimes may lack sensor coverage
06

ATT&CK Mapping

TacticTechniqueNameObserved behaviorQuery / control
Initial AccessT1195.002Compromise Software Supply ChainMalicious 2773 prereleases published to npm with the implant welded into dist bundlesQ10 · registry denylist
ExecutionT1059.007Command and Scripting Interpreter: JavaScriptLoader executes on module import; Function-constructor ladder; eval of decrypted stagesQ3, Q4
ExecutionT1059.006Command and Scripting Interpreter: Python~py verb spawns a detached Python interpreter for the stealerQ5
ExecutionT1059.001Command and Scripting Interpreter: PowerShellpowershell -NoProfile -Command "Get-Clipboard" for clipboard theftQ6
ExecutionT1059.004Command and Scripting Interpreter: Unix Shellpbpaste, xclip, xsel on macOS/Linux; shell execution verbsQ6
PersistenceT1554Compromise Host Software BinaryLoader stub injected into npm CLI, GitHub Desktop, Discord core, @vscode/deviceidQ8 · native sentinel grep
Defense EvasionT1027Obfuscated Files or InformationSeeded string shuffle, dictionary decompression, LZ-String table of 337 entries, control-flow flatteningBehavioral queries; script scanning
Defense EvasionT1027.013Encrypted/Encoded FileRepeating-key XOR on every retrieved stage; AES ZIP for the exfil archiveQ11
Defense EvasionT1497.001Virtualization/Sandbox Evasion: System ChecksSkips github-runner, buildbot, buildkitsandbox, sandbox-pool-, cloudchamber, WSL2; 30-second re-entry guard; decoder self-integrity checkWorkstation-first hunting
DiscoveryT1082System Information Discoveryss_info reports OS, hostname, user, Node path and version, startup pathQ7
DiscoveryT1057Process Discoverytasklist /FO CSV /NH run hidden on WindowsQ7
DiscoveryT1614System Location Discoveryss_ip geolocates the host through ip-api.comQ2
CollectionT1115Clipboard Datass_cb reads the clipboard per platformQ6
CollectionT1005Data from Local Systemss_upf/ss_upd upload files and whole directoriesQ11
CollectionT1560.001Archive via Utilitypyzipper AES ZIP staged under .npmQ11
Credential AccessT1555.003Credentials from Web BrowsersChromium and Firefox stores decrypted via DPAPI, macOS Keychain, Secret Service / KWalletQ5 · credential rotation
Credential AccessT1552.001Unsecured Credentials: Credentials In FilesGit credentials, GitHub CLI config, VS Code storage, GitHub Desktop logs, .npmrc tokensHardening §12
Command and ControlT1102.001Web Service: Dead Drop ResolverTron → BSC → Aptos on-chain transactions carry the encrypted next stageQ2
Command and ControlT1071.001Application Layer Protocol: Web ProtocolsSocket.IO over plain HTTP to 166.88.134.62:443; /$/boot GET with the Sec-V headerQ1
Command and ControlT1105Ingress Tool TransferRuntime npm install of axios/socket.io-client; Python runtime provisioned from /d/ pathsQ4, Q9
ExfiltrationT1041Exfiltration Over C2 ChannelMultipart POST of the encrypted archive to /u/f after registering metadata at /u/eQ1, Q11
ExfiltrationT1567Exfiltration Over Web ServiceOptional Telegram document channel when C2 supplies a bot token and chat IDEgress hunting
07

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.7z against 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 2773 prerelease 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.
08

CrowdStrike LogScale CQL Hunt Queries

Pick your tenant's cloud first — every "Open in Falcon" button below uses this selection.

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.

Q1 · Direct contact with campaign C2 infrastructure
CONF HIGHFP LOWCOST LOW

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])
Q2 · Blockchain RPC and geo-IP resolution from a package-manager process
CONF MEDFP MEDCOST LOW

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)
Q3 · Detached node -e child carrying the campaign tag
CONF HIGHFP LOWCOST LOW

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])
Q4 · Runtime dependency self-healing (npm --prefix install of socket.io-client / axios)
CONF HIGHFP MEDCOST LOW

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])
Q5 · Node spawning a Python interpreter (stealer staging)
CONF MEDFP MEDCOST LOW

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])
Q6 · Node spawning a clipboard reader (ss_cb)
CONF HIGHFP LOWCOST LOW

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])
Q7 · Node-parented host reconnaissance (tasklist, ss_info)
CONF HIGHFP LOWCOST LOW

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])
Q8 · Modification of developer-tool JavaScript (worm re-injection targets)
CONF MEDFP MEDCOST MED

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])
Q9 · Known Joyfill / DEV#POPPER payload hashes
CONF MEDFP LOWCOST LOW

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])
Q10 · Package manager invoked against a Joyfill 2773 prerelease
CONF MEDFP LOWCOST LOW

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])
Q11 · Encrypted credential archive staged under .npm
CONF MEDFP MEDCOST LOW

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])
09

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 install on 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-gyp makes 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 nameSourceCadenceNotify
DETECT — Detached node -e with campaign tag (npm supply chain)Q3HourlySOC on-call, Critical
DETECT — Node-parented clipboard readQ6HourlySOC on-call, High
DETECT — Campaign C2 contact (npm supply chain)Q1Every 15 minutesSOC on-call, Critical
HUNT — Blockchain RPC from build agentsQ2 (CI scope)DailyDetection engineering
HUNT — Developer-tool JavaScript modifiedQ8WeeklyDetection engineering
HUNT — Joyfill 2773 prerelease invokedQ10DailyAppSec / platform
10

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.

Falcon IOC Management CSVbulk import
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 Signatureshunt logic
# 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
Named Tooling & Pivot Identifierstriage / attribution
# 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
Dependency & Policy Auditremediation checklist
# 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)
11

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 ProcessRollup2 volume 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 DnsRequest is being collected — some policies disable it. Without it, Q2 returns nothing regardless of infection.
  • Confirm ZipFileWritten fires: create a benign .zip in a scratch directory and search for it. If it does not appear, Q11 is inert.
  • Confirm CriticalFileModified fires for a script-file write before trusting Q8. Touch a copy of a non-critical .js file 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 of node-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, or xclip -selection clipboard -o). Confirm parent/child attribution is correct.
  • Q7: from Node, spawn tasklist on a Windows test host.
  • Q4: run npm --prefix /tmp/hunttest install axios on a test host, then delete the directory.
  • Q11: write a benign .zip into ~/.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.
12

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.

Immediate — deploy this week
  • 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-rc24 and @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-scripts as 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.
Near term — 1 to 4 weeks, pilot first
  • 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/app trees, 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 npm with 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 .npmrc and .git-credentials alerting targets for the stealer's read pattern. M1027; M1041 Encrypt Sensitive Information.
  • Require registry-proxy mediation. Block direct developer access to registry.npmjs.org so every fetch is scanned, logged, and policy-checked. M1037; M1054 Software Configuration.
Strategic — 1 to 3 months
  • 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 node parents 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.
13

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.
14

Containment Runbook

PhaseActionsOwnerEvidence 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
15

Detection Coverage Map

TechniqueBehaviorCQLIOACoverage
T1195.002Compromised prerelease published to npm (stage 0)GAP registry-side only; covered by lockfile sweep + cooldown policy, not by endpoint telemetry
T1195.002Package manager invoked against a 2773 prereleaseQ10Partial misses transitive lockfile resolves — pair with playbook 1
T1059.007 / T1027In-bundle loader decodes and executes in memory on importGAP no on-disk artifact and no distinct process; only downstream stages are observable
T1102.001Tron / Aptos / BSC dead-drop C2 resolutionQ2Partial high fidelity on CI, medium on workstations with real web3 use
T1059.007Detached node -e child with the campaign tagQ3IOA-1Good
T1071.001 / T1041Socket.IO C2 channel and multipart uploadQ1Partial IP-based; the operator rotates C2 on-chain, so absence proves nothing
T1071.001/$/boot request carrying the Sec-V headerGAP needs proxy URL and header logging; no Falcon endpoint equivalent (section 7 hunt)
T1105Runtime install of socket.io-client / axiosQ4IOA-3Good
T1115Clipboard theft via the platform helperQ6IOA-2Good
T1057 / T1082Host and process reconnaissanceQ7Good
T1614Geolocation via ip-api.comQ2Good
T1059.006Python interpreter spawned by NodeQ5Partial needs the node-gyp exclusion measured in gate 2
T1554Loader stub injected into developer-tool filesQ8Partial depends on script-file write visibility; sentinel grep is the reliable backstop
T1560.001 / T1005Encrypted archive staged under .npmQ11Good
T1555.003 / T1552.001Browser vault, keychain, Git and gh credential theftGAP 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
T1567Optional Telegram exfil channelGAP egress-side only; add Telegram API destinations to CI egress alerting
T1497.001CI and sandbox evasion by hostnamePartial 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.

16

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
17

Changelog

v0.12026-08-02Initial pack. Built from two primary vendor analyses plus three corroborating reports, all snapshotted for provenance. Eleven CQL hunt queries, three Custom IOA candidates, six Scheduled Search recommendations, four deployable playbooks with rollback, and an eight-phase containment runbook. Blockchain RPC endpoints deliberately routed to hunt-only and excluded from the Falcon import CSV. Q8 shipped with an explicit tenant-validation caveat pending the gate 1 CriticalFileModified test.
18

References

TierSourceUsed forAccessed
PrimaryStepSecurity — Compromised npm Packages: @joyfill/components and @joyfill/layouts Ship an Obfuscated Remote Access TrojanFive-stage chain, deobfuscated loader and resolver, RAT command verbs, injection targets and sentinel tags, C2 addresses, request paths, recovery steps2026-08-02
PrimarySocket — Two Joyfill npm Beta Releases Compromised to Deliver DEV#POPPER Remote Access TrojanTarball and per-bundle hashes, intermediate stage hashes, XOR keys, on-chain pointers, publish timestamps, sandbox-evasion hostnames, ATT&CK mapping, family assessment2026-08-02
SecondaryCybersecurity News — Hijacked Joyfill npm Packages Deploy Worm-Like RAT and Steal Developer CredentialsIndependent restatement of the C2, blockchain endpoint, Tron address and hash indicator set2026-08-02
SecondaryGBHackers — Joyfill npm Supply-Chain Attack Deploys RAT and Developer Credential StealerCorroboration of C2 addresses and blockchain lookup endpoints; self-propagation framing2026-08-02
SecondaryThe Hacker News — Two Compromised joyfill npm Packages Run RAT When Imported Into Node.jsFamily naming and confirmation of the import-time trigger2026-08-02
FrameworkMITRE ATT&CK — T1195.002 Compromise Software Supply ChainTechnique and mitigation mapping across sections 6 and 122026-08-02
FrameworkMITRE ATT&CK — T1102.001 Web Service: Dead Drop ResolverClassification of the blockchain-based C2 resolution step2026-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.