WeaselBiscuit — npm-Distributed JavaScript Infostealer with Low-to-Moderate DPRK Overlap

A stripped-down Node.js infostealer shipped in 13-17 malicious npm packages: an Npoint.io dead-drop loader, in-memory new Function execution, and a small polling Express HTTP control plane that steals Chrome extension storage and, on operator command, clipboard and Windows keystrokes.
Threat
WeaselBiscuit
Severity
HIGH
Type
npm Supply Chain / JS Infostealer
Access
Dev Workstation & CI/CD (Node.js)
Version
v0.1
Author
HuntPack
Confidence (malware)
HIGH
DPRK Attribution
LOW–MODERATE
Generated
2026-09-20
01

Executive Summary

WeaselBiscuit is a previously undocumented, self-contained JavaScript infostealer disclosed 2026-09-18 by OpenSourceMalware (researcher Paul McCarty, aka 6mile, with co-founder Jenn Gile), found bundled inside a cluster of 13-17 malicious npm packages first seen between 2026-09-12 and 2026-09-16. Unlike a typosquat that rides an npm install lifecycle hook, WeaselBiscuit has no preinstall/postinstall script at all — it fires when the package is actually imported or required by an application, or manually started, which is the single most important operational detail in this pack and the reason several install-time controls that work against other npm stealers (e.g. npm config set ignore-scripts true) do not stop this one. On import, the package auto-spawns a detached Node.js loader process, writes its PID to a local .pid marker, and retrieves a Base64-encoded second stage from a hardcoded Npoint.io dead-drop URL, executing it in memory via new Function — the payload never touches disk. The second stage resolves its C2 configuration from a second, shared Npoint URL, which in every recovered sample points to the same plain Express HTTP control plane at 103.170.217.184:8787.

Once live, WeaselBiscuit profiles the host (hostname, user, OS, CPU/RAM, local and public IP with a nested api.ipify.org → ip-api.com geolocation lookup) and uploads every readable, nonempty file under Chrome's Local Extension Settings LevelDB store — the directory where extensions like MetaMask cache signing/session state — on Windows, macOS, and Linux. On operator command only, it can also capture clipboard contents (Get-Clipboard / pbpaste) and, Windows-only, log keystrokes via a temporary PowerShell script written to %TEMP%\kb-monitor\keyboard-monitor-*.ps1. Each install is tagged with a numeric campaign identifier baked into the package name and every upload (10, 12, 79, 95, 99 confirmed at the payload level; 44 appears only in package naming — see §2).

The researcher explicitly frames this as behaviorally, not conclusively, DPRK-linked: the code borrows structural tradecraft from the DPRK's Contagious Interview tooling (BeaverTail and OtterCookie) — the Npoint dead-drop pattern, the nested public-IP/geolocation lookup, and a numeric per-install campaign marker resembling the PolinRider cluster — but is deliberately stripped down: no wallet-draining code, no browser-credential decryption, no seed-phrase sweep, no InvisibleFerret/Python second stage, no screenshots, and no Socket.IO/remote shell. OpenSourceMalware itself rates DPRK attribution "Low to moderate" and calls the DPRK-strain hypothesis "an analytic hypothesis, not a confirmed attribution." This pack treats that caveat as binding: nothing here is shipped at Prevent-tier severity on the strength of attribution, and every indicator below traces to a single primary-research lineage (see §2).

Defender priority: because there is no lifecycle hook to block, the highest-leverage control is never letting the package into the dependency tree in the first place (pre-merge SCA/vetting), backed by runtime detection of a detached Node child process reaching an Npoint dead-drop or the known C2 — not an install-time script block, which does nothing against this specific threat.
02

Source Review & Web Hunter Notes

TierSourceKey FindingCarry Forward
1OpenSourceMalware blog, "WeaselBiscuit Strips BeaverTail and OtterCookie Down to Essentials," 2026-09-18 [01]Primary disclosure: capability list, confirmed infection chain, campaign-structure table (6 Npoint resolvers), full capability comparison vs. BeaverTail/OtterCookie, confidence-and-gaps table, tradecraft signals, C2 route mapYes
1GitHub — OpenSourceMalware/WeaselBiscuit repository, README.md [03]Same research org's canonical writeup (near-identical to [01], with a slightly different 16-package enumeration and package-version detail); ends mid-sentence in the live repo as of fetch time on both retrieval paths used (not a fetch artifact)Yes
1GitHub — same repo, yara/ detection rules (3 files) [04]Machine-readable confirmation of every Npoint UUID, the C2 IP:port, and the six package-name strings the researcher considers diagnostic; independently pins the numeric identifier set 10/12/44/79/95/99Yes
1GitHub — same repo, deobfuscated second-stage sample, process-tailwind.second.stage.deobfuscated.js [05]Ground-truth confirmation, straight from the actual sample text (read-only, never executed — see security note below), of the six C2 route strings, the kb-monitor/keyboard-monitor- temp-file pattern, the Local Extension Settings path, the nested ipify/ip-api calls, and the literal identifier: "99" fieldYes
2The Hacker News, "WeaselBiscuit Stealer Spreads via 13 npm Packages to Harvest Chrome Extension Storage," Ravie Lakshmanan, 2026-09-18 [02]Press restatement of [01] with direct quotes from McCarty and Gile; a compact 13-package list and a clean IOC table; adds one external corroborating detail (NVISO, Nov 2025, previously flagged api.npoint.io usage in Contagious Interview activity generally — not WeaselBiscuit-specific)Yes

Single-primary-source limitation — read before treating anything here as Prevent-tier. Sources [01], [03], [04] and [05] are all authored/published by the same research organization (OpenSourceMalware); [02] is press syndication of that same research with direct quotes from the same two people. There is, as of this pack's build date, no independent second research organization that has separately recovered or corroborated any WeaselBiscuit indicator. Per this pipeline's two-source rule, that means every atomic indicator in this pack ships at Detect, never Prevent, and severity is capped below Critical in the Falcon IOC CSV (§10) regardless of how the primary researcher characterizes the threat in prose. This is stated once here and enforced mechanically: the §4 IOC table's Action column and the §10 CSV's action column agree exactly, and both read detect throughout.

Research decisions

  • Package-count discrepancy across (and within) sources. The GitHub README lists "16 packages we've found so far" and enumerates exactly 16 names. The OpenSourceMalware blog's own intro paragraph says "11 npm packages" but its table lists the same 16 names as the README — an apparent editing artifact where the count grew as research continued but the intro sentence wasn't updated. The Hacker News headline and body say "13 npm packages" and name exactly 13 of those 16 (omitting laycot, @vibecheck-polid/process-runtime-utils, and @railone/image-utils). A 17th name, swnwall@1.2.10, appears in both [01] and [03] only inside the campaign-structure/resolver table, never in either source's headline package list. This pack ships the full union of 17 distinct names (§10) rather than picking one source's count, and reports the discrepancy here rather than silently reconciling it.
  • The 7th Npoint resolver's identifier is genuinely unconfirmed. https://api.npoint.io/24c12c4b66a29747764f is hardcoded inside id79-client's code but returned HTTP 404 when the researcher retrieved it, so its embedded stage identifier is listed as "Unknown" in the README/blog prose. The campaign-infrastructure YARA rule's own comment labels this same UUID // ID 44 — almost certainly the researcher's own inference by elimination (six @biz44/id*-client packages map to six numeric IDs; five of six resolvers were independently confirmed with identifiers 99/10/79/12/95, leaving 44 as the remaining candidate) — but that inference is not stated as confirmed in the prose itself. This pack ships that resolver at low confidence/severity with the ambiguity spelled out, rather than asserting "44" as confirmed.
  • Confirmed vs. clustered package linkage. Only four packages (process-tailwind@1.1.99, engin1@1.3.99, swnwall@1.2.10, id79-client@1.1.79) have an individually-documented, direct Npoint-resolver delivery link in the source material. The remaining thirteen names were "grouped together" by the researcher's own automation "as they shared IOCs" — a real and useful signal, but a different (weaker) evidentiary tier than the four confirmed names. §4 and §8 (Q1 vs. Q2) keep these two tiers separate rather than treating all seventeen names as equally confirmed.
  • Malware-sample handling. The GitHub repository hosts live, attacker-authored files (a deobfuscated second-stage JS sample and its raw counterpart) as research artifacts. Per this pack's build instructions, the deobfuscated sample was fetched and read as inert plain text only — never executed, imported, or evaluated in any interpreter — solely to confirm specific literal strings (route paths, the kb-monitor temp-directory name, the identifier field) against the primary artifact rather than taking the prose write-up on faith. The raw (non-deobfuscated) payload file and the two logo/attack-chain images in the repository were not fetched; they add no citable technical detail beyond what [01]/[03]/[04]/[05] already establish.
  • Prompt-injection check. All five fetched sources — including the README, all three YARA rule files, and the deobfuscated malware sample text — were reviewed for embedded instructions directed at an AI agent (hidden text, comments claiming authorization, "note to the AI," redirection to fetch additional URLs, or instructions to skip a step or execute something). None was found in any of the five snapshots. See §18 for the full statement.
03

Hunt Brief & Attack Chain

StepTelemetryHunt Angle
1. A developer or CI job adds/imports one of the 17 known WeaselBiscuit-associated npm packages (no lifecycle hook — runs on import/require, not npm install)ProcessRollup2 (parent application process); no install-time signal exists for this stepBaseline rare/first-seen package imports; SCA/dependency-review gate (see §5 gap, §12)
2. On import, index.js calls initialize(), which starts a detached node loader.js child process and writes its PID to <package-directory>/.pidProcessRollup2 / SyntheticProcessRollup2 — ParentBaseFileName=node/npm, FileName=nodeQ1, Q2, Q3
3. loader.js retrieves JSON from a hardcoded Npoint.io first-stage resolver URLDnsRequest to api.npoint.ioQ4
4. Loader Base64-decodes the JSON code field and executes it in memory via new Function — the payload never touches diskNone directly observable in EDR process/file telemetryGAP — in-memory reflective execution leaves no distinct ProcessRollup2/file event; see §5
5. Second stage retrieves C2 configuration from the shared Npoint resolver, resolving to 103.170.217.184:8787DnsRequest to api.npoint.io / NetworkConnectIP4 to 103.170.217.184Q4, Q5
6. Host profiling: hostname, user, OS, CPU/RAM, local IP; public IP via api.ipify.org, then nested geolocation lookup via ip-api.comDnsRequest to api.ipify.org / ip-api.com from a node child processQ6
7. Uploads every readable, nonempty file under Chrome profiles' Local Extension Settings LevelDB directory (Windows/macOS/Linux)FileOpenInfo / FileReadInfo — TargetFileName contains "Local Extension Settings"Q7 (⚠ validate in tenant) + native audit hunt (§7)
8. On operator command via a 5-second poll of /api/clipboard-status/<hostname>: captures clipboard via Get-Clipboard (Windows) / pbpaste (macOS)ProcessRollup2 — node spawns powershell/pwsh/pbpasteQ9
9. On operator command via a 5-second poll of /api/keyboard-mouse-status/<hostname>: logs Windows keystrokes using a temporary PowerShell script at %TEMP%\kb-monitor\keyboard-monitor-*.ps1ProcessRollup2 — node spawns powershell with a kb-monitor pathQ8
10. All collected data exfiltrates via multipart/JSON HTTP POST to the six named C2 routes on 103.170.217.184:8787NetworkConnectIP4 to 103.170.217.184:8787Q5
04

Consolidated IOC Table

Every row below is single-primary-sourced (see §2). Action is capped at detect throughout — none of these indicators are shipped at prevent, and this table's Action column agrees exactly with the §10 CSV.

TypeValueConfidenceActionContextExpiry
IPv4:Port103.170.217.184:8787High (single primary source, but confirmed via retrieved live configuration + observed client routes + independent sample text)detectShared WeaselBiscuit C2 — plain Express HTTP polling control plane, no TLS observed. All five confirmed second-stage samples resolve to this same IP:port2026-12-20
SHA2567b15605f23b131b3eeea57e031ae7cb32fc4b78c7bbb2025aa7a561ea5ae5159High (exact recovered-payload match)detectConfirmed WeaselBiscuit second-stage infostealer, delivered in-memory via Npoint + Base64 + new Function — never written to disk, so this hash is only useful if the payload is captured from memory/network, not from a file scan2026-12-20
URL (Npoint resolver)https://api.npoint.io/24c25d5f5fcbb0992a4fHighdetectFirst-stage resolver — confirmed process-tailwind@1.1.99 delivery; embedded stage identifier 992026-12-20
URL (Npoint resolver)https://api.npoint.io/641d37178a880b1e8b8fHighdetectFirst-stage resolver — confirmed swnwall@1.2.10 delivery; embedded stage identifier 102026-12-20
URL (Npoint resolver)https://api.npoint.io/33e8d008c334b060adadHighdetectFirst-stage resolver — confirmed WeaselBiscuit clone payload; embedded stage identifier 792026-12-20
URL (Npoint resolver)https://api.npoint.io/ddae72efbb6714fae922HighdetectFirst-stage resolver — confirmed WeaselBiscuit clone payload; embedded stage identifier 122026-12-20
URL (Npoint resolver)https://api.npoint.io/933a731a5e97f4b45249HighdetectFirst-stage resolver — confirmed WeaselBiscuit clone payload; embedded stage identifier 952026-12-20
URL (Npoint resolver)https://api.npoint.io/37c0a0c68bf7a94ed731HighdetectShared second-stage C2-configuration resolver — all five confirmed stages retrieve the same C2 (103.170.217.184:8787) from this single URL; highest-value single indicator in this pack2026-12-20
URL (Npoint resolver)https://api.npoint.io/24c12c4b66a29747764fLow — embedded identifier unconfirmed (see §2)detectHard-coded fallback/alternate resolver referenced inside id79-client; returned HTTP 404 at research time. README lists its identifier as "Unknown"; a YARA comment infers "ID 44" by elimination, not corroborated in prose2026-10-20
Package (confirmed direct link)process-tailwind@1.1.99, engin1@1.3.99, swnwall@1.2.10, id79-client@1.1.79HighdetectIndividually documented direct Npoint-resolver delivery link in primary source; process-tailwind and engin1 are reported byte-identical (loader/init/index)2026-12-20
Package (clustered by shared IOCs)laycot@1.3.10, process-mite, process-lhpm, @biz44/id10-client, @biz44/id12-client, @biz44/id44-client, @biz44/id79-client, @biz44/id95-client, @biz44/id99-client, @biz44/process-runtime-utils, @biz44/runtime-utils, @vibecheck-polid/process-runtime-utils, @railone/image-utilsMedium — grouped by the researcher's automation via shared IOCs, not individually confirmed with a direct resolver linkdetect13 additional names; full per-package first-seen timestamps in the source snapshots (§18, sources 01/03)2026-12-20
BehavioralDetached node loader.js child process retrieving a Base64 code field from a Npoint.io JSON blob and executing it via new FunctionHigh (4 sources)detectCore first-stage signature; survives any future Npoint UUID rotation2026-12-20
BehavioralNested public-IP + geolocation lookup: api.ipify.org → ip-api.com, from a node-spawned processMediumenrichFingerprinting step; both third-party services are themselves legitimate and used by unrelated legitimate tooling — corroborating signal, not standalone2026-12-20
BehavioralBulk read/upload of Chrome's Local Extension Settings LevelDB directory by a node-spawned processMediumhuntFinancially relevant (wallet-extension state) even without dedicated wallet-draining code; field availability is sensor/license dependent — ⚠ validate in tenant2026-12-20
Behavioral%TEMP%\kb-monitor\keyboard-monitor-*.ps1 — temporary Windows keylogger PowerShell script, spawned by node, on operator command onlyHighdetectVery low FP; near-unique string pattern confirmed independently in both the README and the sample text2026-12-20
Behavioral.pid marker file written to the importing package's own directory under node_modulesMediumhuntNo confirmed generic Falcon file-write event maps cleanly to this — treated as a native/manual filesystem check (§7), not a CQL detection, rather than invented as a CQL field2026-12-20
Behavioral / campaign markerNumeric identifier 10, 12, 79, 95, 99 embedded in package names and in uploaded system-info/extension-storage payloadsHigh (confirmed at the payload level in 5 of 6 stages)huntNot an atomic indicator on its own (a bare 2-digit number); useful only as a pivot/grouping key once a WeaselBiscuit hit is already confirmed by another indicator2026-12-20
Behavioral / campaign markerNumeric identifier 44Low — appears only in package naming (@biz44/id44-client, the @biz44 scope itself), not confirmed at the stage/payload levelpivotSee §2 research decision on the 7th resolver2026-10-20
05

Affected Surface & Telemetry Matrix

SurfaceTelemetryPriorityGap
Developer workstations (Windows/macOS/Linux) running application code that imports a compromised dependencyProcessRollup2, DnsRequest, NetworkConnectIP4, FileOpenInfo/FileReadInfoHighNone major for process/network; file-read event field availability is sensor/license dependent
CI/CD runners and test environments that execute application code (not merely npm install) against a compromised dependencySame as above, on the runner hostCriticalSaaS-hosted ephemeral runners (GitHub-hosted Actions, GitLab.com SaaS, CircleCI cloud) cannot carry a Falcon sensor at all — see native hunts (§7)
The import/require execution trigger itselfNone directly — this is applicaton source-code behavior, not a distinguishable install-time eventHighGAP — unlike an npm lifecycle-hook stealer, there is no ignore-scripts-style control point; prevention has to happen before the dependency is added, not at install time (see §12)
In-memory second-stage execution via new FunctionNone observed to map directly to a distinct EDR process/file eventMediumGAP — reflective in-memory loading is inherently hard to see without memory scanning or a Node-specific runtime hook; this pack hunts the network fetch and the resulting behavior instead of the eval step itself
Chrome extension-storage LevelDB store (Local Extension Settings)FileOpenInfo / FileReadInfoMediumPartial — event availability and exact field set is sensor-version/license dependent; validate before relying on Q7 alone
Windows keylogger / clipboard operator-gated modulesProcessRollup2 (node spawning powershell/pwsh/pbpaste)MediumGood coverage for the confirmed Windows keylogger path; broader for clipboard, since powershell/pbpaste have many benign parents besides node
npm registry / package-publishing side (catching the malicious package before it's ever imported)Not applicable to endpoint EDRHighGAP — requires SCA/registry-side tooling; entirely outside Falcon's visibility
06

ATT&CK Mapping

TacticTechniqueObserved BehaviorQuery / Control
Initial AccessT1195.001 — Compromise Software Dependencies and Development ToolsMalicious code shipped directly inside 13-17 published npm packages, triggered on import/require rather than an install-time hookQ1, Q2; pre-merge SCA vetting (§12)
ExecutionT1059.007 — Command and Scripting Interpreter: JavaScriptLoader and second stage both execute as JavaScript inside the Node.js runtimeQ1, Q2, Q3
Defense EvasionT1620 — Reflective Code LoadingSecond-stage payload is Base64-decoded and executed in memory via new Function, never written to diskGAP — no distinct EDR event for the eval step itself; hunted indirectly via Q3/Q4 (the fetch that precedes it)
Defense EvasionT1140 — Deobfuscate/Decode Files or InformationBase64 decoding of the Npoint JSON code field prior to executionContext only — no distinct telemetry
DiscoveryT1082 — System Information DiscoveryCollects hostname, username, OS, CPU, memory, home/temp paths, local interface IP/MACProcess context on Q1-Q3 hits
DiscoveryT1016.001 — Internet Connection DiscoveryQueries api.ipify.org to learn the host's public IPQ6
DiscoveryT1614.001 — System Location DiscoveryFeeds the resolved public IP to ip-api.com for geolocation — the nested-lookup pattern the researcher flags as a DPRK tradecraft signalQ6
CollectionT1005 — Data from Local SystemBulk-uploads every readable, nonempty file under Chrome's Local Extension Settings directoryQ7 (⚠ validate in tenant) + native hunt (§7)
CollectionT1115 — Clipboard DataCaptures clipboard contents via Get-Clipboard (Windows) / pbpaste (macOS), gated by a server-side polling switchQ9
CollectionT1056.001 — Input Capture: KeyloggingLogs Windows keystrokes via a temporary PowerShell script, gated by the same polling patternQ8
Command and ControlT1102.001 — Web Service: Dead Drop ResolverBoth loader and second stage resolve their next step from a hardcoded Npoint.io JSON URL rather than a traditional hardcoded C2 domainQ4
Command and ControlT1071.001 — Application Layer Protocol: Web ProtocolsPlain HTTP POST/GET to a small set of Express routes on 103.170.217.184:8787; client polls two status routes every 5 secondsQ5
ExfiltrationT1041 — Exfiltration Over C2 ChannelHost-recon, extension storage, clipboard, and keystroke data all exfiltrate over the same HTTP C2 channelQ5

No Persistence-tactic technique is mapped. Per the primary source, WeaselBiscuit has "no persistence beyond the detached Node process and .pid file" — it does not survive a reboot on its own. This is deliberately left out rather than force-mapped to a Persistence ID that isn't supported by the evidence.

07

Native Audit-Log Hunts

The largest coverage gaps in this pack — SaaS-hosted CI compute, the pre-eval Base64/new Function step, and the pre-import registry-side detection problem (§5) — have no Falcon-sensor answer at all. These native, non-CQL checks are the only available coverage for those surfaces, and a useful supplement everywhere else.

npm / dependency-tree audit (all platforms)

  • Run an SCA scan (Socket.dev, Snyk, npm audit equivalent, or an internal registry-side scanner) across every repository's package.json/package-lock.json/yarn.lock for the 17 named packages in §10 — by name and, where known, version.
  • Because there is no lifecycle hook to gate, npm config get ignore-scripts auditing (useful against other npm stealers) provides no protection against WeaselBiscuit specifically — do not rely on it as a compensating control here.
  • Grep the dependency tree for any package whose index.js or top-level module unconditionally spawns a detached child process on import — a generic static-analysis heuristic that would catch this family and future clones of it.

Windows Endpoint Security / Sysmon (where Falcon FileOpenInfo/FileReadInfo licensing is unavailable)

  • Sysmon Event ID 11 (FileCreate) / 15, filtered for *kb-monitor\keyboard-monitor-*.ps1 under any user's %TEMP%.
  • Sysmon Event ID 1 (ProcessCreate), filtered for a node.exe parent spawning powershell.exe with Get-Clipboard in the command line.

Linux / macOS endpoint (auditd / Endpoint Security Framework)

  • Linux auditd watch rule on the Chrome extension-settings path, independent of Falcon licensing for FileOpenInfo/FileReadInfo:
    -w /home/*/.config/google-chrome/*/Local\ Extension\ Settings -p r -k weaselbiscuit_ext_read
  • macOS Endpoint Security Framework file-open events on the equivalent Chrome profile path, or as a fallback: log show --predicate 'eventMessage contains "Local Extension Settings"' --last 1h
  • macOS: watch for a node-parented pbpaste child process (ps -ef | grep pbpaste spot-check, or an ESF process-exec hook) — the source material confirms pbpaste as the macOS clipboard-capture mechanism but names no Linux-specific clipboard tool, so Linux clipboard capture is unconfirmed by any cited source and should not be assumed present.

Filesystem spot-check for the .pid marker (all platforms)

  • No generic Falcon file-write event maps cleanly to an arbitrary non-executable file write, so this is a manual/RTR check rather than a CQL query: search under every node_modules/<package>/ directory matching the §10 package list for a .pid file, and correlate its contents (a PID) against currently running or recently-exited node processes.
08

CrowdStrike LogScale CQL Hunt Queries

Pick your tenant's cloud first — every "Open in Falcon" button below uses this selection.
Q1 · node executes from a confirmed, directly-linked WeaselBiscuit package path
CONF HIGHFP LOWCOST LOW

Looks for: node executing a script from inside node_modules/process-tailwind/, node_modules/engin1/, node_modules/swnwall/, or node_modules/id79-client/ — the four names with an individually-confirmed direct Npoint-resolver delivery link. FP: near-zero; these path segments are attacker-registered package names.

// HUNT: node executes from a confirmed, directly-linked WeaselBiscuit package path
// MITRE: T1195.001, T1059.007
// CONF: high  FP: low  COST: low
// REQUIRES: ProcessRollup2 or SyntheticProcessRollup2
// FALSE POSITIVES: none expected -- these are attacker-registered package names; a hit would only be benign if your org vendors software that also depends on an identically-named unrelated package (verify before treating as benign)
// LOOKBACK: 30d (set via the Falcon time picker, not an in-query filter)
#event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/
| FileName=/^node(\.exe)?$/i
| CommandLine = *node_modules/process-tailwind/* or CommandLine = *node_modules\process-tailwind\* or CommandLine = *node_modules/engin1/* or CommandLine = *node_modules\engin1\* or CommandLine = *node_modules/swnwall/* or CommandLine = *node_modules\swnwall\* or CommandLine = *node_modules/id79-client/* or CommandLine = *node_modules\id79-client\*
| table([@timestamp, ComputerName, UserName, CommandLine, ParentBaseFileName])
Q2 · node executes from a broader WeaselBiscuit-clustered package path (shared-IOC grouping only)
CONF MEDIUMFP MEDIUMCOST LOW

Looks for: node executing from the thirteen remaining names the researcher's automation clustered by shared IOCs but did not individually confirm with a direct resolver link. FP: low-medium — several of these (process-mite, process-lhpm, generic @biz44/* scope names) are generic enough that an unrelated internal package could coincidentally share a path fragment.

// HUNT: node executes from a WeaselBiscuit-clustered package path (grouped by shared IOCs, not individually confirmed)
// MITRE: T1195.001, T1059.007
// CONF: medium  FP: medium  COST: low
// REQUIRES: ProcessRollup2 or SyntheticProcessRollup2
// FALSE POSITIVES: process-mite, process-lhpm, and the bare @biz44 scope are generic-sounding names; confirm the full CommandLine path and package.json contents before treating a hit as confirmed malicious
// TUNING: cross-reference any hit against npm registry metadata for the exact package (maintainer account, publish date, download count) before escalating -- this query trades precision for recall across the wider, less-confirmed name cluster
#event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/
| FileName=/^node(\.exe)?$/i
| CommandLine = *node_modules/laycot/* or CommandLine = *node_modules\laycot\* or CommandLine = *node_modules/process-mite/* or CommandLine = *node_modules\process-mite\* or CommandLine = *node_modules/process-lhpm/* or CommandLine = *node_modules\process-lhpm\* or CommandLine = *node_modules/@biz44/* or CommandLine = *node_modules\@biz44\* or CommandLine = *node_modules/@vibecheck-polid/* or CommandLine = *node_modules\@vibecheck-polid\* or CommandLine = *node_modules/@railone/* or CommandLine = *node_modules\@railone\*
| table([@timestamp, ComputerName, UserName, CommandLine, ParentBaseFileName])
Q3 · Detached node loader spawn (loader.js) from npm/node — campaign-agnostic behavioral hunt
CONF MEDIUMFP MEDIUMCOST LOW

Looks for: a node/npm process spawning a child node loader.js process — the generic first-stage shape that survives any future package-name or Npoint-UUID rotation. FP: some legitimate build tooling and dev servers name their entry file loader.js.

// HUNT: npm/node spawns a child node process running a file literally named loader.js (WeaselBiscuit's first-stage entry point)
// MITRE: T1059.007
// CONF: medium  FP: medium  COST: low
// REQUIRES: ProcessRollup2 or SyntheticProcessRollup2
// FALSE POSITIVES: some legitimate build tools, webpack loaders, and dev-server scaffolds also name an entry file loader.js; this query is a triage feed, best correlated with Q1/Q2 package-path hits or Q4/Q5 network hits on the same host
// TUNING: exclude ParentCommandLine values matching your organization's known build-tool invocations (webpack, vite, esbuild dev-server wrappers) once a benign baseline is established
#event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/
| ParentBaseFileName=/^(node|npm|npm-cli\.js|npx)(\.exe|\.cmd)?$/i
| FileName=/^node(\.exe)?$/i
| CommandLine = *loader.js*
| table([@timestamp, ComputerName, UserName, ParentBaseFileName, ParentCommandLine, FileName, CommandLine])
Q4 · node/npm process resolves the Npoint.io dead-drop domain
CONF MEDIUMFP HIGHCOST LOW

Looks for: a node/npm-parented DNS resolution of api.npoint.io, the dead-drop service both the loader and second stage use for every configuration fetch. FP: high — Npoint.io is a legitimate free JSON-hosting service some developers use for mock APIs; this query cannot distinguish a specific malicious UUID from a benign one at the DNS layer alone. Falcon's DNS telemetry does not carry the resolved URL path, so the exact Npoint UUID (§4/§10) requires an HTTP proxy/DLP log, not CQL — a real coverage gap, not an oversight.

// HUNT: node/npm process resolves api.npoint.io, the Npoint.io dead-drop service used for every WeaselBiscuit config fetch
// MITRE: T1102.001, T1071.001
// CONF: medium  FP: high  COST: low
// REQUIRES: DnsRequest with ContextBaseFileName -- ⚠ validate in tenant: confirm DnsRequest carries ContextBaseFileName in your sensor version/cloud before relying on this
// FALSE POSITIVES: api.npoint.io is a legitimate free JSON-hosting service; some developers legitimately use it for mock APIs or small config stores unrelated to WeaselBiscuit
// TUNING: this is a triage feed, not a standalone alert -- escalate only hits also matching Q1/Q2/Q3 on the same host, or correlate with an HTTP proxy log to recover the specific Npoint UUID path against §10's known-bad list
#event_simpleName=DnsRequest
| ContextBaseFileName=/^(node|npm|npm-cli\.js|npx)(\.exe|\.cmd)?$/i
| DomainName=/^api\.npoint\.io$/i
| table([@timestamp, ComputerName, UserName, ContextBaseFileName, DomainName])
Q5 · Direct match against the confirmed shared WeaselBiscuit C2
CONF HIGHFP LOWCOST LOW

Looks for: outbound connections to the confirmed shared C2, 103.170.217.184:8787. FP: low — a dedicated, non-cloud-hosted IP:port pair with no other documented legitimate purpose, though it remains single-primary-sourced (see §2).

// HUNT: direct match against the confirmed shared WeaselBiscuit C2 (103.170.217.184:8787)
// MITRE: T1071.001, T1041
// CONF: high  FP: low  COST: low
// REQUIRES: NetworkConnectIP4
// FALSE POSITIVES: none expected -- dedicated attacker infrastructure with no other documented purpose; single-primary-sourced (see §2), so treat as high-confidence DETECT, not as grounds for automatic isolation without analyst review
#event_simpleName=NetworkConnectIP4
| RemoteAddressIP4="103.170.217.184"
| RemotePort=8787
| table([@timestamp, ComputerName, UserName, RemoteAddressIP4, RemotePort, ContextBaseFileName])
Q6 · Nested public-IP + geolocation fingerprint chain (ipify → ip-api) from node
CONF MEDIUMFP MEDIUMCOST LOW

Looks for: a node-parented DNS resolution of either api.ipify.org or ip-api.com — the two-step fingerprint lookup the researcher calls a DPRK tradecraft signal. FP: both services are legitimate and used by unrelated dev tooling (VPN clients, dynamic-DNS updaters); this query is a corroborating signal, best paired with a Q1-Q5 hit on the same host and time window, not a standalone alert.

// HUNT: node-parented DNS resolution of api.ipify.org or ip-api.com (nested public-IP + geolocation fingerprint)
// MITRE: T1016.001, T1614.001
// CONF: medium  FP: medium  COST: low
// REQUIRES: DnsRequest -- ⚠ validate in tenant: ContextBaseFileName availability on DnsRequest (see Q4)
// FALSE POSITIVES: both api.ipify.org and ip-api.com are legitimate third-party services also called by unrelated dev tooling, VPN clients, and some CLI utilities
// TUNING: fire only when correlated with a Q1/Q2/Q3 hit on the same aid within a short window; do not alert on this query alone
#event_simpleName=DnsRequest
| ContextBaseFileName=/^(node|npm|npm-cli\.js|npx)(\.exe|\.cmd)?$/i
| DomainName=/^(api\.ipify\.org|ip-api\.com)$/i
| table([@timestamp, ComputerName, UserName, ContextBaseFileName, DomainName])
Q7 · node-spawned process reads Chrome's Local Extension Settings store
CONF MEDIUMFP MEDIUMCOST LOW

Looks for: a node process opening a file under a Chrome profile's Local Extension Settings directory — the LevelDB store WeaselBiscuit uploads wholesale. FP: medium — browser-automation tooling (Puppeteer/Playwright) and some dev-focused browser extensions legitimately touch this path from a node-spawned process.

// HUNT: node-spawned process opens a file under Chrome's Local Extension Settings LevelDB directory
// MITRE: T1005
// CONF: medium  FP: medium  COST: low
// REQUIRES: FileOpenInfo or FileReadInfo -- ⚠ validate in tenant: event availability and exact field set (TargetFileName, ContextBaseFileName) is sensor-version/license dependent; confirm a sample event before relying on this in production
// FALSE POSITIVES: Puppeteer/Playwright browser-automation frameworks and some developer-focused browser extensions legitimately read files under this path from a node-spawned process
// TUNING: exclude ContextBaseFileName/ParentCommandLine values for known Puppeteer/Playwright automation harnesses in your CI fleet; this query is highest-value when correlated with a Q1-Q5 hit on the same host
#event_simpleName=/FileOpenInfo|FileReadInfo/
| TargetFileName="*Local Extension Settings*"
| ContextBaseFileName=/^node(\.exe)?$/i
| table([@timestamp, ComputerName, UserName, ContextBaseFileName, TargetFileName])
Q8 · Windows keylogger temp PowerShell script (kb-monitor) spawned by node
CONF HIGHFP LOWCOST LOW

Looks for: node spawning powershell/pwsh with kb-monitor in the command line — the confirmed Windows keylogger temp-script pattern, independently verified in both the primary write-up and the raw sample text. FP: near-zero; this exact temp-directory naming is highly specific.

// HUNT: node spawns powershell/pwsh with the confirmed WeaselBiscuit kb-monitor keylogger temp-script path in the command line
// MITRE: T1056.001
// CONF: high  FP: low  COST: low
// REQUIRES: ProcessRollup2 or SyntheticProcessRollup2
// FALSE POSITIVES: none expected -- kb-monitor / keyboard-monitor-*.ps1 under %TEMP% is a highly specific, non-standard naming pattern with no known legitimate use
#event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/
| ParentBaseFileName=/^node(\.exe)?$/i
| FileName=/^(powershell|pwsh)(\.exe)?$/i
| CommandLine = *kb-monitor*
| table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine])
Q9 · node spawns a clipboard-capture tool (powershell/pwsh/pbpaste) — broad triage feed
CONF MEDIUMFP HIGHCOST LOW

Looks for: any node-spawned powershell/pwsh/pbpaste process — deliberately broad because WeaselBiscuit's clipboard command triggers Get-Clipboard with no other distinguishing command-line argument, and pbpaste on macOS legitimately takes no arguments at all. FP: high — many legitimate npm scripts and Electron-based dev tools shell out to PowerShell or pbpaste. This is a hunt-only triage feed.

// HUNT: node spawns powershell, pwsh, or pbpaste -- broad triage feed for the WeaselBiscuit clipboard-capture module
// MITRE: T1115
// CONF: medium  FP: high  COST: low
// REQUIRES: ProcessRollup2 or SyntheticProcessRollup2
// FALSE POSITIVES: many legitimate npm build scripts and Electron-based developer tools shell out to PowerShell on Windows; pbpaste has few callers on macOS but is not unique to WeaselBiscuit
// TUNING: filter the results for CommandLine containing "Get-Clipboard" (Windows) as the strongest sub-signal, or for any pbpaste child of node with no other legitimate automation context on that host; do not alert on this query's raw output alone
#event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/
| ParentBaseFileName=/^node(\.exe)?$/i
| FileName=/^(powershell|pwsh|pbpaste)(\.exe)?$/i
| table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine])
Q10 · Rare / first-seen domain contacted by node or npm (undiscovered dead-drop hunt)
CONF MEDIUMFP HIGHCOST HIGH

Looks for: low-count, widely-distributed domains contacted by node/npm — the shape of a not-yet-attributed alternate dead-drop service if the operator moves off Npoint.io. FP: high — new legitimate dependencies look identical. Analyst-reviewed hunt only; never automate blocking on this.

// HUNT: node/npm process resolves a rare, low-count domain -- candidate undiscovered dead-drop or C2 (run over two windows, e.g. last 24h vs. last 90d, and diff manually; do not add an in-query time filter)
// MITRE: T1102.001, T1071.001
// CONF: medium  FP: high  COST: high
// REQUIRES: DnsRequest with 90+ days retention; ContextBaseFileName validated in tenant (see Q4)
// FALSE POSITIVES: new legitimate npm dependencies, internal tooling rollouts, and one-off CDN/JSON-hosting endpoints all look identical to a new dead-drop domain under this model; this is a hunt-only, analyst-reviewed query
// TUNING: exclude your approved-registry/CDN allowlist; review only domains with RecentCount below 5 across the whole fleet AND DistinctHosts above 1 -- widespread-but-low-count-per-host is the WeaselBiscuit shape, a domain hit hundreds of times by one host is more likely legitimate build tooling
#event_simpleName=DnsRequest
| ContextBaseFileName=/^(npm|npm-cli\.js|node|npx)(\.exe|\.cmd)?$/i
| groupBy([DomainName], function=[count(as=RecentCount), count(ComputerName, distinct=true, as=DistinctHosts)], limit=5000)
| sort(RecentCount, order=asc, limit=200)
09

Custom IOA Recommendations

Three queries are strong Custom IOA candidates; the remaining seven stay Investigate/Hunt-only pending tuning, because their host service is legitimate (Npoint, ipify, ip-api), or because they are deliberately broad triage feeds.

IOA NameSource QueryPatternExclusions RequiredDeployment Path
Confirmed WeaselBiscuit Package ExecutionQ1node executing from node_modules/process-tailwind/, engin1/, swnwall/, or id79-client/None identifiedDetect immediately — near-zero FP, attacker-registered names; hold at Detect (never Prevent) per the single-primary-source rule in §2
WeaselBiscuit Known C2 MatchQ5NetworkConnectIP4 to 103.170.217.184:8787None identifiedDetect immediately — dedicated attacker infrastructure; Detect only per §2, not Prevent
WeaselBiscuit Keylogger Script SpawnQ8node spawns powershell/pwsh with kb-monitor in CommandLineNone identifiedDetect immediately with high-priority SOC paging — keystroke logging is an active-collection signal, not a passive one

Q4 (Npoint DNS) and Q10 (rare-domain hunt) are intentionally not recommended for Custom IOA promotion — both host services (Npoint.io, and the open-ended rare-domain model) are legitimately used by unrelated developer tooling. Q2, Q3, Q6, Q7, and Q9 remain Investigate/Hunt-only until a fleet-specific benign baseline is established (see §11).

10

Machine-Readable IOC Appendix

Action-tier mapping — read before bulk import. Falcon IOC Management supports only prevent, detect, allow and no_action; it has no hunt, enrich or pivot action. Those three are analyst-workflow tiers used in the §4 table, and they are encoded here as detect at reduced severity, exactly per §4. Because every indicator in this pack is single-primary-sourced (§2), nothing below ships at prevent — every row is detect, capped at high severity (never critical), regardless of how urgently the primary researcher frames the threat in prose. npm package names are not a Falcon IOC-Management importable type and are shipped separately below, not in this CSV.

Falcon IOC Management CSVbulk import
type,value,action,severity,expiration,description,tags
ipv4,103.170.217.184,detect,high,2026-12-20,Shared WeaselBiscuit HTTP C2 -- polling Express API on TCP/8787 -- single-sourced (OpenSourceMalware),campaign:WeaselBiscuit
sha256,7b15605f23b131b3eeea57e031ae7cb32fc4b78c7bbb2025aa7a561ea5ae5159,detect,high,2026-12-20,Confirmed WeaselBiscuit second-stage infostealer payload -- delivered via Npoint dead-drop + Base64 + new Function -- single-sourced,campaign:WeaselBiscuit
url,https://api.npoint.io/24c25d5f5fcbb0992a4f,detect,high,2026-12-20,WeaselBiscuit Npoint resolver -- confirmed process-tailwind@1.1.99 delivery -- embedded stage identifier 99,campaign:WeaselBiscuit
url,https://api.npoint.io/641d37178a880b1e8b8f,detect,high,2026-12-20,WeaselBiscuit Npoint resolver -- confirmed swnwall@1.2.10 delivery -- embedded stage identifier 10,campaign:WeaselBiscuit
url,https://api.npoint.io/33e8d008c334b060adad,detect,high,2026-12-20,WeaselBiscuit Npoint resolver -- confirmed clone payload -- embedded stage identifier 79,campaign:WeaselBiscuit
url,https://api.npoint.io/ddae72efbb6714fae922,detect,high,2026-12-20,WeaselBiscuit Npoint resolver -- confirmed clone payload -- embedded stage identifier 12,campaign:WeaselBiscuit
url,https://api.npoint.io/933a731a5e97f4b45249,detect,high,2026-12-20,WeaselBiscuit Npoint resolver -- confirmed clone payload -- embedded stage identifier 95,campaign:WeaselBiscuit
url,https://api.npoint.io/37c0a0c68bf7a94ed731,detect,high,2026-12-20,Shared WeaselBiscuit C2-configuration resolver -- all five confirmed stages retrieve C2 103.170.217.184:8787 from this URL,campaign:WeaselBiscuit
url,https://api.npoint.io/24c12c4b66a29747764f,detect,low,2026-10-20,Hard-coded fallback resolver referenced in id79-client -- returned HTTP 404 at research time -- embedded identifier unconfirmed (see report §2),campaign:WeaselBiscuit
Behavioral Signatureshunt heuristics
# Not literal IOCs -- paste into a hunt runbook or SOAR playbook as review criteria
1. npm package has NO preinstall/install/postinstall lifecycle hook and instead auto-runs
   a detached child "node loader.js" process the moment it is imported/required -- see Q3
2. Detached loader retrieves JSON from a hardcoded api.npoint.io/<uuid> URL, Base64-decodes
   a "code" field, and executes it via new Function() -- payload never touches disk
3. Second stage retrieves its C2 config from a SECOND Npoint URL -- watch for any process
   making two sequential api.npoint.io requests moments apart -- see Q4
4. A .pid file appears directly inside the importing package's own node_modules directory
   (not a project-root PID file) -- manual/RTR filesystem check, see section 7
5. Client polls /api/clipboard-status/<hostname> and /api/keyboard-mouse-status/<hostname>
   every ~5 seconds -- both return {"isMonitoring": false} until the operator flips a switch
6. Exfiltration route pattern confirmed in the recovered sample: /api/system-info,
   /api/upload-local-extension-settings, /api/clipboard-data, /api/keyboard-mouse-data
   (all on the C2 host -- see Q5)
7. Numeric identifier field ("identifier": "10"/"12"/"79"/"95"/"99") present in uploaded
   system-info and extension-storage multipart form data -- confirmed at the payload level;
   useful as a post-hoc pivot/grouping key, not as a standalone detection
8. npm account / package-naming pattern: short, generic-sounding utility names (engin1,
   process-mite, laycot) or a numbered scope (@biz44/id10-client .. id99-client) with little
   or no legitimate publishing history -- registry-side signal, not endpoint telemetry
Named Malicious npm Packagesregistry-side pivot
# Confirmed direct Npoint-resolver delivery link (highest confidence, 4 names):
process-tailwind@1.1.99      (ID-99 Client Module -- byte-identical to engin1)
engin1@1.3.99                (ID-99 Client Module -- byte-identical to process-tailwind)
swnwall@1.2.10                (ID-10 Client Module)
id79-client@1.1.79            (ID-79 Client Module)

# Clustered by shared IOCs / researcher automation (medium confidence, 13 names):
laycot@1.3.10
process-mite
process-lhpm
@biz44/id10-client
@biz44/id12-client
@biz44/id44-client
@biz44/id79-client
@biz44/id95-client
@biz44/id99-client
@biz44/process-runtime-utils
@biz44/runtime-utils
@vibecheck-polid/process-runtime-utils
@railone/image-utils

# 17 names total (union across all cited sources -- see report section 2 for the
# 11 vs. 13 vs. 16 vs. 17 count discrepancy across and within the primary sources).
# Screen your dependency tree against this list via your SCA tool of choice
# (Socket.dev, Snyk, npm audit equivalent, or an internal registry-side scanner).
Dependency & Runtime Audit Checklistpaste into runbook
[ ] SCA scan of every repo's package.json/package-lock.json/yarn.lock against the
    17-name list above (Named Malicious npm Packages block)
[ ] Pre-merge dependency-review gate requires human sign-off on any brand-new
    or low-download-count npm package before it reaches application code
[ ] NOTE: npm config set ignore-scripts=true provides NO protection against this
    threat -- WeaselBiscuit has no lifecycle hook, it runs on import/require
[ ] Fleet-wide search for a .pid file directly inside a node_modules/<pkg>/ directory
[ ] Egress allowlist review: is 103.170.217.184 reachable from dev/CI networks today?
[ ] Chrome profile "Local Extension Settings" directory permissions reviewed on
    workstations that also hold cryptocurrency wallet browser extensions
[ ] CI/CD runner egress allowlisted (self-hosted); SaaS-hosted runner gap documented
11

Detection Validation Gates

GateRequirement
Telemetry readyFalcon sensor deployed to developer workstations and every self-hosted CI runner image; ProcessRollup2, DnsRequest and NetworkConnectIP4 confirmed flowing. Confirm FileOpenInfo/FileReadInfo licensing and field availability in-tenant before relying on Q7.
Benign baselineRun Q2, Q3, Q4, Q6, Q9 and Q10 in Detect/hunt-only mode for 7-14 days; catalog build-tool, browser-automation, and legitimate PowerShell/pbpaste hits before adding any exclusion to a Prevent-eligible query (there currently are none — see §2).
Positive testsValidate Q1/Q3/Q8 fire using a disposable, lab-only npm package that mimics the loader/keylogger-script pattern in an isolated VM/sandbox — never test against the actual WeaselBiscuit C2 infrastructure or any of the recovered live samples. Validate Q5 against a replayed lab PCAP containing the known-bad IP:port.
Promotion criteriaQ1, Q5 and Q8 → Detect immediately (§9); none of the three are promoted to Prevent while this pack remains single-primary-sourced (§2) — re-evaluate only after independent corroboration. Q2, Q3, Q4, Q6, Q7, Q9, Q10 remain Investigate/Hunt-only pending a fleet-specific benign baseline.
12

Hardening — Tiered

Read this before reusing another npm-stealer pack's hardening list against this threat. WeaselBiscuit has no npm lifecycle hook — the code runs when the package is imported/required by application code, not when npm install runs. npm config set ignore-scripts true, the single most effective control against a preinstall/postinstall-hook stealer, does nothing against WeaselBiscuit. Controls below are chosen accordingly: the leverage point is stopping the package from ever entering the dependency tree, plus runtime/network detection of the behavior that follows import.

Immediate — this week

1. Require a pre-merge SCA/dependency-review gate for any new or updated npm dependency before it can reach application code that will actually run (not just pass CI's install step). MITRE M1013 (Application Developer Guidance), M1033 (Limit Software Installation). Authority: NIST SP 800-218 (SSDF) practice PW.4; OpenSourceMalware's own recommendation to screen the dependency tree against the named-package list (§10).
Verify: a PR that adds a net-new npm dependency cannot merge without a passing SCA scan and a human reviewer sign-off; spot-check by attempting to add one of the §10 package names in a test branch.
Rollback: revert the branch-protection rule requiring the SCA check to pass before merge — only do this if the gate is blocking unrelated legitimate work, and fix the gate's false-positive rate instead of disabling it where possible.

2. Default-deny egress allowlisting for developer workstations and CI runners, at minimum blocking 103.170.217.184. MITRE M1037 (Filter Network Traffic). Authority: NIST SP 800-204C; CIS Benchmark network egress-control guidance for the self-hosted case.
Verify: curl -m 5 http://103.170.217.184:8787 from a workstation/runner times out or is denied; review firewall/VPC Flow Logs for the denied attempt.
Rollback: remove the specific deny rule if it collides with an unrelated legitimate destination sharing infrastructure — unlikely for a dedicated non-cloud-hosted IP, but verify ownership before assuming otherwise.

Near term — 1 to 4 weeks, pilot first

3. Run Q1-Q3 and Q8 in Detect/hunt-only mode fleet-wide for a 14-day burn-in, reviewed by two analysts before any promotion decision. MITRE M1040 (Behavior Prevention on Endpoint). Authority: this pack's own Detection Validation Gates (§11); no formal CIS/vendor benchmark exists specifically for "detached Node child process behavior," so this control is grounded in the pipeline's own validated-query methodology rather than an external benchmark. ⚠ best-practice, no formal CIS/MITRE-mitigation entry specific to this exact behavior pattern
Verify: Falcon shows Q1/Q3/Q8 running on schedule with logged hit counts for the burn-in window; a two-analyst sign-off record exists before any query is promoted.
Rollback: disable the scheduled search if it generates unmanageable noise during burn-in — re-tune the TUNING exclusions (§8) rather than abandoning the hunt entirely.

4. Restrict file-system permissions on Chrome's Local Extension Settings directory on workstations known to hold cryptocurrency wallet browser extensions, and/or segregate wallet-extension use into a dedicated browser profile isolated from general development/testing browsing. MITRE M1022 (Restrict File and Directory Permissions). Authority: ⚠ best-practice, no formal CIS/MITRE-mitigation entry specific to browser-extension LevelDB store permissions — grounded in the primary source's own observation that this directory's contents are "financially relevant" even without dedicated wallet-draining code.
Verify: a non-owner OS account cannot read the target Chrome profile's Local Extension Settings directory; confirm via a permissions audit script appropriate to the OS (icacls on Windows, ls -l/stat on macOS/Linux).
Rollback: revert to the OS default permissions on that directory if the restriction breaks legitimate multi-user browser profile sync or an IT-managed browser-extension deployment tool.

Strategic — 1 to 3 months

5. Evaluate sandboxed/ephemeral dev containers or VMs for evaluating any newly-added third-party npm dependency before it ever touches a developer's primary workstation, CI secrets, or a wallet-bearing browser profile. MITRE M1048 (Application Isolation and Sandboxing). Authority: NIST SP 800-218 (SSDF) PW.4; general container/VM-isolation guidance for untrusted code evaluation (no npm-specific CIS benchmark covers this directly).
Verify: a test dependency-evaluation workflow runs entirely inside an ephemeral container/VM with no access to the developer's real credentials, wallet extensions, or persistent filesystem; confirm the container is destroyed after evaluation.
Rollback: revert to direct workstation-based dependency evaluation if the sandboxing workflow proves too slow for the team's release cadence — prioritize this control for the highest-risk repos (those touching production secrets or crypto-adjacent code) first if a full rollout isn't feasible.

6. Train developers on npm supply-chain tradecraft signals specific to this family: short, generic-sounding utility package names with little publishing history, numbered/scoped package families (e.g. @biz44/id10-client .. id99-client), and packages that spawn a detached background process on import with no clear product justification. MITRE M1017 (User Training).
Verify: a tabletop exercise asking developers to evaluate a mocked-up package listing (mixing real and simulated WeaselBiscuit-style names) against real popular packages measures catch rate.
Rollback: none required — training/process control with no endpoint state change.

13

Deployable Playbooks

Playbook A — Pre-Merge Dependency Vetting & C2 Egress Containment

MITRE: T1195.001, T1071.001 → M1013, M1033, M1037   Estimated deploy time: 2-4 hours for the egress rule; 1-2 days to wire the SCA gate into CI branch protection   Prerequisites: CI platform admin rights for branch-protection rules; an SCA tool (Socket.dev, Snyk, or equivalent) already licensed or evaluable; cloud/firewall admin access for egress rules   Reboot required: No

Step 1 — Add the 17 known package names to your SCA tool's custom denylist

What it does: Fails an SCA scan (and therefore the merge gate) the moment any of the confirmed WeaselBiscuit-associated package names appears in a lockfile. Why it works: stops the package from ever entering the dependency tree — the only point where WeaselBiscuit can be prevented outright, since it has no install-time hook to block later.

# Example: Socket.dev / Snyk-style custom policy denylist (adapt to your tool's config format)
denylist:
  - laycot
  - process-mite
  - process-lhpm
  - process-tailwind
  - engin1
  - swnwall
  - id79-client
  - "@biz44/id10-client"
  - "@biz44/id12-client"
  - "@biz44/id44-client"
  - "@biz44/id79-client"
  - "@biz44/id95-client"
  - "@biz44/id99-client"
  - "@biz44/process-runtime-utils"
  - "@biz44/runtime-utils"
  - "@vibecheck-polid/process-runtime-utils"
  - "@railone/image-utils"

Verify: a test branch that adds any denylisted name to package.json fails the SCA check and cannot merge.

Rollback: remove the denylist entries if your tool's policy engine conflicts with an unrelated legitimate internal package that happens to share a name — verify no false collision exists before rolling back rather than disabling the whole gate.

Step 2 — Block the confirmed C2 IP at the network egress layer

What it does: Denies outbound connectivity to 103.170.217.184 from developer and CI networks. Why it works: even if a compromised package is imported and runs, the exfiltration and second-stage-config fetch cannot reach the operator.

# Example: cloud security-group egress deny (adapt to your firewall/VPC tooling)
aws ec2 authorize-security-group-egress --group-id sg-0123456789abcdef0 \
  --protocol tcp --port 8787 --cidr 103.170.217.184/32 --rule-action deny

# Or on a traditional perimeter firewall, an explicit deny rule ordered ABOVE any broad allow:
# deny tcp any any -> 103.170.217.184 8787

Verify: curl -m 5 http://103.170.217.184:8787 from an affected network times out; firewall logs show the denied attempt.

Rollback: remove the specific deny rule if it is later confirmed to collide with unrelated infrastructure sharing that IP — verify current ownership before assuming that risk, since this is a small non-cloud-hosted IP with no other documented legitimate use as of this pack's build date.

Playbook B — Runtime Burn-In for Detached Node Child Processes

MITRE: T1059.007, T1005 → M1040, M1022   Estimated deploy time: 1-2 hours to schedule the searches; 14 days for the burn-in window itself   Prerequisites: Falcon Investigate/Scheduled Search access; two analysts available to review daily hit output   Reboot required: No

Step 1 — Schedule Q1, Q3, and Q8 as Falcon Scheduled Searches at Detect-only

What it does: Runs the confirmed-package, detached-loader, and keylogger-script queries on a recurring schedule and routes hits to a review queue rather than an auto-response action. Why it works: establishes a real fleet-specific benign baseline before any query is considered for further tuning, without risking a false isolation action on legitimate developer tooling.

# Falcon Next-Gen SIEM / Scheduled Search (conceptual -- configure via the Falcon console
# Investigate > Scheduled Searches UI, using the Q1/Q3/Q8 CQL from section 8 as the query body)
Schedule:    Every 6 hours
Query:       
Action:      Log to review queue -- NO automated containment action during burn-in
Retention:   30 days of hit history for baseline comparison

Verify: the Scheduled Search shows a run history with hit counts for each 6-hour window; a review-queue entry exists for every hit during the burn-in period.

Rollback: disable the Scheduled Search if hit volume is unmanageable for the review team — reduce scope (e.g., to a pilot ring of hosts) rather than abandoning the burn-in entirely.

Step 2 — Two-analyst review before any promotion decision

What it does: Requires a second analyst to independently confirm each candidate true-positive before a query is proposed for Custom IOA promotion. Why it works: matches this pack's own Detection Validation Gates (§11) and avoids a single analyst's misjudgment promoting a query that later floods the SOC with false positives.

# Review checklist per hit (paste into your ticketing/SOAR tool)
[ ] CommandLine matches an EXACT confirmed package path (Q1) or loader.js/kb-monitor
    pattern (Q3/Q8), not a substring collision
[ ] No known legitimate build tool, browser-automation harness, or internal package
    explains the hit
[ ] Second analyst independently reaches the same true/false-positive conclusion
[ ] Decision and rationale logged before any promotion to Custom IOA

Verify: every review-queue entry has a logged two-analyst decision before the 14-day window closes.

Rollback: none required — this is a review process control with no endpoint state change.

14

Containment Runbook

PhaseActionsOwnerEvidence
IsolateNetwork-isolate the affected host via Falcon RTR "contain" to stop further C2 polling and exfiltrationIR/SOCFalcon detection ID, host AID, timestamp of first Q3/Q4/Q5 hit
IsolateIdentify and kill the detached node loader.js (or downstream) process using the PID recorded in the .pid marker file under the affected node_modules/<package>/ directoryIR/Endpoint.pid file contents, process-tree snapshot at time of containment
IsolateIf a wallet-bearing browser profile is present on the host, treat any credentials/signing state accessible via that profile's extensions as potentially exposed and coordinate with the asset owner before any wallet-related actionIR/Asset OwnerList of installed extensions in the affected Chrome profile, timestamp of Q7 hit if present
EradicateSearch the organization-wide dependency tree (all repos, all lockfiles) for any of the 17 named packages (§10) and remove themAppSec/DevSCA scan results, PR diffs removing the dependency
EradicatePurge npm/yarn/pnpm caches on the affected host (npm cache clean --force, remove ~/.npm and the affected node_modules) to eliminate any cached malicious tarballIT/EndpointCache-clear command output
EradicateRemove the temporary keylogger script directory (%TEMP%\kb-monitor\) and any leftover keyboard-monitor-*.ps1 files on Windows hostsIT/EndpointDirectory listing before/after removal
EradicateRe-image or fully re-provision the affected workstation/runner if the second-stage payload is confirmed to have executed, rather than trusting in-place cleanup of an in-memory-only implantIT/EndpointRe-image ticket, asset tag
RecoverRotate any credentials, API tokens, or session state that were reachable via the affected host's Chrome profile extensions or clipboard during the estimated compromise windowIdentity/PlatformRotation/revocation timestamps or audit-log entries
RecoverRe-enable the host/runner only after confirming the pre-merge SCA gate (Playbook A) covers the repository that introduced the package, and the C2 egress block is in placeAppSec/DevOpsConfig audit output
RecoverMonitor the recovered host/runner for 30 days with Q1, Q3, Q5, and Q8 in heightened, lower-threshold reviewSOC30-day watch-list entry, daily query hit review log
15

Detection Coverage Map

TechniqueBehaviorCQLIOACoverage
T1195.001Malicious npm package imported/required into application codeQ1, Q2Q1 candidatePartial — the import trigger itself has no install-time EDR signal; covered only by named-package path matching, which is blind to any not-yet-named clone
T1059.007 / T1620Detached node loader + in-memory new Function executionQ3NonePartial for the loader spawn; GAP for the eval step itself — no distinct EDR event maps to reflective in-memory code loading
T1102.001 / T1071.001Npoint dead-drop resolution and HTTP C2 polling/exfiltrationQ4, Q5, Q10Q5 candidateGood for the known C2 IP:port; Partial for Npoint (legitimate host, high FP) and for undiscovered infrastructure (Q10 hunt-only)
T1016.001 / T1614.001Nested public-IP + geolocation fingerprintQ6NonePartial — both third-party services are legitimate; corroborating signal only
T1005Chrome Local Extension Settings LevelDB theftQ7NonePartial — field availability is sensor/license dependent, and SaaS-hosted CI has zero coverage
T1056.001Windows keylogger via temp PowerShell scriptQ8Q8 candidateGood
T1115Clipboard capture (Get-Clipboard / pbpaste)Q9NonePartial — deliberately broad triage feed; no Linux clipboard mechanism confirmed by any source

See §11 for the full Detection Validation Gates this coverage assumes: telemetry readiness, a 7-14 day benign baseline, lab-only positive testing, and the specific promotion criteria for each query.

16

Hunt Summary Ticket

TITLE:      WeaselBiscuit -- npm-Distributed JavaScript Infostealer
SEVERITY:   HIGH  (single-primary-sourced -- see section 2; not shipped at CRITICAL
            despite the primary researcher's own "treat as critical" framing)
SCOPE:      Developer workstations (Win/macOS/Linux) + CI/CD runners executing
            application code that imports a compromised dependency (NOT merely
            npm install -- this threat has no lifecycle hook). SaaS-hosted CI
            compute OUT OF EDR SCOPE -- see native audit-log hunts, section 7.
HYPOTHESIS: A detached node child process (loader.js) is retrieving a Base64
            payload from a hardcoded Npoint.io dead-drop and executing it in
            memory via new Function, which then beacons to 103.170.217.184:8787
            and harvests Chrome extension storage, and on operator command,
            clipboard and Windows keystrokes.
QUERIES:    Q1-Q10 (section 8) -- Q1/Q5/Q8 are Custom IOA candidates, Detect only.
DO FIRST:   1. SCA scan every repo against the 17-name package list (section 10).
            2. Run Q5 and Q8 immediately at Detect -- near-zero cost, known-good.
            3. Block 103.170.217.184 at the network egress layer (Playbook A).
GAPS:       In-memory new Function eval step (no distinct EDR event, section 6);
            SaaS-hosted CI runner coverage (section 5); Npoint UUID-path matching
            requires HTTP proxy/DLP logs, not available via Falcon DNS telemetry
            alone (section 8, Q4); Linux clipboard mechanism unconfirmed by any
            cited source.
ACTIONS:    Deploy hardening controls 1-2 (section 12) this week; brief AppSec on
            the full package list (section 10) for SCA screening; note explicitly
            to defenders that ignore-scripts does NOT mitigate this threat.
OWNER:      SOC / AppSec (joint) -- see section 14 for per-phase ownership
VERSION:    v0.1 -- 2026-09-20
17

Changelog

v0.12026-09-20Initial pack. Built from OpenSourceMalware's 2026-09-18 primary disclosure (blog + GitHub repository README, YARA rules, and a read-only text review of the deobfuscated second-stage sample) plus The Hacker News' same-day syndication. 10 CQL queries across 10 hunt hypotheses; 3 Custom IOA candidates, all capped at Detect (never Prevent) per the single-primary-source rule documented in section 2. No prompt-injection content found in any of the 5 fetched/saved sources, including the malware sample text, which was read as inert plain text only and never executed.
18

References

TierSourceUsed ForAccess Date
1OpenSourceMalware — WeaselBiscuit Strips BeaverTail and OtterCookie Down to Essentials [01]Primary disclosure: infection chain, capability list, campaign-structure table, confidence-and-gaps table, tradecraft signals2026-09-20
2The Hacker News — WeaselBiscuit Stealer Spreads via 13 npm Packages to Harvest Chrome Extension Storage [02]Press syndication of [01] with direct researcher quotes; compact IOC table; one external corroborating detail on Npoint.io usage in Contagious Interview activity generally (NVISO, Nov 2025)2026-09-20
1GitHub — OpenSourceMalware/WeaselBiscuit (README.md) [03]Same research org's canonical writeup; 16-package enumeration, per-package first-seen timestamps and versions2026-09-20
1GitHub — OpenSourceMalware/WeaselBiscuit YARA detection rules [04]Machine-readable confirmation of Npoint UUIDs, C2 IP:port, package-name strings, and numeric identifier set2026-09-20
1GitHub — OpenSourceMalware/WeaselBiscuit deobfuscated second-stage sample [05]Ground-truth text confirmation of C2 routes, kb-monitor path, Local Extension Settings path, nested ipify/ip-api calls, and the identifier field. Read as inert text only — never executed; see security note below2026-09-20

Security note on source [05]: the GitHub repository hosts a live, attacker-authored JavaScript sample as a research artifact. This pack's build process fetched that file and read it strictly as plain text for citation/verification purposes — confirming specific literal strings against the prose write-up — and at no point executed, imported, ran, or evaluated it in any interpreter or sandbox. The repository's raw (non-deobfuscated) payload file and its two image assets were not fetched, since they add no citable technical detail beyond what [01]/[03]/[04]/[05] already establish.

Prompt-injection check: all five fetched sources — the OpenSourceMalware blog [01], The Hacker News article [02], the GitHub README [03], all three YARA rule files [04], and the deobfuscated malware sample text [05] — were reviewed for embedded instructions directed at an AI agent (hidden text, HTML comments, "note to the AI," claims of user/Anthropic/administrator authorization, redirection to fetch an additional URL because "the real IOCs are here," or instructions to change this pack's output path or skip a build step). None was found in any of the five snapshots. All content in every fetched source was either ordinary site chrome/navigation, the cited research prose itself, or (for [04]/[05]) literal YARA rule syntax and JavaScript malware logic with no embedded natural-language directives aimed at an AI reader. No source attempted to redirect this research, claim authorization to skip a step, or influence a Prevent-tier IOC decision — and even if one had, this pack's design (Detect-only, single-primary-source caveat throughout) does not depend on trusting the source's own framing of its urgency.