Technical architecture reference for MUAD'DIB supply-chain threat detection tool.
For development instructions, constraints, and workflow, see CLAUDE.md.
bin/muaddib.js (yargs CLI)
└─► src/index.js — run(targetPath, options)
├─► Module Graph pre-analysis (src/scanner/module-graph/, directory of 9 files, 5s timeout)
├─► Deobfuscation pre-processing (src/scanner/deobfuscate.js)
├─► 22 parallel scanners (Promise.allSettled)
│ ├── AST scanner (src/scanner/ast.js) [timeout 45s]
│ ├── Dataflow scanner (src/scanner/dataflow.js) [timeout 45s]
│ ├── Shell scanner (src/scanner/shell.js)
│ ├── Package scanner (src/scanner/package.js)
│ ├── Dependencies scanner (src/scanner/dependencies.js)
│ ├── Obfuscation scanner (src/scanner/obfuscation.js)
│ ├── Entropy scanner (src/scanner/entropy.js) [timeout 45s]
│ ├── Typosquat scanner (src/scanner/typosquat.js)
│ ├── Python IOC scanner (src/scanner/python.js — matchPythonIOCs)
│ ├── PyPI typosquat (src/scanner/python.js — checkPyPITyposquatting)
│ ├── AI Config scanner (src/scanner/ai-config.js)
│ ├── GitHub Actions scanner (src/scanner/github-actions.js)
│ ├── Hash scanner (src/scanner/hash.js)
│ ├── IOC strings (src/scanner/ioc-strings.js, intel-triage P1.1)
│ ├── Anti-forensic AST (src/scanner/anti-forensic.js, intel-triage P1.2) [timeout 45s]
│ ├── Stub package (src/scanner/stub-package.js, intel-triage P1.3)
│ └── Monorepo scanner (src/scanner/monorepo.js, Sprint 1 audit MR-C2 fix)
├─► Deduplication
├─► FP reductions (src/scoring.js — applyFPReductions)
├─► Reachability post-processing (src/scanner/reachability.js — file-level + function-level FP downgrade)
├─► Intent coherence analysis (src/intent-graph.js — buildIntentPairs)
├─► Rule enrichment (src/rules/index.js — 278 rules)
├─► Scoring (src/scoring.js — per-file max + compound boosts)
└─► Output (CLI / JSON / HTML / SARIF)
Note: `muaddib scan` does NOT invoke the ML classifier. `src/ml/classifier.js` is
exercised only by `muaddib evaluate` (offline metric replay) and `muaddib monitor`
(LOG-ONLY since 2026-04-08 — model collapsed pending retrain, see
src/monitor/queue.js:628).
Core orchestration: src/index.js delegates to src/pipeline/{initializer,executor,processor,outputter}.js. executor.js runs cross-file module graph analysis first (pre-analysis, 5s timeout), then launches 22 individual scanners in parallel via Promise.allSettled (intel-triage P1: scanIocStrings + scanAntiForensic + scanStubPackage; Sprint 1: scanMonorepo audit MR-C2 fix; v2.11.41+: scanPythonSource PYSRC + scanPythonAst PYAST), then processor.js deduplicates, applies FP reductions + reachability post-processing, scores using per-file max (v2.2.11: riskScore = min(100, max(file_scores) + package_level_score), severity weights: CRITICAL=25, HIGH=10, MEDIUM=3, LOW=1), applies intent coherence analysis (intra-file source-sink pairing), enriches with rules/playbooks (278 rules), and outputter.js formats CLI/JSON/HTML/SARIF. Result includes warnings: [] array (v2.6.5) for incomplete scan notifications (module graph timeout/skip, deobfuscation failures). Exports isPackageLevelThreat and computeGroupScore for testing.
| Phase | Count | Modules | Mechanism |
|---|---|---|---|
| Pre-analysis | 2 | module-graph/ (directory, 9 files, 5s timeout), deobfuscate.js (passed as arg to AST + dataflow) |
Sequential, before parallel phase |
| Parallel | 22 | AST, dataflow, shell, package, dependencies, obfuscation, entropy, typosquat, python (IOC + PyPI typosquat = 2 functions), ai-config, github-actions, hash, ioc-strings (P1.1), anti-forensic (P1.2), stub-package (P1.3), monorepo, trusted-dep-diff (opt-in), python-source (PYSRC), python-ast (PYAST tree-sitter), anti-scanner-injection (ASI-001..004), binary-source (BINSRC-001, binary masquerading as source) | Promise.allSettled (executor.js) |
| Conditional / post-processing | 6 | paranoid.js (--paranoid), temporal-runner.js + temporal-analysis.js + temporal-ast-diff.js (--temporal*), reachability.js (post-processor FP downgrade), phantom-gyp.js (Phantom Gyp compound correlator, post-processor at processor.js:451) |
Skipped unless flag or post-pipeline |
| Metadata fetcher | 1 | npm-registry.js |
NPM API for age/downloads/maintainers (ML features, monitor, evaluate) |
Total: 36 .js files at src/scanner/ root + 3 directories: ast-detectors/ (15 files — the AST scanner's detector modules, split out of ast.js), module-graph/ (9 files), and python-ast-detectors/ (6 files). Of the 36: 21 files serve the 22 parallel scan functions (python.js provides 2 - matchPythonIOCs + checkPyPITyposquatting), deobfuscate.js is pre-analysis, 7 are conditional/post-processing (paranoid, 3× temporal-*, reachability, phantom-gyp, native-drop-exec), npm-registry.js is metadata, env-var-classification.js is a taint-source helper (used by dataflow.js + module-graph/), and the remaining 5 (release-zero.js, email-domain.js, pypi-maintainer.js, pypi-registry.js, pypi-release-zero.js) are monitor-side / registry-metadata scanners invoked by the daemon, not the CLI scan path. Intent coherence (src/intent-graph.js) runs in pipeline processor, not in src/scanner/.
Scanner pattern: Each of the 22 individual scanners in src/scanner/ returns Array<{type, severity, message, file}>:
filemust usepath.relative(targetPath, absolutePath)for Windows compatibility- Sync scanners are wrapped in
Promise.resolve()in the Promise.all - Use
findFiles(dir, { extensions, excludedDirs })fromsrc/utils.jsfor file walking - Size guard: skip files > 10MB via
fs.statSync
src/scanner/python.js detects Python projects by scanning requirements.txt, setup.py, and pyproject.toml. Dependencies are matched against PyPI IOCs (14K+ from OSV dump) and checked for typosquatting via Levenshtein distance with PEP 503 normalization.
src/scanner/ai-config.js scans AI agent configuration files (.cursorrules, .cursorignore, .windsurfrules, CLAUDE.md, AGENT.md, .github/copilot-instructions.md, copilot-setup-steps.yml) for prompt injection patterns. Detects shell commands, exfiltration, credential access, and injection instructions. Compound detection (shell + exfil/credentials) escalates to CRITICAL.
src/scanner/deobfuscate.js applies static AST-based deobfuscation before AST and dataflow scanners. 4 transformations: string concat folding, charcode reconstruction, base64 decode, hex array resolution. Phase 2 const propagation resolves const x = 'literal' references. Additive approach: original code scanned first (preserves obfuscation signals), then deobfuscated code adds new findings. Disable with --no-deobfuscate.
src/scanner/module-graph/ (directory of 9 files: index.js, build-graph.js, constants.js, parse-utils.js, annotate-tainted.js, annotate-sinks.js, detect-cross-file.js, detect-callback-flows.js, detect-event-flows.js) builds a dependency graph of local modules, annotates tainted exports (fs.readFileSync, process.env, os.homedir, os.hostname, os.userInfo, os.networkInterfaces, child_process, dns), and detects when credentials read in one module reach a network/exec sink in another module. Features: 3-hop re-export chain propagation, class method analysis, named export destructuring, inline require re-export, function-wrapped taint propagation. v2.6.1 additions: bounded path (MAX_GRAPH_NODES=50 at the time, raised to 5000 in v2.11.x post-audit DF-C1; MAX_GRAPH_EDGES=200 raised to 400; MAX_FLOWS=20, 5s timeout), imported sink method detection, class this.X instance taint, stream pipeline .pipe() chain following, EventEmitter cross-module detection, pipe chain cross-file flows. Runs before individual scanners. Disable with --no-module-graph.
Centralized constants in src/scanner/module-graph/constants.js + per-detector caps. Listed here for transparency since bypass via cap saturation is part of the threat model.
| Constant | Value | File:line | Role |
|---|---|---|---|
MAX_GRAPH_NODES |
5000 | module-graph/constants.js:6 |
Max files in dependency graph. Raised from 50 (v2.6.1) to 5000 in v2.11.x post-audit DF-C1 — now covers ~99.5% of npm packages. If exceeded, the graph build short-circuits and a warning is appended to scan result (warnings[]). |
MAX_GRAPH_EDGES |
400 | module-graph/constants.js:7 |
Max total import edges (prevents explosion on monorepos that still fit under node cap). |
MAX_FLOWS |
20 | module-graph/constants.js:8 |
Max cross_file_dataflow flows reported per package. Saturated flows are dropped silently in insertion order (audit DF-C2 — future fix: priority queue by severity). |
MAX_TAINT_DEPTH |
50 | module-graph/constants.js:9 |
Max AST recursion depth in the walker (DoS guard against pathologically nested minified code). |
MAX_PIPE_DEPTH |
5 | module-graph/detect-cross-file.js |
Walk depth for .pipe() chain taint following. |
MAX_EMITTER_FLOWS |
2 | module-graph/detect-event-flows.js |
Cross-file EventEmitter flow cap per package (prevents explosion on event-heavy libraries). |
| Module-graph timeout | 5 s | src/pipeline/executor.js |
Hard timeout on the whole pre-analysis phase. Emits module_graph_timeout warning if exceeded. |
| Constant | Value | File:line | Role |
|---|---|---|---|
| Source-sink proximity | 50 lines | dataflow.js:987 |
If Math.abs(src.line - sink.line) < 50 → CRITICAL ; otherwise HIGH (graduation telemetry-only). Recovered to CRITICAL when hasExposureSink && hasCredentialSource regardless of distance. |
analyzeAST / analyzeDataFlow / scanEntropy / scanAntiForensic timeout |
45 s | src/pipeline/executor.js |
Per-scanner timeout via withTimeout(); rejected scanners contribute an empty result + logged warning. |
Replaces global score accumulation with per-file max scoring. Formula: riskScore = min(100, max(file_scores) + package_level_score). Threats are split into package-level (lifecycle scripts, typosquat, IOC matches, sandbox findings — classified by PACKAGE_LEVEL_TYPES Set + file heuristics) and file-level (AST, dataflow, obfuscation). File-level threats grouped by threat.file, each group scored independently via computeGroupScore(). Package-level threats scored separately. Result includes globalRiskScore (old sum), maxFileScore, packageScore, mostSuspiciousFile, fileScores map.
Each threat's contribution to the score is multiplied by a confidence factor derived from rule.confidence (declared per rule in src/rules/index.js). A rule marked confidence: 'low' therefore contributes less than the nominal severity weight, even when the threat itself is HIGH/CRITICAL.
rule.confidence |
Multiplier | Effect on score contribution |
|---|---|---|
high |
1.0 | Nominal weight (CRITICAL=25, HIGH=10, MEDIUM=3, LOW=1) |
medium |
0.85 | −15% vs nominal |
low |
0.6 | −40% vs nominal |
Source: const CONFIDENCE_FACTORS = { high: 1.0, medium: 0.85, low: 0.6 }; applied in applyConfidenceFactor() (called at scoring.js:203 and 261). Unknown / missing rule.confidence falls back to 1.0. Used by --explain output to attribute the actual numeric contribution of each finding: effective_points = severity_weight × CONFIDENCE_FACTORS[rule.confidence].
applyFPReductions() in src/scoring.js applies count-based severity downgrades between deduplication and scoring. Thresholds: dynamic_require >10 HIGH→LOW, dangerous_call_function >5 MEDIUM→LOW, require_cache_poison >3 CRITICAL→LOW (single hit CRITICAL→HIGH), suspicious_dataflow >5 any→LOW, obfuscation_detected >3 any→LOW, module_compile >3 HIGH→LOW, module_compile_dynamic >3 HIGH→LOW, zlib_inflate_eval >2 CRITICAL→LOW, credential_regex_harvest >4 HIGH→LOW. Framework prototype hooks (Request/Response/App/Router.prototype) downgraded HIGH→MEDIUM (CRITICAL core prototypes untouched). HTTP client prototype whitelist: packages with >20 prototype_hook hits targeting HTTP methods → MEDIUM. Prototype hook MEDIUM scoring capped at 15 points max. Dist/build/minified file downgrade (one severity notch). Reachability-based downgrade (unreachable files → LOW). Typosquat whitelist expanded with 10 packages (chai, pino, ioredis, bcryptjs, recast, asyncdi, redux, args, oxlint, vasync). Scanner-level: expanded SAFE_ENV_VARS (+13 vars) and added SAFE_ENV_PREFIXES (npm_config_, npm_lifecycle_, npm_package_, lc_) in src/scanner/ast.js. Obfuscation in dist/build/*.bundle.js and .cjs/.mjs >100KB → LOW. Entropy: encoding table paths → LOW. Dataflow: os.platform/arch categorized as telemetry_read (capped at HIGH, not CRITICAL). Package scanner: DEP_FP_WHITELIST (es5-ext, bootstrap-sass), npm alias skip (npm: prefix).
FP Reduction P5 (v2.5.15): 7 heuristic precision fixes improving detection accuracy without reducing coverage.
FP Reduction P6 (v2.5.16): Compound detection precision — 6 fixes: (1) credential_regex_harvest count-based downgrade (>4 hits HIGH→LOW). (2) Remove remote_code_load and proxy_data_intercept from DIST_EXEMPT_TYPES. (3) Obfuscation .js >100KB → LOW. (4) Remove discord/leveldb from SENSITIVE_PATH_PATTERNS. (5) module_compile/module_compile_dynamic baseline CRITICAL→HIGH. (6) DATAFLOW_SAFE_ENV_VARS — exclude Node.js runtime config from credential sources. TPR: 91.8% → 93.9% (46/49). FPR: 13.6% → 12.3% (65/529).
FP Reduction P7 (v2.6.2): 7 heuristic fixes: (1) LOW-severity alert filtering in monitor. (2) Monorepo scope grouping for publish anomaly. (3) env_access count threshold (>10 → LOW). (4) suspicious_dataflow full bypass (removed 80% ratio guard). (5) high_entropy_string count threshold (>5 → LOW). (6) Extended DIST_FILE_RE (out|output) + env_access added to DIST_BUNDLER_ARTIFACT_TYPES. (7) credential_regex_harvest threshold lowered (>2 → LOW). FPR: 12.3% → 12.1% (64/529).
Intent Graph Analysis (v2.6.0): src/intent-graph.js performs intra-file source-sink coherence analysis. When a single file contains both a high-confidence credential source (sensitive_string, env_harvesting_dynamic, credential_regex_harvest) AND a dangerous sink (eval, exec, network), the intent graph boosts the score via a coherence matrix. Design principles: (1) INTRA-FILE pairing only — cross-file co-occurrence without proven data flow causes FP explosion on SDKs. (2) Cross-file detection delegated to src/scanner/module-graph/ (proven taint paths). (3) LOW severity threats excluded from pairing (respects FP reductions). (4) env_access and suspicious_dataflow excluded (standard config / double-counting). Intent bonus capped at 30 in scoring.js. Pipeline: deduplication → applyFPReductions → buildIntentPairs → enrichWithRules → calculateRiskScore.
Destination-aware Intent (v2.7.7): isSDKPattern() with 22 curated SDK env-domain mappings (AWS, Azure, Google, Firebase, Stripe, Twilio, SendGrid, Datadog, Sentry, Slack, GitHub, GitLab, Cloudflare, OpenAI, Anthropic, MongoDB, Auth0, HubSpot, Contentful, Salesforce, Supabase, Mailgun). Heuristic brand-matching fallback for unknown SDKs. SUSPICIOUS_DOMAIN_PATTERNS blocks tunneling services (ngrok, serveo, localtunnel) and raw IP addresses from SDK exemption. buildIntentPairs() accepts targetPath parameter for file reading (SDK pattern detection). Helpers: extractEnvVarFromMessage(), extractBrandFromEnvVar(), domainMatchesSuffix().
src/scanner/reachability.js (v2.5.x file-level, v2.11.9 function-level default ON via MUADDIB_FN_REACHABILITY !== '0') computes the set of files and functions actually reachable from package entry points (package.json main / bin / exports / module / browser + lifecycle scripts + spawn/fork/Worker targets). Consumed by applyFPReductions (scoring.js:874): any non-exempt threat located in an unreachable file is downgraded to LOW and tagged t.unreachable = true. Threats in dead function ranges of a reachable file are tagged t.unreachableFunction = true (function-level pass).
Exempt types (REACHABILITY_EXEMPT_TYPES, scoring.js:444-467): intel-triage P1 types + AI config injection + function_constructor_require + env_charcode_reconstruction (Mini Shai-Hulud) + ~16 other types structurally malicious independent of reachability. Files containing exempt threats also shield their other findings from the unreachable downgrade.
| Constant | Value | File:line | Role |
|---|---|---|---|
MAX_FN_REACH_FILES |
250 | reachability.js:370 |
Max files processed by function-level reachability. Beyond this cap, the function-level BFS short-circuits and all threats remain considered live (fail-open). |
MAX_FN_REACH_BYTES |
1 MiB (1024×1024) | reachability.js:371 |
Per-file size cap. Files larger than 1 MB (typically minified bundles) are skipped — their threats remain live (fail-open). |
Known limitation : cross-file function reachability is not implemented (intra-file BFS only, file-level approximation handles cross-file). Documented backlog item RC-M1: a non-exported helper called only via dynamic dispatch from another file is marked dead → severity LOW (FP). Combined with ESM ImportDeclaration gaps (DF-C3 backlog), the fail-direction is biased toward LOW, so cap saturation degrades coverage rather than introducing FPs.
src/sandbox/index.js — Docker-based dynamic analysis: installs a package in an isolated container, captures filesystem changes, network traffic (tcpdump), and process spawns (strace). Injects canary tokens by default. Multi-run mode (v2.4.9) with monkey-patching preload for time-bomb detection.
Sandbox Enhancements (v2.1.2):
- CI-aware environment:
sandbox-runner.shsets CI=true, GITHUB_ACTIONS, GITLAB_CI, TRAVIS, CIRCLECI, JENKINS_URL to trigger CI-aware malware - Enriched canary tokens: 6 static honeypots (GITHUB_TOKEN, NPM_TOKEN, AWS keys, SLACK/DISCORD webhooks) as fallback to dynamic tokens
detectStaticCanaryExfiltration()insrc/sandbox/index.jssearches all report fields for static canary values- Strict webhook filtering: monitor alerts only for IOC match, sandbox confirm, or canary exfiltration
docker/preload.js is a runtime monkey-patching script injected via NODE_OPTIONS=--require /opt/preload.js in the Docker sandbox. Detects time-bomb malware (MITRE T1497.003) that delays exfiltration past sandbox timeout.
- Architecture:
src/sandbox/index.js(migrated fromsrc/sandbox.js) orchestrates 3 sequential Docker runs at time offsets [0ms, 72h, 7d].runSandbox()callsrunSingleSandbox()withMUADDIB_TIME_OFFSET_MSenv var. Early exit on score >= 80. - Preload patches (IIFE, closure-scoped originals): Time APIs (Date.now, constructor, performance.now, process.hrtime/bigint, process.uptime), timers (setTimeout→0, setInterval→immediate first exec), network (http/https.request, fetch, dns, net.connect), filesystem (sensitive path detection via regex), process (child_process.* with dangerous command detection), environment (Proxy on process.env for sensitive key access).
- Analyzer:
src/sandbox/analyzer.jsparses[PRELOAD]log lines with 6 scoring rules: timer delay suspicious (>1h, MEDIUM +15), timer delay critical (>24h, CRITICAL +30, supersedes suspicious), sensitive file read (HIGH +20), network after sensitive read (CRITICAL +40, compound), exec suspicious (HIGH +25), env token access (MEDIUM +10). - Docker changes:
docker/Dockerfilecopiespreload.jsto/opt/preload.js.docker/sandbox-runner.shsetsNODE_OPTIONS, captures/tmp/preload.log, includespreload_login JSON report.
Trois nouveaux scanners statiques + l'extension du scraper IOC. Aligne le
projet sur la realite du marche : Zenbox (sandbox commercial) a classe
Axios npm CLEAN avec 99% de confiance ; les malwares 2026 defeat les
sandbox runtime by design (hardware fingerprinting, C2-side detection,
keys in response headers, 48h rate limit). Les outils SOTA (Socket.dev,
GuardDog, Semgrep, Phylum) sont tous static. Cf. feedback_static_over_dynamic
memoire.
Scanner P1.1 src/scanner/ioc-strings.js (rule MUADDIB-IOC-001,
threat ioc_string_match CRITICAL): YARA-style substring match contre
iocs/string-iocs.yaml. Bootstrap avec 15 strings (9 Axios par
N3mes1s gist + 3 TeamPCP + 1 GlassWorm + 2 CanisterSprawl + extensions).
Loader loadStringIocsYAML ajoute dans src/ioc/yaml-loader.js ;
expose iocs.stringIocs + iocs.stringIocsMap via loadCachedIOCs.
Match en source = signal CRITICAL transverse a tous les variants qui
reuse un meme stager / XOR key / RAT command name.
Scanner P1.2 src/scanner/anti-forensic.js (rules MUADDIB-AF-001/002):
detection AST compound de la classe Axios/csec autodelete. Trois patterns :
(1) XOR loop avec literal-derived operand (charCodeAt sur literal, ou XOR
avec literal number), (2) self-delete (fs.unlink/rename de __filename ou
path matchant node_modules/<pkg>/<file>.(c|m)?js), (3) decoy write
(fs.writeFile vers .md|.bak|.tmp|.txt|.log). 3 of 3 = CRITICAL ; 2 of 3
= HIGH partial. Pre-filtre regex pour skipper les bundles benign sans
operateur ^.
Scanner P1.3 src/scanner/stub-package.js (rules MUADDIB-STUB-001/002):
ferme le gap ltidi chain attack documente en memoire. Conditions :
(1) package.json declare au moins une dep avec URL externe (https/git/github),
(2) main file < 500 octets meaningful (comments stripped), (3) lifecycle hook
present. Triggers stub_package_external_payload CRITICAL. Sans lifecycle
= stub_package_external_dep HIGH. Walks node_modules sub-deps avec scoped
package handling.
Aikido feed src/ioc/scraper.js:scrapeAikidoMalwareFeed : pull
malware-list.aikido.dev/malware_predictions.json + _pypi.json. Smoke
live mai 2026 : 121 521 npm + 3 064 PyPI MALWARE entries source-tagged
aikido. Plug into runScraper Phase 2 + dedup avec source accumulation.
Source-aware confidence getSourceConfidence(pkg) : la dedup au scraper
maintient un array sources: [{name, added_at}] par entree (name@version).
Helper retourne {tier: 'high'|'medium'|'low', count, sources} ou tier
high = 3+ feeds distincts. Permet aux consumers (webhook, /diff) de
prioriser les alertes cross-confirmees.
Compounds intel-triage : axios_family (ioc_string_match + lifecycle_script
- anti_forensic_partial) et
stub_with_string_ioc(stub_package_external_dep - ioc_string_match). Rules
MUADDIB-COMPOUND-AXIOSetMUADDIB-COMPOUND-STUB-IOC.
CLI tool scripts/git-vs-npm-diff.js : telecharge tarball npm + archive
GitHub correspondant au tag, hash chaque fichier .js/.cjs/.mjs/.ts/.tsx/.json,
signale les npm-only (publish-time injection) ou hash-differs. Catch les
compromissions par token vole (Axios mode). Usage manuel par l'operateur
quand un package suspect est surface.
-
src/ioc/data/iocs-compact.json(~5MB, ships with npm) — wildcards[] + versioned{} Maps for O(1) lookup -
YAML files in
iocs/— builtin rules -
External sources, two refresh paths:
- Light path —
muaddib update(~5s, JSON/REST only): Shai-Hulud, DataDog, OpenSourceMalware (scrapeOSMQueryLatest, requiresOSM_API_TOKEN). - Full path —
muaddib scrape(~5min, includes heavy zip downloads): all of the above PLUS OSV bulk dump (npm + PyPI, incl. GHSA malware — MAL- entries), OSSF malicious-packages, Aikido bulk feed.
On the VPS, systemd units drive these automatically:
muaddib-monitor.service- long-running daemon (npm/PyPI poll + scan + in-process GHSA poller + webhooks)muaddib-scrape-light.timer(every 15 min) ->muaddib update- fast feeds incl. OSMmuaddib-scrape.timer(every 6 h) ->muaddib scrape- deep refreshmuaddib-coverage-audit.timer(daily 05:00 UTC) ->scripts/coverage-audit.js- Phase 5 operational coverage audit
The
OSM_API_TOKENfor OpenSourceMalware lives in/opt/muaddib/.env(loaded viaEnvironmentFile=-in the unit). Absent token = scraper skips OSM gracefully, no error. - Light path —
loadCachedIOCs() from src/ioc/updater.js merges all tiers and returns optimized Maps/Sets. Source attribution is preserved per IOC entry via sources: [{name, added_at}] accumulation, exposed by getSourceConfidence() for cross-feed confirmation gating.
src/ioc/feed-health.js detects silent IOC-feed failures: a feed that returns 0 entries after a healthy baseline signals stale data or a broken endpoint. evaluateFeedHealth() (pure decision core) compares per-feed counts against a persisted baseline (data/feed-health.json) and fires a ONE-SHOT alarm on a healthy->dark transition + a recovery notice on dark->alive. Baseline minimum 5 IOCs (MUADDIB_FEED_HEALTH_MIN) prevents FPs on small/volatile feeds. Best-effort: never breaks the IOC refresh. This closes the blind spot where the OSM feed went dark for weeks without ops notice. The OSM alert path is token-gated.
src/ioc/ghsa-poller.js polls the GitHub Advisory Database for type=malware advisories every ~15 min (GHSA_POLL_INTERVAL_MS, started by src/monitor/daemon.js). GHSA_ECOSYSTEMS = ['npm', 'pypi', 'crates']. It (1) persists each advisory's malicious packages to data/ghsa-malware.jsonl - the authoritative "what should we have caught" denominator for the Phase 5 coverage-audit; (2) pre-alerts genuinely fresh names (updated since the cursor) as early warning, capped per poll (GHSA_PREALERT_CAP); (3) records withdrawn advisories (withdrawn_at set) to the scan-ledger as outcome:dropped; (4) feeds its fetch count into the feed-health alarm. fetchAllGhsaMalware(ecosystem) returns the paginated full list (used by coverage-audit). The poller does NOT inject scans into the live queue: GHSA lags downstream vendors, so the package is usually already removed or in the IOC store. Honest scope - its value is the denominator + early-warning + withdrawn tracking, not net-new live detection. From v2.11.75 the rust/crates pre-alert is enriched with the nearest POPULAR_CRATES typosquat via findCratesTyposquatMatch().
Evaluation Framework (v2.2, corrected v2.2.7, updated through v2.11.48): src/commands/evaluate.js measures TPR (Ground Truth, 94 in-scope real attacks from 96 samples — 2026-05-25 enrichment +22 samples: 16 synthetic PYSRC/PYAST/AST-092/AICONF-004/PKG-022 + 6 real-world tarballs from VPS archive + 7 reconstructions from data/all-review-results.json; 2 out-of-scope GT-005/GT-009 protestware with min_threats=0), FPR (Benign, 548 npm curated packages — real source code via npm pack + native tar extraction; 132 PyPI added 2026-05-25 as benignPyPI), and ADR (Adversarial + Holdout, 107 evasive samples — 67 adversarial + 40 holdout). The schema now includes expected.score_typical (risk observed in isolation) and expected.tpr_tier (tpr20 or tpr3 — documentary; 3 samples flagged tpr3-only for HIGH/MEDIUM rules that don't cross 20 in isolation). Benign tarballs cached in .muaddib-cache/benign-tarballs/ (npm), .muaddib-cache/benign-pypi/ (PyPI). Flags: --benign-limit N, --refresh-benign. Results saved to metrics/v{version}.json.
FPR progression: 0% (invalid, v2.2.0–v2.2.6) → 38% (v2.2.7) → 19.4% (v2.2.8) → 17.5% (v2.2.9) → ~13% (69/527, v2.2.11) → 8.9% (47/527, v2.3.0) → 7.4% (39/525, v2.3.1) → 6.0% (32/529, v2.5.8, included BENIGN_PACKAGE_WHITELIST bias) → ~13.6% (72/529, v2.5.14, audit hardening + whitelist removed in v2.5.10) → 12.3% (65/529, v2.5.16, P5+P6) → 12.1% (64/529, v2.6.2, P7) → 12.9% (68/529, v2.9.4, compound scoring + new rules) → 10.8% (57/529, v2.10.1, audit v3 FP reduction) → 14.0% (74/532, v2.10.57, curated benign corpus rebuild) → estimated 6-9% (v2.10.74, P1-P4 FP cluster fixes — projected gain) → 15.6% measured (85/545 scanned of 548, v2.10.95 metrics file — the v2.10.74 projected reduction did NOT materialize on the rebuilt corpus) → post-filtre F1-F14 contextual FP caps (v2.10.97 → v2.11.31) → 1.10% measured (6/545 scanned of 548, v2.11.47 — the F1-F14 caps + the HARD/SOFT exfil split (F14) compounded over 11 versions to drive the rate down. The 6 remaining FPs are meteor, prisma, @prisma/client, drizzle-orm, scrypt, liquid — all real legitimate-pattern hits, not whitelist artifacts. FPR-after-ML-T1: 0.92% (5/545)).
Datasets:
- Adversarial samples in
datasets/adversarial/(67 samples) - Holdout samples in
datasets/holdout-v2/throughdatasets/holdout-v5/(40 samples) - Benign package lists in
datasets/benign/packages-npm.txt(532 packages) anddatasets/benign/packages-pypi.txt(132 packages) - Ground truth attacks in
tests/ground-truth/attacks.json(51 entries) - Ground truth malware database in
datasets/ground-truth/known-malware.json(65 entries)
96 real-world attack samples in tests/ground-truth/ (94 in-scope, 2 with min_threats=0: GT-005 colors and GT-009 faker, both protestware). 22 samples added 2026-05-25 (Track C synthetic + Track A real-world + Track B reconstructions). 13 PyPI samples (was 0). TPR@3: 95.74% (90/94 in-scope) as of v2.11.48 (full measurement on the enriched GT). TPR@20: 88.30% (83/94), +3.1pp vs v2.11.47 — Track D recon_exfil_direct_ip compound (MUADDIB-COMPOUND-016) closed the GT-095 gap (risk 3→50), plus GT-091/GT-092 gained from linux_fingerprint_exec. 4 active misses include the 3 browser-only attacks (lottie-player, polyfill-io, trojanized-jquery) plus 1 other. 2 intentional tpr3-only samples remain (GT-072 PYSRC-007, GT-077 PYAST-002 — HIGH/MEDIUM rules that don't cross 20 in isolation by design, see attacks.json expected.tpr_tier schema). ADR: 107 samples (67 adversarial + 40 holdout). ADR: 96.26% (103/107 available) as of v2.11.48.
The custom test framework (tests/run-tests.js) favors behavioral tests (run the code, assert on observable effects) over structural source-grep tests (read an impl file, assert it .includes() a string — these break on a harmless rename and pass when the logic is broken but the string survives). A guard, tests/meta/no-source-grep.test.js (enforced since v2.11.57, detector tests/meta/structural-test-scanner.js), fails the suite on any new source-grep outside a documented C4 allowlist (irreducible Docker/bash/daemon-loop/absence-guard sites with no host-runnable behavioral equivalent). To support the conversion, several decision functions were extracted as pure, exported seams: buildDockerArgs / generateFakeHostname (src/sandbox/index.js, the docker run argument vector), computeWorkersToSpawn (src/monitor/queue.js, worker-pool spawn count), reusing the already-exported computeMemoryPressure / computeTarget (memory circuit breaker / adaptive concurrency). Preload behaviors (/proc spoofing, process.dlopen, worker_threads injection) are exercised cross-platform via the runWithPreload subprocess harness in tests/unit/preload.test.js. These are pure refactors (no behavior change) and off the static-scan path, so detection metrics are unaffected.
Monitor (internal, not user-facing): src/monitor.js — muaddib monitor runs on VPS via systemd, polls npm/PyPI every 60s. Exports loadDetections, getDetectionStats, loadScanStats.
4 chantiers: (1) Self-exclude: SELF_PACKAGE_NAME constant skips muaddib-scanner in pollNpm(). (2) WASM standalone: new rule wasm_standalone (AST-046, MEDIUM) for WebAssembly without network sinks, mutually exclusive with wasm_host_sink (CRITICAL). (3) Reputation scoring: computeReputationFactor() adjusts webhook score based on age/versions/downloads (floor 0.3, ceiling 1.5), IOC matches bypass. (4) Scope dedup: bufferScopedWebhook() groups scoped npm packages within 5min window into single grouped Discord webhook.
HIGH_CONFIDENCE_MALICE_TYPES (30 types, in src/monitor/classify.js): lifecycle_shell_pipe, fetch_decrypt_exec, download_exec_binary, reverse_shell, crypto_staged_payload, intent_credential_exfil, intent_command_exfil, cross_file_dataflow, canary_exfiltration, sandbox_network_after_sensitive_read, sandbox_known_exfil_domain, detached_credential_exfil, node_modules_write, npm_publish_worm, systemd_persistence, npm_token_steal, root_filesystem_wipe, proc_mem_scan, trusted_new_unknown_dependency, curl_env_exfil, function_constructor_require, newsletter_auto_follow, self_destruct_eval, external_tarball_dep, function_runtime_args, env_charcode_reconstruction, ide_hook_autoexec, workflow_secrets_dump, gyp_command_exec, gyp_phantom_exec (the last two added v2.11.67/70, Phantom Gyp). These bypass reputation attenuation — supply-chain compromise of established packages cannot be suppressed.
Aggressive reputation tiers: computeReputationFactor() floor lowered from 0.30 to 0.10. New tiers: 5+ years age (-0.5), 200+ versions (-0.3), 1M+ weekly downloads (-0.4).
Graduated webhook threshold: getWebhookThreshold() returns 35 (very established, factor ≤0.5), 25 (established, factor ≤0.8), 20 (new/unknown) — established packages require higher static score to trigger webhook.
See Intent Graph section for isSDKPattern() details and 22 SDK env-domain mappings.
HC bypass severity check: Monitor validates severity !== LOW before counting HC types.
Size cap 20MB (monitor-only): LARGE_PACKAGE_SIZE = 20MB — skip full scan for packages >20MB unpacked. Malware payloads are tiny (<1MB); 20MB provides 20x safety margin. Exceptions: IOC match (always scan), suspicious lifecycle scripts (always scan).
MCP server awareness: mcp_config_injection downgraded CRITICAL→MEDIUM when @modelcontextprotocol/sdk is in package dependencies — legitimate MCP servers write config files.
Scan history memory (monitor-only): Cross-session webhook dedup via scan-memory.json. shouldSuppressByMemory() suppresses duplicate webhooks when score within ±15% and no new threat types. 30-day expiry, 50K max entries. IOC match and HC types bypass memory suppression.
Tarball archive (src/monitor/tarball-archive.js): suspect packages are downloaded + persisted to /opt/muaddib/archive/YYYY-MM-DD/ for retrospective audit (when npm/PyPI unpublish a malicious package). Fire-and-forget, never blocks the scan pipeline.
Three independent retention layers added after the 2026-05-24 disk-fill incident (96 GB VPS hit 100% → ~/.claude.json corrupted → +89K monitor errors). Each layer is sufficient alone to prevent the crash; they have no shared failure mode.
| Layer | Mechanism | Prevents |
|---|---|---|
| 1 | DEFAULT_RETENTION_DAYS = 7 (env MUADDIB_ARCHIVE_RETENTION_DAYS, bounded [1, 365]) |
Unbounded growth (root cause: ~4.5 GB/day × 30 d default = ~135 GB, impossible on 96 GB) |
| 2 | startPeriodicCleanup() re-runs cleanupOldArchives() every 6 h, .unref()'d |
Long-running daemon (no restarts for weeks) never gets a chance to purge |
| 3 | hasEnoughSpace(targetDir) pre-check via fs.statfsSync, skip archive if < 5 GB free (env MUADDIB_ARCHIVE_MIN_FREE_GB, bounded [1, 100], Node ≥ 18.15 — fail-open below) |
Burst of malicious campaigns blows past the 7-day budget between periodic ticks |
Wiring: src/monitor/daemon.js calls cleanupOldArchives() at startup AND startPeriodicCleanup() after the startup phase. Systemd deploy/muaddib-monitor.service sets Environment=MUADDIB_ARCHIVE_RETENTION_DAYS=7 explicitly (belt-and-suspenders with the code default).
Math: 7 d × 4.5 GB/day ≈ 31 GB archives + ~20 GB base = ~53 % of 96 GB disk in steady state. Even at peak ingestion days (5.8 GB), 14 d would push to 86 % (no margin); 7 d stays at ~63 %.
The tarball archive became alert-only in v2.11.67 (Phase 4-archive): a .tgz is persisted only when the package score >= 20, cutting archive disk churn to suspect packages.
src/monitor/state.js appendScanLedger() writes one outcome row per scanned package to data/scan-ledger.jsonl (fields: ts, name, version, ecosystem, outcome, score, tier, maxSeverity, types capped at 12, sandbox, firstPublish, source). Auto-compacted at MAX_SCAN_LEDGER (default 100k, env MUADDIB_SCAN_LEDGER_MAX). This is the authoritative per-package record consumed by the Phase 0b rollup and the Phase 5 coverage-audit. Fire-and-forget, never blocks the scan.
computeLedgerRollup(sinceTs) streams the ledger and groups outcomes by ecosystem: total / scanned / dropped / alerted / vanished + alertRate, with fair-window filtering. Surfaced as a "Ledger (24h)" section in the daily report + operational node metrics, with a trend regression-check. alertRate is an operational throughput signal, NOT detection TPR - the real operational TPR comes from the Phase 5 coverage-audit.
The T1a sandbox stage runs via src/monitor/deferred-sandbox.js asynchronously, so a slow Docker sandbox no longer holds a scan worker hostage - workers return to the pool immediately while the sandbox verdict is reconciled later.
Pre-alert embeds are ecosystem-aware: registryLink() resolves npm vs PyPI, and buildIOCPreAlertEmbed / buildCampaignPreAlertEmbed are pure builders reused across ecosystems. PyPI campaign pre-alerts now fire at npm parity, and first-publish detection extends to PyPI (flag + cache). The deferred sandbox gate stays npm-only.
recentWindowCount (the uncapped true count of versions published in the recent window, distinct from the capped recents list) drives a burst detector: at >= 10 versions in the window (account-takeover / Miasma mass-republish), sendBurstPreAlert() emits an amber pre-alert (buildBurstPreAlertEmbed), deduped per name/window (one ping per burst, not per version). When the scan queue hits its cap, eviction protects high-signal entries (IOC match, active burst, first-publish, account-takeover) from being dropped, with a strict-oldest fallback when every candidate is protected.
scripts/coverage-audit.js (classifyCoverage()) joins the GHSA malware denominator (data/ghsa-malware.jsonl) against scan-ledger outcomes + the tarball archive to compute an honest GHSA-denominated operational TPR (alerted / total). Every GHSA malware package is classified alerted / scannedClean (a false negative) / dropped (queue-cap or ghsa_gone) / neverSeen. Outputs data/coverage-audit.json + data/gt-proposals.json (scannedClean misses surfaced as ground-truth candidates, human-gated promotion). Runs daily via deploy/muaddib-coverage-audit.service + .timer (05:00 UTC; GITHUB_TOKEN from /opt/muaddib/.env raises the GHSA rate limit for pagination). This is the project's honest production detection rate, distinct from the static GT TPR and from the ledger alertRate.
Supply Chain Anomaly Detection (v2.0): 5 behavioral detection features that detect attacks before IOCs exist:
src/temporal-analysis.js— Sudden lifecycle script detection (--temporal): detectspreinstall/install/postinstalladded in latest versionsrc/temporal-ast-diff.js— Temporal AST diff (--temporal-ast): compares ASTs between versions to detect newly added dangerous APIssrc/publish-anomaly.js— Publish frequency anomaly (--temporal-publish): detects publish bursts, dormant spikes, rapid successionsrc/maintainer-change.js— Maintainer change detection (--temporal-maintainer): detects new/suspicious maintainers, sole maintainer changesrc/canary-tokens.js— Canary tokens (sandbox): injects fake credentials and detects exfiltration attempts--temporal-fullenables all 4 temporal features at once
Security Hardening (v2.1.2):
src/shared/download.js— SSRF-safe downloadToFile (domain allowlist + private IP blocking), injection-safe extractTarGz (execFileSync), sanitizePackageName (path traversal prevention)src/shared/constants.js— Centralized NPM_PACKAGE_REGEX, MAX_TARBALL_SIZE, DOWNLOAD_TIMEOUT
Validation & Observability (v2.1): Features for measuring and validating scanner effectiveness:
src/ground-truth.js— Ground truth dataset: 96 real-world attacks (94 in-scope, 2 protestware withmin_threats=0) replayed against scanner. 95.74% TPR@3 (90/94 in-scope), 88.30% TPR@20 (83/94, +3.1pp vs v2.11.47 — Track Drecon_exfil_direct_ipcompound closed GT-095 gap), v2.11.48 measurement.--breakdownflag — Explainable score decomposition showing per-finding contribution
Rules & playbooks: Threat types map to rules in src/rules/index.js (278 rules: 273 RULES + 5 PARANOID — Track D v2.11.48 added AST-093 linux_fingerprint_exec, AST-094 direct_ip_exfil, COMPOUND-016 recon_exfil_direct_ip; v2.11.67/70 Phantom Gyp added PKG-023 gyp_command_exec + COMPOUND-017 gyp_phantom_exec; MITRE ATT&CK mapped) and remediation text in src/response/playbooks.js. Both keyed by threat type string.
- MUADDIB-AST-008 to AST-012: Dynamic require with decode patterns, sandbox evasion, detached process, binary dropper patterns
- MUADDIB-AST-013: AI agent abuse (s1ngularity/Nx pattern —
--dangerously-skip-permissions,--yoloflags) - MUADDIB-AST-014: Credential CLI theft (
gh auth token,gcloud auth print-access-token,aws sts get-session-token) - MUADDIB-AST-015: Workflow write (fs.writeFileSync to
.github/workflows, with variable propagation + regex fallback) - MUADDIB-AST-016: Binary dropper (fs.chmodSync 0o755 + exec of temp file)
- MUADDIB-AST-017: Prototype hooking (globalThis.fetch, XMLHttpRequest.prototype, Node.js core module prototypes)
- MUADDIB-AST-018: Env charcode reconstruction (String.fromCharCode to build env var names)
- MUADDIB-AICONF-001: AI config prompt injection (HIGH)
- MUADDIB-AICONF-002: AI config compound injection — shell + exfil/credentials (CRITICAL)
- MUADDIB-PKG-010: Lifecycle shell pipe (curl|sh or wget|sh in preinstall/install/postinstall)
- MUADDIB-FLOW-003: Credential tampering / cache poisoning (sensitive read + write to cache paths)
- MUADDIB-AST-019: Require cache poisoning (require.cache access to hijack loaded modules)
- MUADDIB-AST-020: Staged binary payload (binary file .png/.jpg/.wasm + eval in same file — steganographic execution)
- MUADDIB-AST-021: Staged eval decode (eval/Function with atob or Buffer.from base64 argument — CRITICAL)
- MUADDIB-FLOW-004: Cross-file dataflow (credential read in one module, network exfil in another — CRITICAL)
- MUADDIB-AST-022: Encrypted payload decryption (crypto.createDecipher/createDecipheriv — flatmap-stream pattern, HIGH, T1140)
- MUADDIB-AST-023: Module compile execution (module._compile() — in-memory code execution, HIGH, T1059)
- MUADDIB-AST-024: Obfuscated payload via zlib inflate (zlib.inflateSync + eval — CRITICAL, T1140)
- MUADDIB-AST-025: Dynamic module compile execution (new Module() + _compile — HIGH, T1059)
- MUADDIB-AST-026: Anti-forensics write-execute-delete (write + exec + unlink — HIGH, T1070)
- MUADDIB-AST-027: MCP config injection (MCP server config manipulation — CRITICAL, T1059)
- MUADDIB-AST-028: Git hooks injection (write to .git/hooks — HIGH, T1195.002)
- MUADDIB-AST-029: Dynamic env var harvesting (Object.keys(process.env) — HIGH, T1552)
- MUADDIB-AST-030: DNS chunk exfiltration (dns.resolve with data in subdomain — HIGH, T1048)
- MUADDIB-AST-031: LLM API key harvesting (OPENAI_API_KEY, ANTHROPIC_API_KEY — MEDIUM, T1552)
- MUADDIB-AST-033: Steganographic payload chain (fetch + crypto decrypt + eval/Function — CRITICAL, T1027.003)
- MUADDIB-AST-034: Download-execute binary (download + chmod + execSync — CRITICAL, T1105)
- MUADDIB-AST-035: IDE task persistence (tasks.json + runOn:folderOpen + writeFileSync — HIGH, T1546)
- MUADDIB-SANDBOX-009: Suspicious timer delay (setTimeout/setInterval > 1h — MEDIUM, T1497.003)
- MUADDIB-SANDBOX-010: Critical timer delay / time-bomb (setTimeout/setInterval > 24h — CRITICAL, T1497.003)
- MUADDIB-SANDBOX-011: Preload sensitive file read (.npmrc/.ssh/.aws/.env via runtime monkey-patching — HIGH, T1552.001)
- MUADDIB-SANDBOX-012: Network after sensitive read (compound: file read + network — CRITICAL, T1041)
- MUADDIB-SANDBOX-013: Suspicious command execution (curl/wget/bash/sh/powershell via runtime monkey-patching — HIGH, T1059)
- MUADDIB-SANDBOX-014: Sensitive env var access (TOKEN/SECRET/KEY/PASSWORD via runtime monkey-patching — MEDIUM, T1552.001)
- MUADDIB-SHELL-013: FIFO + netcat reverse shell (mkfifo + nc sans /dev/tcp — CRITICAL, T1059.004)
- MUADDIB-SHELL-014: Base64 decode pipe to shell (base64 -d | bash — CRITICAL, T1140)
- MUADDIB-SHELL-015: Wget + base64 decode two-stage (wget + base64 -d — HIGH, T1105)
- MUADDIB-ENTROPY-004: Fragmented high entropy cluster (many short high-entropy strings — MEDIUM, T1027)
- MUADDIB-INTENT-001: Intent credential exfiltration (intra-file credential_read + exec/network sink — CRITICAL, T1041)
- MUADDIB-INTENT-002: Intent command output exfiltration (intra-file command_output + network sink — HIGH, T1041)
- MUADDIB-AST-046: WASM standalone module load (WebAssembly without network sinks — MEDIUM, T1027)
10 new adversarial samples in datasets/adversarial/. Group A (5 pure-API, multi-file): locale-config-sync, metrics-aggregator-lite, env-config-validator, stream-transform-kit, cache-warmup-utils. Group B (5 eval evasion): fn-return-eval, call-chain-eval, regex-source-require, charcode-arithmetic, object-method-alias. Scanner fixes: eval factory detection (() => eval), .call.call(eval) deep MemberExpression, require(/regex/.source) resolution, charcode arithmetic evaluation, object-method-alias taint tracking.
src/diff.js— Compares scan results between two git refs to surface only new threats (useful in CI). ExportsgetThreatId,compareThreats,resolveReffor testing.
The following commands are internal infrastructure/dev tools. They work when called directly but are intentionally hidden from --help and the interactive menu. Do not expose them in user-facing documentation or CLI help.
src/monitor.js—muaddib monitorruns on VPS via systemd, polls npm/PyPI every 60s. ExportsloadDetections,getDetectionStats,loadScanStats.src/threat-feed.js—muaddib feed(JSON stdout) andmuaddib serve(HTTP server with/feedand/health). SIEM integration for VPS infrastructure.muaddib detections— Detection history with lead time metrics. Uses monitor exports.muaddib stats— Daily scan statistics and FP rate. Uses monitor exports.src/commands/evaluate.js—muaddib evaluatemeasures TPR/FPR/ADR. Dev-only evaluation command.
applyCompoundBoosts() in src/scoring.js (array SCORING_COMPOUNDS lines 495-679) injects synthetic threats when co-occurring threat types are detected - combinations rarely seen in benign packages. Called after applyFPReductions. Currently 17 compound rules (14 CRITICAL + 3 HIGH) in SCORING_COMPOUNDS. A separate post-processor, phantom-gyp.js (correlatePhantomGyp, NOT part of SCORING_COMPOUNDS), emits the Phantom Gyp compound gyp_phantom_exec (MUADDIB-COMPOUND-017, CRITICAL) - listed last in the table below.
| # | Type | Required types | Severity | Flags |
|---|---|---|---|---|
| 1 | crypto_staged_payload |
staged_binary_payload + crypto_decipher | CRITICAL | sameFile |
| 2 | lifecycle_typosquat |
lifecycle_script + typosquat_detected | CRITICAL | — |
| 3 | dependency_typosquat_require |
dependency_typosquat + dependency_typosquat_used | CRITICAL | — |
| 4 | typosquat_lifecycle |
dependency_typosquat + lifecycle_script | CRITICAL | — |
| 5 | typosquat_dataflow |
dependency_typosquat + suspicious_dataflow | HIGH | excludeIfBundled |
| 6 | lifecycle_inline_exec |
lifecycle_script + node_inline_exec | CRITICAL | — |
| 7 | lifecycle_remote_require |
lifecycle_script + network_require | CRITICAL | — |
| 8 | websocket_credential_exfil |
env_access + suspicious_module_sink | CRITICAL | sameFile |
| 9 | lifecycle_dataflow |
lifecycle_script + suspicious_dataflow | HIGH | lifecycleScoped, excludeIfBundled |
| 10 | lifecycle_dangerous_exec |
lifecycle_script + dangerous_exec | CRITICAL | lifecycleScoped |
| 11 | obfuscated_lifecycle_env |
obfuscation_detected + env_access + lifecycle_script | HIGH | sameFileTypes, requireOriginalSeverityHigh, excludeIfBundled |
| 12 | lifecycle_newsletter_hijack |
lifecycle_script + newsletter_auto_follow | CRITICAL | — |
| 13 | lifecycle_env_exfil |
lifecycle_script + curl_env_exfil | CRITICAL | — |
| 14 | axios_family |
ioc_string_match + lifecycle_script + anti_forensic_partial | CRITICAL | — |
| 15 | stub_with_string_ioc |
stub_package_external_dep + ioc_string_match | CRITICAL | — |
| 16 | staged_remote_loader |
function_constructor_require + process_variable_shadow | CRITICAL | sameFile |
| 17 | recon_exfil_direct_ip |
linux_fingerprint_exec + direct_ip_exfil | CRITICAL | sameFile |
| - | gyp_phantom_exec (via phantom-gyp.js, COMPOUND-017) |
binding.gyp command-sub sink + independent malice verdict on invoked file | CRITICAL | post-processor, not in SCORING_COMPOUNDS |
sameFile: all required types must co-occur in the same file (vs package-level).lifecycleScoped: compound fires only when the co-occurring file-level signal is in the file directly executed by the lifecycle script or in its 1-level static imports (v2.11.11 — monorepo FP fix).excludeIfBundled: compound suppressed when all components live indist/build/out/output(bundler aggregation artefact, not real malware).sameFileTypes: explicit subset of required types that must be co-located (for compounds mixing file-level + package-level components — e.g.obfuscated_lifecycle_envrequires obfuscation_detected + env_access in the same file, lifecycle_script can be elsewhere).requireOriginalSeverityHigh: at least one component must haveoriginalSeverityHIGH+ (filters MEDIUM-only dilutions of a CRITICAL signal).
- Compounds 1, 2, 6, 7, 8 : foundational (v2.5–v2.7).
- Compounds 9, 10, 11 : v2.10.5 (post-audit fondamental, recover 3336 under-threshold malwares).
- Compounds 12, 13 : v2.10.89 (security review).
- Compound 14 : intel-triage P3.1 (Axios/csec family signature).
- Compound 15 : intel-triage P3.x (stub package chain-attack staging).
- Compound 16 : security review 2026-05-09 (chai-* / poxios-chain / express-guardrail / justenv pattern).
- Compound 3 : audit 2026-05 Sprint 5 (RT-C1, Axios UNC1069 boundary-squat).
- Compounds 4, 5 : audit 2026-05 Sprint 5 (RT-C1-FPR boundary-squat lifecycle + dataflow mirrors).
- Compound 17 (
recon_exfil_direct_ip) : Track D (v2.11.48), closes the GT-095 direct-IP recon+exfil gap (was missing from this table until now). gyp_phantom_exec(COMPOUND-017, viaphantom-gyp.js) : v2.11.70 Phase 1b Phantom Gyp. Correlates abinding.gypcommand-substitution sink with an independent malice verdict on the invoked file (FP~0 by construction). The Phase 1 marker PKG-023gyp_command_exec(v2.11.67) is the speed-bump; this is the precise compound fix.
3 compounds are also in PACKAGE_LEVEL_TYPES (lifecycle_typosquat, lifecycle_inline_exec, lifecycle_remote_require). dangerous_exec is in DIST_EXEMPT_TYPES (curl|bash in dist/ is always malicious).
GlassWorm campaign (March 2026, 433+ packages): Unicode invisible characters + Blockchain C2.
- Unicode invisible detection:
countInvisibleUnicode()inobfuscation.js, threshold >=3 chars. Detects zero-width (U+200B/C/D), BOM (U+FEFF pos>0), word joiner (U+2060), Mongolian vowel separator (U+180E), variation selectors (U+FE00-FE0F, U+E0100-E01EF), tag characters (U+E0001-E007F). - OBF-003:
unicode_invisible_injectionrule - AST-053:
unicode_variation_decoder— .codePointAt + 0xFE00/0xE0100 compound - AST-054:
blockchain_c2_resolution— Solana import + C2 methods (getSignaturesForAddress etc.). CRITICAL with eval/exec, HIGH otherwise - AST-055:
blockchain_rpc_endpoint— hardcoded Solana/Infura/Ankr endpoints (MEDIUM) - 6 GlassWorm C2 IPs added to SUSPICIOUS_DOMAINS_HIGH
- IOC: 4 markers, 2 files, 1 hash, 8 compromised packages (builtin.yaml)
5 bypass remediations from security audit v2 (score 71.9/100):
- Config security (CRITIQUE):
.muaddibrc.jsonin scanned package is IGNORED. Config auto-detection now only from~/.muaddibrc.jsonor CWD (if CWD ≠ targetPath). Emits[SECURITY]warning if config found inside scanned package. - BinaryExpression computed property:
resolveStringConcatWithVars()resolves double-indirection patterns likevar a='ev',b='al'; globalThis[a+b]()→ CRITICAL. - process.mainModule.require: Detection of
process.mainModule.require('child_process')as CRITICALdynamic_require. - Module._load: Detection of
Module._load()/require('module')._load()as CRITICALmodule_load_bypass. - AST-056:
module_load_bypass— Module._load() internal loader bypass (CRITICAL, T1059.007)
recentWindowCount(uncapped) drives a burst detector (>= 10 versions/window -> ambersendBurstPreAlert); queue-cap eviction now protects IOC/burst/first-publish/ATO entries, with a strict-oldest fallback. Files:src/monitor/{webhook,queue,scan-queue,ingestion}.js.
GHSA_ECOSYSTEMS += crates;POPULAR_CRATES(~88) +findCratesTyposquatMatch()intyposquat.js; rust GHSA pre-alerts enriched with the nearest-crate typosquat. Light pre-alert only (no crates download/sandbox).
- Ecosystem-aware
registryLink()+ purebuildIOCPreAlertEmbed/buildCampaignPreAlertEmbed; PyPI campaign pre-alerts at npm parity; first-publish extended to PyPI; deferred-sandbox gate stays npm-only.
scripts/coverage-audit.js(classifyCoverage) joins the GHSA denominator x scan-ledger x archive -> honest operational TPR (alerted/total);data/coverage-audit.json+ human-gateddata/gt-proposals.json. systemdmuaddib-coverage-audit.{service,timer}(daily 05:00 UTC).
src/ioc/ghsa-poller.jspolls the GitHub Advisory DB (type=malware) every ~15 min (npm/pypi/crates); writes theghsa-malware.jsonldenominator, fresh-name pre-alert, withdrawn -> ledger,fetchAllGhsaMalware(). Does not inject scans into the live queue.
src/ioc/feed-health.jsone-shot healthy->dark alarm + recovery (evaluateFeedHealthpure core,feed-health.json, baseline min 5). Closes the silent-feed-death blind spot.
src/scanner/phantom-gyp.jspost-processor emits COMPOUND-017gyp_phantom_exec(CRITICAL) only when abinding.gypcommand-substitution sink meets an independent malice verdict on the invoked file (FP~0 by construction). Added toSINGLE_FIRE_CRITICAL_TYPES(6) +HIGH_CONFIDENCE_MALICE_TYPES(30).
- The T1a sandbox runs async via
src/monitor/deferred-sandbox.js; a slow Docker sandbox no longer holds a scan worker hostage.
computeLedgerRollup(sinceTs)-> a 24h daily-report section + operational node metrics + trend regression-check.alertRateis throughput, not TPR.
- Phase -1 ops hardening (disk guard, POSIX ACL, resurrected IOC feeds, OSM token). Phase 0a per-scan ledger (
appendScanLedger->data/scan-ledger.jsonl). Phase 1 Phantom Gyp speed-bump PKG-023gyp_command_exec(CRITICAL marker onbinding.gypcommand-substitution). Phase 4-archive alert-only (.tgz persisted only if score >= 20).
- v2.11.58 coordinated 429 backoff (
signal429) - fixes the benign-FPR-drift measurement artifact; v2.11.59 per-worker 429-storm fix; v2.11.60 crash-resilience P0-P2; v2.11.61 sandbox skip on memory-match + native binary shards; v2.11.62loadashtyposquat-cycle guard + PR gate; v2.11.63 republish; v2.11.64 poll watchdog + HTTP deadline; v2.11.65-66 heap-leak root cause (packument projection + IOC store singleton).
- Splits the
F9_EXFIL_TYPESset (used by F9 mcpServerEnvAccess, F10 vendorCliSdk, F11 aiAgentBot for the "no third-party exfil" C5 condition) into two semantically distinct sets insrc/ml/feature-extractor.js:HARD_EXFIL_TYPES(12 types) — unambiguous adversary capability:suspicious_domain,remote_code_load,fetch_decrypt_exec,reverse_shell,binary_dropper,download_exec_binary,curl_env_exfil,curl_exec,external_tarball_dep,dependency_url_suspicious,blockchain_c2_resolution,dns_exfil.SOFT_EXFIL_TYPES(4 types) — compound/intent co-occurrence signals that fire on legit AI proxies:suspicious_dataflow,intent_credential_exfil,intent_command_exfil,detached_credential_exfil.
- Root cause (diagnostic 2026-05-24,
data/rescan/REPORT.md): rescanning 107 high-score FPs fromData/all-review-results.jsonagainst v2.11.30 showed 46 packages still at 90-100. For 41/46, the C5 disqualifier triggered on a SOFT compound — packages readingprocess.env.ANTHROPIC_API_KEYand POSTing toapi.anthropic.comfireintent_credential_exfil+suspicious_dataflowby construction, even when the destination is the legit first-party AI provider. - F9/F10/F11 now disqualify on
HARD_EXFIL_TYPES.has(t.type)only. F11 additionally absorbs the same hard-veto (pre-F14, F11 only checkedmcp_config_injection,suspicious_domain,binary_dropper,download_exec_binary— now alsoremote_code_loadfor slopsquat staging,external_tarball_dep+dependency_url_suspiciousfor dep-confusion, the curl/shell channels, and the covert egress types). - Threat-model safety: validated against the 3 MALWARE labels from the HEBDO 2026-05-18→24 review (
@cseo-hr/trpweb-shareddep-confusion, twin stagingbuild-scripts-utils/project-init-toolscampaign P-2024-001) — all emit ≥1 HARD signal. Shai-Hulud 2.0/3.0 signatures (suspicious_domainC2,binary_dropperTruffleHog,external_tarball_depBun fetch) → all HARD → still blocked.postmark-mcp(first public malicious MCP) →suspicious_domain→ blocked. F9_EXFIL_TYPESpreserved as a back-compat union (HARD ∪ 3 SOFT) for any external audit script referencing it.- Tests: 3656 → 3664 passed, 0 failed. 8 new F14 unit tests in
tests/unit/ml-feature-extractor.test.js(3 positive SOFT-only TRUE for F9/F10/F11 + 5 negative HARD-present FALSE coveringbinary_dropper,external_tarball_dep,dependency_url_suspicious,remote_code_load). 1 existing test updated (ai_agent_bot: TRUE on multi-LLM orchestrator (gm-skill pattern)) — removedremote_code_loadfrom fixture, since the post-F14 behaviour is correctly blocking; theremote_code_loadcase is now covered by the dedicated F14 negative test. - Projected impact on the 107-package FP corpus: cap coverage 57% → 80% (+25 packages dropped from 90-100). The 21 remaining at 90-100 all have ≥1 HARD signal — gray-zone packages that legitimately warrant manual review (gm-skill cluster with
bun xpatterns,@inetafrica/open-claudiaChinese MCP rerouting viasuspicious_domain).
- New helper
aiAgentBot(result, meta)insrc/ml/feature-extractor.js, wired as F11 inapplyContextualFPCaps()(src/scoring.js) with cap 35 (CRITICAL → MEDIUM-HIGH boundary). - Targets the third cluster from the audit 2026-05-week3 (54 entries, 18.9% of FP): packages that ARE multi-provider AI agents / orchestrators / chatbots / IM⇄AI bridges. Examples:
gm-skill,codexmate,lazyclaw,linco-connect(WeChat→Claude),natureco-cli(WhatsApp+Telegram),multis(Telegram chatbot),@aitne-sh/aitne,@jhizzard/termdeck,triflux,opuscode. - Discriminator architecture: these packages legitimately fire
dangerous_call_eval(LLM tool-use feature),remote_code_load(bun x pkg@latestorchestrator pattern),detached_credential_exfil(local session token storage). F11 cannot blacklist these threat types because they ARE the core capabilities of AI agents. Instead it requires:- Positive AI agent identity (name/desc/keywords/deps signal)
- Demonstrable operation on agent runtime data (touches paths like
~/.claude/,~/.codex/,~/.cursor/, etc.) - Absence of SANDWORM_MODE signatures (no preinstall hook, no
mcp_config_injection→ F9, nosuspicious_domainexfil, no credential file harvest, nobinary_dropper/download_exec_binary→ F2).
- Conjunction of 7 conditions:
- C1 AI agent identity —
meta.namematchesAGENT_NAME_REORmeta.descriptionmatchesAGENT_DESC_RE(multi-provider, AI agent/bot/orchestrator/harness, telegram/whatsapp/wechat bridge, etc.) ORmeta.registryMeta.keywordsincludes one ofAGENT_KEYWORDS_SETORmeta.registryMeta.dependenciesincludes one ofAGENT_DEPS(@anthropic-ai/sdk,openai,@google/genai,ollama,telegraf,@whiskeysockets/baileys,whatsapp-web.js,discord.js,node-pty, etc.). - C2 No install lifecycle hook.
- C3 No
mcp_config_injection(F9 priority). - C4 No
suspicious_domainthreat (third-party exfil discriminator). - C5 No credential file path in any threat message (reuses
F9_CREDENTIAL_FILE_RE). - C6 At least one threat references an agent runtime path (regex
AGENT_RUNTIME_PATHS_REoverthreat.messageANDthreat.file—~/.claude/,~/.codex/,~/.cursor/,~/.windsurf/,~/.continue/,~/.openclaude/,~/.openclaudia/,~/.hermes/,~/.aiflow/,~/.tdpilot/,~/.aitne/,~/.kimi/,~/.opuscode/,~/.freddie/,~/.gm-log/,~/.termdeck/,~/.relaydesk/,~/.natureco/). - C7 No
binary_dropper/download_exec_binary(F2 priority).
- C1 AI agent identity —
- Cap 35 (same as F10 — broader conjunction than F9).
Math.minlowest-wins arbitrates if F11 co-fires with another cap; C3 makes F9-F11 co-firing impossible by construction; F2 vs F11 mutual exclusion via C7. - Reuses
F9_CREDENTIAL_FILE_REfrom v2.11.22. ReuseshasLifecycleScriptshelper. No duplication. - Tests: 3594 → 3602 passed, 0 failed. 8 unit tests on
aiAgentBot(TP gm-skill via deps + path + identity, TP lazyclaw via desc, 6 disqualifying FN paths) + extended cluster-FP integration vector test (10 → 11 features). - Estimated effective coverage: 30-40 of the 54 cluster entries (55-75%). The remainder fails C4 (yingclaw rerouting via Chinese providers if flagged as suspicious_domain) or C6 (skill packs that don't directly read agent runtime paths).
- FPR delta measurement on 545 curated benign + ground truth regression check pending (post-merge on VPS).
- New helper
vendorCliSdk(result, meta)insrc/ml/feature-extractor.js, wired as F10 inapplyContextualFPCaps()(src/scoring.js) with cap 35 (CRITICAL → MEDIUM-HIGH boundary). - Targets the largest residual FP cluster from the audit 2026-05-week3 (96 entries, 33.6% of FP): legitimate vendor / community CLIs and SDKs that fire
credential_regex_harvest+env_accesson their OWN credential handling (Stripe checkout, OAuth-PKCE, bearer tokens to vendor APIs, .env template scaffolding). Examples:@nocobase/cli-v1,@posterly/cli,@super-hands/cli,codeapp-js-cli,nodebb-plugin-flawless-donations,@aiyiran/myclaw,usegrain,@tapestry-mud/cli,db-model-router. - Conjunction of 7 conditions, discriminator vs vendor-impersonating SANDWORM_MODE droppers:
- C1
binfield present (CLI signal — droppers rarely expose a bin) - C2
credential_regex_harvest/env_access/env_charcode_reconstruction/credential_tamperingfires (the FP source) - C3 No
mcp_config_injection(F9 garde la main on MCP packages) - C4 No install lifecycle hook (legit vendor CLIs are opt-in)
- C5 No third-party exfil threat (reuses
F9_EXFIL_TYPES, 15 types) - C6 No credential file path in any threat message (reuses
F9_CREDENTIAL_FILE_RE— blocks.npmrc/.aws/ SSH /.kube/.docker/.netrc/.git-credentials) - C7 Vendor identity hint:
homepagehost non-empty OR scoped@vendor/name
- C1
- Cap value 35 (vs F9 at 30 because the conjunction is structurally weaker — broader cluster, less restrictive identity).
Math.minlowest-wins still arbitrates if F10 co-fires with another cap (e.g., F9 wins if both fire, but C3 excludes that combination by construction). - Reuses F9 constants
F9_EXFIL_TYPESandF9_CREDENTIAL_FILE_RE— no duplication. - Tests: 3586 → 3594 passed, 0 failed. 8 unit tests on
vendorCliSdkcovering TP fixture, scoped-identity alternative, and 6 disqualifying FN paths (no bin, lifecycle hook, MCP territory, exfil signal, credential file path, no vendor identity). Integration vector test extended from 9 to 10 features. - Estimated effective coverage 60-75 of the 96 cluster entries (some lack a bin field — e.g., design-system asset packages that fall under F1
bundle_without_install_scriptsinstead). - FPR delta measurement on 545 curated benign + ground truth regression check pending (post-merge on VPS).
- New helper
mcpServerEnvAccess(result, meta)insrc/ml/feature-extractor.js, wired as F9 inapplyContextualFPCaps()(src/scoring.js) with cap 30 (CRITICAL → MEDIUM). - Targets the audit 2026-05-week3 cluster of 25 legitimate MCP installers/servers (Cachly, Roadmapfy, Llama Ventures, Flomenco, Supericons, cf-memory-mcp, mcp-memory-service, etc.) that currently score 75-99 from
mcp_config_injectionCRITICAL +env_access+credential_regex_harvesttriple-stacking on legitimate provider-key reads. - Conjunction of 5 conditions (AND), discriminant vs SANDWORM_MODE droppers:
- C1 MCP self-identity (
name/keywords/bin/descriptionmatchesmcp,mcp-server,mcp-init,model context protocol, etc.). - C2 Threat
mcp_config_injectionis present (positive signal that the package actually does MCP work). - C3 No install lifecycle hook (
preinstall|install|postinstallabsent). Legit MCP installers are opt-in (npx pkg init). - C4
env_access/credential_regex_harvestcite ONLY known provider keys (ANTHROPIC_API_KEY,OPENAI_API_KEY,STRIPE_*,GEMINI_API_KEY, etc., 28 literals +*_API_KEY|*_TOKEN|MCP_*suffix pattern) or infra vars (HOME,PATH, …). Never~/.npmrc,~/.aws/credentials,id_rsa,.ssh/. - C5 No third-party exfil signal (
suspicious_domain,remote_code_load,download_exec_binary,curl_env_exfil,binary_dropper, etc. — 15 exfil types).
- C1 MCP self-identity (
applyContextualFPCaps()'sMath.minlowest-wins arbitrates if F9 co-fires with another cap.- Plumbing:
_pkgMetainsrc/pipeline/processor.jsextended withkeywords+bin;meta.registryMetaconstruction inapplyContextualFPCapsextended in kind. - Tests: 3578 → 3586 passed, 0 failed. 7 unit tests on
mcpServerEnvAccess+ extendedextractFeatures: exposes all 9 cluster FP featuresintegration vector test intests/unit/ml-feature-extractor.test.js. - No new rule, no scoring change beyond the cap. Pure additive post-filter.
- FPR delta measurement on 545 curated benign + ground truth regression check pending (will be measured post-merge).
- New
applyContextualFPCaps()insrc/scoring.js(lines 1036-1119), called aftercalculateRiskScore()so compound boosts and lifecycle floors keep the last word. - 7 deterministic caps wired on the 7 active features from
feature-extractor.js(F8 still disabled, see v2.10.96):- F1
bundle_without_install_scripts→ cap 30 - F2
install_url_github_releases→ cap 35 - F3
network_destination_first_party→ cap 30 - F4
git_hook_source_local→ cap 35 - F5
typosquat_scoped_package→ subtract typosquat points (no cap) - F6
obfuscation_without_vector→ cap 35 - F7
placeholder_anti_dep_confusion→ cap 20
- F1
- When several caps apply,
Math.min()wins (tightest cap). F5 always applies separately. - Validated on 302-package human-reviewed corpus (198 FP + 104 malware): 67/198 FP capped (33.8 %), 0/104 malware impacted. The 131 remaining FP CRITICAL packages don't match any of the 7 clusters and need a different approach (compound scoring dedup roadmap).
- Tests: 3258 → 3280 passed, 0 failed. Positive + negative tests for each of the 7 caps + composition test (F1+F3 simultaneously → cap = min(30, 30) = 30) + regression test on 67 adversarial samples (zero cap applied).
- Monitor emits
summary.contextualCaps[]field listing applied caps.
- 8 boolean contextual features added to
src/ml/feature-extractor.js(lines 691-704). Each maps to a known FP cluster identified during the security review v2.10.93-95. - F1-F7 active and computed at every scan, F8 (
install_script_no_network_egress) volontairement laissee inerte (= 0) carEGRESS_TYPESne couvre pasdangerous_exec/lifecycle_dangerous_exec/node_inline_execet fire sur des malwares confirmes. A re-activer apres fix deEGRESS_TYPES. - Plumbing:
src/scanner/npm-registry.jsextractshomepagefrom manifest (needed for F3),src/scanner/ast-detectors/handle-post-walk.jsaddsthreat.urls[](host extracted from each detected URL),src/scanner/ast.jspasses context to post-walk handler,src/pipeline/processor.jspropagatesfileSizes+homepage+threat.urlsto ML record builder,src/monitor/ingestion.jsmirrors the same plumbing for the monitor. - Aucun changement de scoring, aucun changement de detection (pure plomberie). Les scans v2.10.96 produisent les memes scores que v2.10.95.
- Tests: 3236 → 3258 passed, 0 failed. 22 nouveaux tests dans
tests/unit/ml-feature-extractor.test.jscouvrant les 7 features actives (positif/negatif/edge cases).
src/scanner/ast.js:211:hasHashVerificationheuristique durcie. Exige maintenant la presence d'un operateur de comparaison (===,!==,.equals(,assert.strictEqual/equal/deepEqual/deepStrictEqual,throw) en plus decreateHash + digest. Bypass a 3 lignes ferme : un attaquant ne peut plus declencher le downgrade CRITICAL→HIGH dedownload_exec_binaryavec justecrypto.createHash('sha256').update(buf).digest('hex')sans comparaison.src/commands/evaluate.js: 8 occurrences defs.rmSync(pkgCacheDir, ...)enveloppees dans try/catch silencieux + 3 bouclesevaluateBenign/evaluateBenignPyPI/evaluateBenignRandomwrappentdownloadAndExtract+silentScanen try/catch. Un cleanup qui rate sur Windows (EPERM, file lock) ne tue plus une eval de 545 packages.- Triple-gate downgrade CRITICAL→MEDIUM abandonne : un plan initial proposait un downgrade quand
download_exec_binaryco-occurre avechasHashVerification=trueETfetchOnlySafeDomains=true. Diagnostic empirique sur 545 packages benign curated : 0 FPR delta (15.60 % → 15.60 %). La rule fire sur 3 packages seulement (esbuild, yarn, @backstage/create-app), les 2 derniers etaient deja LOW, esbuild est domine par d'autres rules CRITICAL. Plan archive dansdata/fp-v2.10.95-validation.md(gitignored). - Tests: 3232 → 3236 passed, 0 failed. 4 nouveaux tests
download_exec_binary(hash sans comparaison → CRITICAL nouveau gate, hash avec comparaison → HIGH preserve, etc.).
Apres v2.10.93 qui ajoutait les rules ltidi/csec/OAST/self_destruct_eval, un rescan empirique sur les tarballs reels de la semaine 2026-04-10 a 04-17 a mesure les scores effectifs et identifie 4 gaps restants :
src/scanner/package.js— nouveau threat typeexternal_tarball_dep(CRITICAL) emis a la place dedependency_url_suspiciousquand l'URL pointe vers une tarball (.tgz/.tar.gz/.tar.bz2/.zip) sur un host non-allowlist (cloud storage, CDN random). Ajoute aHIGH_CONFIDENCE_MALICE_TYPESpour bypass le MT-1 score ceiling. Rule ID :MUADDIB-PKG-020.src/scanner/ast-detectors/handle-new-expression.js— nouveau threat typefunction_runtime_args(CRITICAL) emis quandnew Function()recoit >= 2 args literal runtime (require,__dirname,__filename,module,exports,process) + body dynamique + obfuscation dans le meme fichier. Le gating obfuscation evite les FP sur les wrappers CommonJS legitimes (babel-register, ts-node, pirates, jest, nyc, vitest). Rule ID :MUADDIB-AST-090.src/scanner/package.js—curl_env_exfiletend son regex pour inclureping|nslookup|dig|host|getent, catchant koa-v3 qui utiliseping -c 1 $(whoami).<hex>.oast.fun.src/scoring.js— nouveau floor a 75 quand 2+ threat types DISTINCTS sont CRITICAL package-level. Co-occurrence 2 CRITICAL package-level = signature quasi-certaine de malware.
Rescan empirique v2.10.93 → v2.10.94 : ltidi cmp-api-stub 35 → 50, csec-crypto-toolkit 19 → 88, apache-arrow-14 50 → 75, koa-v3 9 → 75. Tests: 3230 → 3232 passed, 0 failed. 2 nouveaux tests function_runtime_args. Rules: 207 → 209 (204 RULES + 5 PARANOID).
- Review manuelle de 22 858 tarballs sur 8 jours, 37 malwares confirmés dont 10 sous le seuil de triage (ADR_THRESHOLD=20). Deux campagnes bypassaient entièrement la détection :
- ltidi dependency chain attack (9 packages, score 10) : stub packages sans install hooks, charge utile dans la dépendance
ltidisafehébergée surltidi.storage.googleapis.comcomme tarball HTTPS. Payload confirmée par téléchargement et analyse : DNS exfil de hostname/homedir/username via sous-domaines hex*.v519dosgqsrbgub2skqpw9azyq4oseg3.oastify.com. - csec credential stealer (3 packages, score 19) : variante de la campagne
plain-crypto-jsdocumentée par Resecurity (mars 2026). XOR avec cléOrDeR_7077+new Function(decoded)+unlinkSync(__filename)pour effacer les traces. Exfiltre.envsur 6 niveaux parents, tokens (GITHUB_TOKEN/NPM_TOKEN/AWS_*), clés SSH,.npmrc,.aws/credentialsverscsec-supply-chain-attack.vercel.app. - package.js —
dependency_url_suspiciousescalation : severity CRITICAL quand l'URL est une tarball (.tgz/.tar.gz/.tar.bz2/.zip) ET l'hôte n'est PAS sur une allowlist (github.com,codeload.github.com,objects.githubusercontent.com,gitlab.com,bitbucket.org,registry.npmjs.org,registry.yarnpkg.com). Les tarballs de cloud storage (GCS, S3, Azure blob) bypassent l'audit npm registry et correspondent au pattern ltidi. - ast.js + handle-post-walk.js —
self_destruct_evalcompound (AST-089) : nouveau flagctx.hasSelfDeletevia regex content pourunlinkSync/rmSync/renameSyncciblant__filename/module.filename/require.main.filename. Compound CRITICAL quandhasDynamicExecEThasSelfDeletedans le même fichier. Exempt de dist-file downgrade, dansHIGH_CONFIDENCE_MALICE_TYPES(bypass reputation attenuation). - constants.js — SUSPICIOUS_DOMAINS_HIGH : ajout de
oast.online,oast.pro,interact.sh,projectdiscovery.io,csec-supply-chain-attack.vercel.app,files.giftedtech.co.ke(Baileys V3 C2),phish.sh(depconf webhook exfil). - iocs/builtin.yaml : +9 stubs ltidi +
ltidisafewildcard + 3 csec variants +koa-v3@9.4.0(typosquat + ping DNS exfil) +cmp-api-stub@99.9.1(ltidi session 2). - Tests : 3134 → 3230 (8 nouveaux tests self_destruct_eval + 3 nouveaux tests dep URL escalation + 2 tests existants mis à jour de HIGH → CRITICAL pour les tarballs externes + 2 tests de compteur rule count mis à jour + 1 test compteur HC_TYPES mis à jour). Rules : 200 → 207 (202 RULES + 5 PARANOID). HC_TYPES : 22 → 23.
- Attendu en post-merge : ltidi stubs passent de score 10 à 50+ (dep_url CRITICAL 25 pts + dependency_ioc_match HIGH 10 pts + floor CRITICAL à 50). csec variants passent de 19 à 90+ (self_destruct_eval CRITICAL file-level + signaux existants, bypass reputation via HC_TYPES). Les 2 campagnes sortent du radar sous-threshold.
- Forensic audit of 53,953 production alerts on 8,396 high-score packages, 78 deep-reviewed
- P1 — Bundle FP downgrade: new
src/shared/bundle-detect.jswith extendedBUNDLE_PATH_REmatching.umd.js/.esm.js/.es.js/.common.js/.max.js/hash-suffixed chunks/fesm*/browser/assets/chunks/_app/lib/bundled. NewhasBundleVetoSignal()blocks downgrade whenreverse_shell/node_modules_write/npm_publish_worm/systemd_persistence/unicode_invisible_injection/IOC signals present. Types with compound recovery (staged_binary_payload,crypto_decipher,fetch_decrypt_exec) intentionally excluded fromVETO_TYPESto preserveoriginalSeveritygate. 14 packages recovered: babylonjs, electron, @kitware/vtk.js, dprint, @jetbrains/junie, @zuplo/core, @stencil/core, playwright, @equinor/echo-*, @alipay/ams-checkout, @testim/testim-cli, @vanwei-wcs/video-player-v2, @bookolosystem/engine, @epie/bi-crud. - P2 — AST-006 source qualification: new
ctx.varSourceMap in AST context tracks variable provenance (string_literal/array_literal/object_literal/fs_readdir/require_json/function_call/computed_expression/env_var).handleCallExpressionnow selects severity based on source: plugin loader patterns → LOW,require(process.env.X)→ CRITICAL, real obfuscation → HIGH. Fixes 53 FP fires on legitimate plugin loaders (@itentialopensource/adapter-*, masterrecord, testim, apminsight, @teamfabric/xpm, @forklaunch/express). - P3 — AST-007 quick-scan downgrade:
src/pipeline/executor.jsquick-scan emits threats as MEDIUM (was HIGH) with newdegraded: true, quickScan: trueflags.Module._loadstays CRITICAL (never legit).scoring.jsexcludesdegradednon-CRITICAL threats frommax_file_scoreand puts them in a separatedegradedScorebucket capped at 15 points. Fixes 18+ FP fires on rsshub/dist-lib/*.mjs route handlers. - P4 — WASM/Emscripten artifact skip in
src/scanner/obfuscation.js:isWasmEmscriptenArtifact()detects WASM files via basename (mpg123-decoder,wasm-audio-decoders, etc.) OR content markers (Module["asm"],HEAPU8,WebAssembly.instantiate,_emscripten_, base64 magicAGFzbQ). Skipped from obfuscation heuristics only — other scanners (AST, dataflow, hash, IOC) continue to analyze these files. Fixes 52 FP fires on@leoqlin/openclaw-qqbot/node_modules/mpg123-decoder/src/EmscriptenWasm.js. - TIER1 sandbox evasion guard:
isSuspectClassification()insrc/monitor/classify.jspreserves tier 2 whenever anyTIER1_TYPESfinding is present — even at LOW severity.enqueueDeferred()insrc/monitor/deferred-sandbox.jshas matching exception to theDEFERRED_MIN_SCOREgate. Threat model: adversary tunes malware to fire only weak TIER1 patterns (e.g.staged_payloadLOW +sandbox_evasionLOW) to bypass sandbox verification via the 2-distinct-types fallback. - react-emits ground truth fixture: new GT-067 forensic fixture reproduces the live-detected malware (fake
pathmodule with two top-level IIFEs doingfetch(atob(URL)).then(.json()).then(eval(data.content))). Attack evolved v1.0.0 (split payload/trigger via 5 malicious deps) → v1.0.2 (URLs hardcoded inpath.js) → v1.0.3 (broken half-retract). C2 IP173.211.46.220added toiocs/builtin.yamlmarkers (plain + base64 prefix). data/auto-labels.jsonupdates: 4 new entriesreact-emits@1.0.0-1.0.3asconfirmed_malicious,@lystech/core@3.0.42mutated frompendingtoconfirmed_malicious(aggressive vendor telemetry pattern: silent.github/workflows/write during postinstall).- Tests: 3068 → 3134 across 66 files. Rules: 200 (195 RULES + 5 PARANOID, unchanged — P2 extends AST-006 without splitting). Ground truth: 66 → 67 samples.
- Estimated FPR improvement: 14.0% → 6-9% (-5 to -8 points). TPR maintained at 93.75% (60/64). Measurement deferred to post-release sweep.
- ML1 XGBoost trained: P=0.978, R=0.933, F1=0.955 (114 trees, 21 features)
- ML2 Bundler detector trained: P=0.992, R=1.000, F1=0.996 (98 trees, 30 features)
- Audit fondamental: 8176 contaminated "fp" labels cleaned to "unconfirmed"
- Webhook triage P1/P2/P3:
computeAlertPriority()with visual classification - 3 new compound scoring rules: lifecycle_dataflow, lifecycle_dangerous_exec, obfuscated_lifecycle_env
- Lifecycle guard, score-0 investigation script, LLM triage design doc
- Honey environment: canary tokens, Docker camouflage
- Tests: 2533 → 2643 across 57 files. Rules: 158 → 162 (157 RULES + 5 PARANOID)
- Wild TPR: 92.8% (13538/14587). FPR curated: 11.0% (58/529)
- 6 bypasses closed, 5 new detection rules
- WebSocket/MQTT/Socket.IO sink detection, split entropy payload, lifecycle-file-exec compound
- FPR curated: 13.2% → 10.8% (57/529). FPR random: 8.0% → 7.5% (15/200)
- Tests: 2477 → 2533 across 56 files. Rules: 153 → 158 (153 RULES + 5 PARANOID)
- XGBoost-based binary classifier for T1 zone FP reduction (stub model)
- 71 features (62 → 71), ML filter in monitor pipeline
- Tests: 2435 → 2477 across 56 files
- 5 bypass remediations (config neutralization, BinaryExpression concat, process.mainModule.require, Module._load)
- 1 new rule: AST-056
module_load_bypass - Tests: 2435 across 54 files
- Rules: 153 (148 RULES + 5 PARANOID)
- 3 FP fixes, 3 quick wins
- ADR: 96.3% (103/107)
- Tests: 2336 across 50 files
- 4 compound scoring rules: co-occurring threat types that never appear in benign packages
applyCompoundBoosts()in scoring.js, called after applyFPReductionsdangerous_execadded to DIST_EXEMPT_TYPES- Tests: 2300 → 2329. Rules: 147 → 152 (147 RULES + 5 PARANOID)
- GlassWorm campaign: Unicode invisible + Blockchain C2 detection
- 3 new AST rules (AST-053/054/055), 1 new OBF rule (OBF-003)
- 6 GlassWorm C2 IPs, 8 compromised packages IOC
- Tests: 2266 → 2300. Rules: 143 → 147 (142 RULES + 5 PARANOID)
- 8 new rules: bin_field_hijack (PKG-013), npm_publish_worm (AST-051), node_modules_write (AST-048), bun_runtime_evasion (AST-049), static_timer_bomb (AST-050), ollama_local_llm (AST-052), network_require (PKG-011), node_inline_exec (PKG-012)
- Additional PKG rules: git_dependency_rce (PKG-014), npmrc_git_override (PKG-015), lifecycle_hidden_payload (PKG-016)
- Tests: 2222 → 2266. Rules: 134 → 143 (138 RULES + 5 PARANOID)
- ML feature extraction pipeline: 62 features per package scan
- JSONL feature export for offline model training
- Test optimization P1-P3 (373s → 134s)
- Real-time npm monitoring via changes stream (replaces RSS polling)
- Parallel scan processing (concurrency=3→5)
- Daily stats persistence
- v2.7.9: IPv6 SSRF fix, preload hardening, FP audit trail
- v2.7.10: Confidence-weighted scoring, zip bomb protection
- Size cap 20MB: bypass full scan for packages >20MB (IOC and lifecycle exceptions)
- MCP server awareness: mcp_config_injection CRITICAL→MEDIUM for SDK packages
- Scan history memory: cross-session webhook dedup via scan-memory.json (30d, 50K max, ±15%)
- Tests: 2143 → 2166 (+23)
isSDKPattern()with 22 curated SDK env-domain mappingsSUSPICIOUS_DOMAIN_PATTERNSblocks tunneling services and raw IPs- HC bypass severity check in monitor
- Tests: 2093 → 2143 (+50)
- HIGH_CONFIDENCE_MALICE_TYPES (19 types) bypass reputation attenuation
- Aggressive reputation tiers: floor 0.30→0.10
- Graduated webhook threshold: 35/25/20 based on establishment
- Fix double DORMANT log
- Tests: 2093 → 2093 (monitor-only changes)
- Self-exclude muaddib-scanner from pollNpm()
- WASM standalone rule (AST-046, MEDIUM)
- Reputation scoring: computeReputationFactor() with age/versions/downloads
- Scope dedup: bufferScopedWebhook() for monorepo noise
- Tests: 2042 → 2093 (+51). Rules: 133 → 134 (129 RULES + 5 PARANOID)
- SSRF IPv6 bypass fix, monitor scoring alignment, eval methodology fixes
- 3 new shell IFS evasion rules (SHELL-016 to SHELL-018), charcode validation
- Tests: 2009 → 2042 (+33). Rules: 130 → 133 (128 RULES + 5 PARANOID)
- 6 phases: safety, detection bypasses, evaluation methodology, IOC validation, paranoid mode, documentation
- Tests: 1940 → 1974 (+34)
- 10 new adversarial samples, intent graph (intra-file source-sink coherence)
- 2 new rules: INTENT-001, INTENT-002
- Tests: 1869 → 1940 (+71). Rules: 121 → 129 (124 RULES + 5 PARANOID)
- v2.5.13: Scoring thresholds, IOC integrity, sandbox NODE_OPTIONS, dataflow Promise/.then(), deobfuscation TemplateLiteral. Tests: 1656 → 1790 (+134)
- v2.5.14: AST bypasses, dataflow taint, shell patterns (SHELL-013 to SHELL-015), entropy (ENTROPY-004), typosquat whitelist. Tests: 1790 → 1815 (+25). Rules: 117 → 121 (116 RULES + 5 PARANOID)
- 41 issues remediated (14 CRITICAL, 18 HIGH, 9 MEDIUM)
- FP Reduction P4: IOC wildcard audit, webhook noise reduction
- Tests: 1522 → 1656 (+134). Rules: 107 → 113 (108 RULES + 5 PARANOID)
- v2.4.7: 5 adversarial samples, resolveStringConcat(), 3 new rules (AST-033/034/035). ADR: 98.8% (82/83)
- v2.4.9: Multi-run sandbox preload for time-bomb detection. 6 new rules (SANDBOX-009 to 014). Rules: 102 → 121 (116 RULES + 5 PARANOID)
- FPR: ~13% → 8.9% → 7.4% (39/525)
- 8 new rules (AST-024 to AST-031). Tests: 1317 → 1387
- v2.2.6: Inter-module dataflow (module-graph.js)
- v2.2.8–v2.2.9: FP Reduction P1 (38% → 17.5%)
- v2.2.11: Per-file max scoring (FPR ~13%)
- v2.2.12: Ground truth expansion (51 samples, 93.9% TPR)
- v2.2.22: Scan freeze fix (EXCLUDED_DIRS)
- v2.2.24: Coverage expansion (862 → 1317 tests, 72% → 86%)
- 5 features: temporal lifecycle, AST diff, publish anomaly, maintainer change, canary tokens
- IOC matching, pattern scanning, basic AST analysis