muaddib-scanner 2.5.7 → 2.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -15
- package/iocs/builtin.yaml +80 -11
- package/iocs/packages.yaml +216 -12
- package/logs/alerts/2026-03-06T19-26-22-192-evil-pkg.json +20 -0
- package/logs/alerts/2026-03-06T19-26-22-192-suspect-pkg.json +24 -0
- package/logs/alerts/2026-03-06T19-26-22-193-evil-pkg.json +20 -0
- package/logs/alerts/2026-03-06T19-26-22-572-evil-pkg.json +24 -0
- package/logs/daily-reports/2026-03-06.json +61 -0
- package/package.json +1 -1
- package/src/ioc/scraper.js +1 -1
- package/src/scanner/ast-detectors.js +40 -12
- package/src/scanner/ast.js +26 -1
- package/src/scanner/typosquat.js +5 -1
- package/src/scoring.js +61 -14
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
npm and PyPI supply-chain attacks are exploding. Shai-Hulud compromised 25K+ repos in 2025. Existing tools detect threats but don't help you respond.
|
|
32
32
|
|
|
33
|
-
MUAD'DIB combines static analysis + **deobfuscation engine** (v2.2.5) + **inter-module dataflow** (v2.2.6) + **per-file max scoring** (v2.2.11) + dynamic analysis (Docker sandbox) + **behavioral anomaly detection** (v2.0) + **ground truth validation** (v2.1) to detect threats AND guide your response — even before they appear in any IOC database.
|
|
33
|
+
MUAD'DIB combines static analysis + **deobfuscation engine** (v2.2.5) + **inter-module dataflow** (v2.2.6) + **per-file max scoring** (v2.2.11) + dynamic analysis (Docker sandbox with **monkey-patching preload** for time-bomb detection, v2.4.9) + **behavioral anomaly detection** (v2.0) + **ground truth validation** (v2.1) + **security audit** (41 issues remediated, v2.5.0–v2.5.6) to detect threats AND guide your response — even before they appear in any IOC database.
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
@@ -205,6 +205,7 @@ Multi-layer monitoring:
|
|
|
205
205
|
- **Data exfiltration detection**: 16 sensitive patterns (tokens, credentials, SSH keys, private keys, .env)
|
|
206
206
|
- **CI-aware environment** (v2.1.2): simulates CI environments (GITHUB_ACTIONS, GITLAB_CI, TRAVIS, CIRCLECI, JENKINS) to trigger CI-aware malware that would otherwise stay dormant
|
|
207
207
|
- **Enriched canary tokens** (v2.1.2): 6 honeypot credentials injected as env vars (GITHUB_TOKEN, NPM_TOKEN, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, SLACK_WEBHOOK_URL, DISCORD_WEBHOOK_URL). If exfiltrated via network, DNS, or filesystem, triggers CRITICAL alert with +50 score
|
|
208
|
+
- **Monkey-patching preload** (v2.4.9): Runtime instrumentation via `NODE_OPTIONS=--require /opt/preload.js`. Patches time APIs (Date.now, setTimeout→0, setInterval→immediate), intercepts network/filesystem/process/env calls. Multi-run mode at [0h, 72h, 7d] offsets to detect time-bomb malware (MITRE T1497.003)
|
|
208
209
|
- **Scoring engine**: 0-100 risk score based on behavioral severity
|
|
209
210
|
|
|
210
211
|
Use `--strict` to block all non-essential outbound network traffic via iptables.
|
|
@@ -285,7 +286,7 @@ Add to `.pre-commit-config.yaml`:
|
|
|
285
286
|
```yaml
|
|
286
287
|
repos:
|
|
287
288
|
- repo: https://github.com/DNSZLSK/muad-dib
|
|
288
|
-
rev: v2.
|
|
289
|
+
rev: v2.5.8
|
|
289
290
|
hooks:
|
|
290
291
|
- id: muaddib-scan # Scan all threats
|
|
291
292
|
# - id: muaddib-diff # Or: only new threats
|
|
@@ -641,7 +642,7 @@ Alerts appear in Security > Code scanning alerts.
|
|
|
641
642
|
## Architecture
|
|
642
643
|
|
|
643
644
|
```
|
|
644
|
-
MUAD'DIB 2.
|
|
645
|
+
MUAD'DIB 2.5.8 Scanner
|
|
645
646
|
|
|
|
646
647
|
+-- IOC Match (225,000+ packages, JSON DB)
|
|
647
648
|
| +-- OSV.dev npm dump (200K+ MAL-* entries)
|
|
@@ -663,7 +664,7 @@ MUAD'DIB 2.3.1 Scanner
|
|
|
663
664
|
| +-- 3-hop re-export chains, class method analysis
|
|
664
665
|
| +-- Cross-file credential read -> network sink detection
|
|
665
666
|
|
|
|
666
|
-
+-- 14 Parallel Scanners (
|
|
667
|
+
+-- 14 Parallel Scanners (113 rules)
|
|
667
668
|
| +-- AST Parse (acorn) — eval/Function, credential CLI theft, binary droppers, prototype hooks
|
|
668
669
|
| +-- Pattern Matching (shell, scripts)
|
|
669
670
|
| +-- Obfuscation Detection (skip .min.js, ignore hex/unicode alone)
|
|
@@ -690,21 +691,31 @@ MUAD'DIB 2.3.1 Scanner
|
|
|
690
691
|
| +-- Score Breakdown (explainable per-rule scoring)
|
|
691
692
|
| +-- Threat Feed API (HTTP server, JSON feed for SIEM)
|
|
692
693
|
|
|
|
693
|
-
+-- FP Reduction Post-processing (v2.2.8-v2.2.9, v2.3.0-v2.3.1)
|
|
694
|
+
+-- FP Reduction Post-processing (v2.2.8-v2.2.9, v2.3.0-v2.3.1, v2.5.7-v2.5.8)
|
|
694
695
|
| +-- Count-based severity downgrade (dynamic_require, dataflow, module_compile, etc.)
|
|
695
696
|
| +-- Framework prototype scoring cap + HTTP client whitelist
|
|
696
697
|
| +-- Obfuscation in dist/build/.cjs/.mjs → LOW
|
|
697
698
|
| +-- Safe env var + prefix filtering
|
|
698
699
|
| +-- Dataflow telemetry source categorization (os.platform/arch → telemetry_read)
|
|
699
700
|
| +-- DEP whitelist (es5-ext, bootstrap-sass) + npm alias skip
|
|
701
|
+
| +-- IOC wildcard audit (v2.5.8): FPR 10.8% → 6.0%
|
|
700
702
|
|
|
|
701
703
|
+-- Per-File Max Scoring (v2.2.11)
|
|
702
704
|
| +-- Score = max(file_scores) + package_level_score
|
|
703
705
|
| +-- Eliminates score accumulation across many files
|
|
704
706
|
| +-- Package-level threats (lifecycle, typosquat, IOC) scored separately
|
|
705
707
|
|
|
|
708
|
+
+-- Sandbox Monkey-Patching Preload (v2.4.9)
|
|
709
|
+
| +-- Runtime time manipulation (Date.now, setTimeout→0, setInterval→immediate)
|
|
710
|
+
| +-- Network/filesystem/process/env interception and logging
|
|
711
|
+
| +-- Multi-run [0h, 72h, 7d] for time-bomb detection (T1497.003)
|
|
712
|
+
|
|
|
713
|
+
+-- Security Audit (v2.5.0-v2.5.6)
|
|
714
|
+
| +-- 41 issues remediated (14 CRITICAL, 18 HIGH, 9 MEDIUM)
|
|
715
|
+
| +-- Native addon path traversal, atomic writes, AST bypasses
|
|
716
|
+
|
|
|
706
717
|
+-- Paranoid Mode (ultra-strict)
|
|
707
|
-
+-- Docker Sandbox (behavioral analysis, network capture, canary tokens, CI-aware)
|
|
718
|
+
+-- Docker Sandbox (behavioral analysis, network capture, canary tokens, CI-aware, preload)
|
|
708
719
|
+-- Zero-Day Monitor (internal: npm + PyPI RSS polling, Discord alerts, daily report)
|
|
709
720
|
|
|
|
710
721
|
v
|
|
@@ -725,8 +736,8 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
|
|
|
725
736
|
|--------|--------|---------|
|
|
726
737
|
| **Wild TPR** (Datadog 17K) | **88.2%** raw · **~100%** adjusted | 17,922 real malware samples. 2,077 misses are all out-of-scope (see below) |
|
|
727
738
|
| **TPR** (Ground Truth) | **91.8%** (45/49) | 51 real-world attacks (49 active). 4 out-of-scope: browser-only (3) + FP-risky (1) |
|
|
728
|
-
| **FPR** (Benign, global) | **
|
|
729
|
-
| **ADR** (Adversarial + Holdout) | **98.
|
|
739
|
+
| **FPR** (Benign, global) | **6.0%** (32/529) | 529 npm packages, real source code via `npm pack`, threshold > 20 |
|
|
740
|
+
| **ADR** (Adversarial + Holdout) | **98.8%** (82/83) | 43 adversarial + 40 holdout evasive samples. 1 documented miss: `require-cache-poison` (accepted trade-off) |
|
|
730
741
|
|
|
731
742
|
**Datadog 17K benchmark** — [DataDog Malicious Software Packages Dataset](https://github.com/DataDog/malicious-software-packages-dataset), 17,922 real malware samples (npm). Raw TPR: 88.2% (15,810/17,922). The 2,077 misses (score=0) were manually categorized:
|
|
732
743
|
|
|
@@ -747,7 +758,7 @@ All 2,077 misses lack Node.js malware patterns. MUAD'DIB performs AST-based Node
|
|
|
747
758
|
| Large (50-100 JS files) | 40 | 10 | 25.0% |
|
|
748
759
|
| Very large (100+ JS files) | 62 | 25 | 40.3% |
|
|
749
760
|
|
|
750
|
-
**FPR progression**: 0% (invalid, empty dirs, v2.2.0-v2.2.6) → 38% (first real measurement, v2.2.7) → 19.4% (v2.2.8) → 17.5% (v2.2.9) → ~13% (v2.2.11, per-file max scoring) → 8.9% (v2.3.0, P2) →
|
|
761
|
+
**FPR progression**: 0% (invalid, empty dirs, v2.2.0-v2.2.6) → 38% (first real measurement, v2.2.7) → 19.4% (v2.2.8) → 17.5% (v2.2.9) → ~13% (v2.2.11, per-file max scoring) → 8.9% (v2.3.0, P2) → 7.4% (v2.3.1, P3) → **6.0%** (v2.5.8, P4 + IOC wildcard audit)
|
|
751
762
|
|
|
752
763
|
**Holdout progression** (pre-tuning scores, rules frozen):
|
|
753
764
|
|
|
@@ -761,11 +772,11 @@ All 2,077 misses lack Node.js malware patterns. MUAD'DIB performs AST-based Node
|
|
|
761
772
|
|
|
762
773
|
- **Wild TPR** (Datadog Benchmark): detection rate on 17,922 real malware packages from the [DataDog Malicious Software Packages Dataset](https://github.com/DataDog/malicious-software-packages-dataset). Raw 88.2% (15,810/17,922). Adjusted ~100% on JS/Node.js malware when excluding out-of-scope samples (1,233 phishing HTML pages, 824 native binaries, 20 corrected libraries). See [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md#14-datadog-17k-benchmark).
|
|
763
774
|
- **TPR** (True Positive Rate): detection rate on 49 real-world supply-chain attacks (event-stream, ua-parser-js, coa, flatmap-stream, eslint-scope, solana-web3js, and 43 more). 4 misses are browser-only (lottie-player, polyfill-io, trojanized-jquery) or risky to fix (websocket-rat) — see [Threat Model](docs/threat-model.md).
|
|
764
|
-
- **FPR** (False Positive Rate): packages scoring > 20 out of 529 real npm packages (source code scanned, not empty dirs).
|
|
765
|
-
- **ADR** (Adversarial Detection Rate): detection rate on
|
|
775
|
+
- **FPR** (False Positive Rate): packages scoring > 20 out of 529 real npm packages (source code scanned, not empty dirs).
|
|
776
|
+
- **ADR** (Adversarial Detection Rate): detection rate on 83 evasive malicious samples — 43 adversarial + 40 holdout (5 batches of 10, testing obfuscation, inter-module dataflow, etc.). 1 documented miss: `require-cache-poison` (score 10 < threshold 20, accepted trade-off from FP reduction P3).
|
|
766
777
|
- **Holdout** (pre-tuning): detection rate on 10 unseen samples with rules frozen (measures generalization)
|
|
767
778
|
|
|
768
|
-
Datasets: 17,922 Datadog malware samples, 529 npm + 132 PyPI benign packages,
|
|
779
|
+
Datasets: 17,922 Datadog malware samples, 529 npm + 132 PyPI benign packages, 83 adversarial/holdout samples, 51 ground-truth attacks (65 documented malware packages). **1656 tests**, 86% code coverage.
|
|
769
780
|
|
|
770
781
|
See [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) for the full experimental protocol.
|
|
771
782
|
|
|
@@ -801,12 +812,12 @@ npm test
|
|
|
801
812
|
|
|
802
813
|
### Testing
|
|
803
814
|
|
|
804
|
-
- **
|
|
815
|
+
- **1656 unit/integration tests** across 42 modular test files - 86% code coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
|
|
805
816
|
- **56 fuzz tests** - Malformed YAML, invalid JSON, binary files, ReDoS, unicode, 10MB inputs
|
|
806
817
|
- **Datadog 17K benchmark** - 17,922 real malware samples, 88.2% raw TPR, ~100% on JS/Node.js malware (2,077 out-of-scope misses: phishing, binaries, corrected libs)
|
|
807
|
-
- **
|
|
818
|
+
- **83 adversarial/holdout samples** - 43 adversarial + 40 holdout, 82/83 detection rate (98.8% ADR). 1 documented miss: `require-cache-poison` (accepted trade-off)
|
|
808
819
|
- **Ground truth validation** - 51 real-world attacks (45/49 detected = 91.8% TPR). 4 out-of-scope: browser-only (3) + FP-risky (1)
|
|
809
|
-
- **False positive validation** -
|
|
820
|
+
- **False positive validation** - 6.0% FPR global (32/529) on real npm source code via `npm pack`
|
|
810
821
|
- **ESLint security audit** - `eslint-plugin-security` with 14 rules enabled
|
|
811
822
|
|
|
812
823
|
---
|
package/iocs/builtin.yaml
CHANGED
|
@@ -7,39 +7,108 @@ packages:
|
|
|
7
7
|
version: "4.1.1"
|
|
8
8
|
source: shai-hulud-v1
|
|
9
9
|
- name: "ng2-file-upload"
|
|
10
|
-
version: "
|
|
10
|
+
version: "7.0.2"
|
|
11
|
+
source: shai-hulud-v1
|
|
12
|
+
- name: "ng2-file-upload"
|
|
13
|
+
version: "7.0.3"
|
|
14
|
+
source: shai-hulud-v1
|
|
15
|
+
- name: "ng2-file-upload"
|
|
16
|
+
version: "8.0.1"
|
|
17
|
+
source: shai-hulud-v1
|
|
18
|
+
- name: "ng2-file-upload"
|
|
19
|
+
version: "8.0.2"
|
|
20
|
+
source: shai-hulud-v1
|
|
21
|
+
- name: "ng2-file-upload"
|
|
22
|
+
version: "8.0.3"
|
|
23
|
+
source: shai-hulud-v1
|
|
24
|
+
- name: "ng2-file-upload"
|
|
25
|
+
version: "9.0.1"
|
|
11
26
|
source: shai-hulud-v1
|
|
12
27
|
- name: "ngx-bootstrap"
|
|
13
|
-
version: "
|
|
28
|
+
version: "18.1.4"
|
|
29
|
+
source: shai-hulud-v1
|
|
30
|
+
- name: "ngx-bootstrap"
|
|
31
|
+
version: "19.0.3"
|
|
32
|
+
source: shai-hulud-v1
|
|
33
|
+
- name: "ngx-bootstrap"
|
|
34
|
+
version: "19.0.4"
|
|
35
|
+
source: shai-hulud-v1
|
|
36
|
+
- name: "ngx-bootstrap"
|
|
37
|
+
version: "20.0.3"
|
|
38
|
+
source: shai-hulud-v1
|
|
39
|
+
- name: "ngx-bootstrap"
|
|
40
|
+
version: "20.0.4"
|
|
41
|
+
source: shai-hulud-v1
|
|
42
|
+
- name: "ngx-bootstrap"
|
|
43
|
+
version: "20.0.5"
|
|
44
|
+
source: shai-hulud-v1
|
|
45
|
+
- name: "ngx-bootstrap"
|
|
46
|
+
version: "20.0.6"
|
|
14
47
|
source: shai-hulud-v1
|
|
15
48
|
|
|
16
49
|
# Shai-Hulud v2 (novembre 2025)
|
|
17
50
|
- name: "@asyncapi/specs"
|
|
18
|
-
version: "
|
|
51
|
+
version: "6.8.2"
|
|
52
|
+
source: shai-hulud-v2
|
|
53
|
+
- name: "@asyncapi/specs"
|
|
54
|
+
version: "6.8.3"
|
|
55
|
+
source: shai-hulud-v2
|
|
56
|
+
- name: "@asyncapi/specs"
|
|
57
|
+
version: "6.9.1"
|
|
58
|
+
source: shai-hulud-v2
|
|
59
|
+
- name: "@asyncapi/specs"
|
|
60
|
+
version: "6.10.1"
|
|
19
61
|
source: shai-hulud-v2
|
|
20
62
|
- name: "@asyncapi/openapi-schema-parser"
|
|
21
|
-
version: "
|
|
63
|
+
version: "3.0.25"
|
|
64
|
+
source: shai-hulud-v2
|
|
65
|
+
- name: "@asyncapi/openapi-schema-parser"
|
|
66
|
+
version: "3.0.26"
|
|
22
67
|
source: shai-hulud-v2
|
|
23
68
|
- name: "get-them-args"
|
|
24
|
-
version: "
|
|
69
|
+
version: "1.3.3"
|
|
25
70
|
source: shai-hulud-v2
|
|
26
71
|
- name: "kill-port"
|
|
27
|
-
version: "
|
|
72
|
+
version: "2.0.2"
|
|
73
|
+
source: shai-hulud-v2
|
|
74
|
+
- name: "kill-port"
|
|
75
|
+
version: "2.0.3"
|
|
28
76
|
source: shai-hulud-v2
|
|
29
77
|
- name: "shell-exec"
|
|
30
|
-
version: "
|
|
78
|
+
version: "1.1.3"
|
|
79
|
+
source: shai-hulud-v2
|
|
80
|
+
- name: "shell-exec"
|
|
81
|
+
version: "1.1.4"
|
|
31
82
|
source: shai-hulud-v2
|
|
32
83
|
- name: "posthog-node"
|
|
33
|
-
version: "
|
|
84
|
+
version: "4.18.1"
|
|
85
|
+
source: shai-hulud-v2
|
|
86
|
+
- name: "posthog-node"
|
|
87
|
+
version: "5.11.3"
|
|
88
|
+
source: shai-hulud-v2
|
|
89
|
+
- name: "posthog-node"
|
|
90
|
+
version: "5.13.3"
|
|
34
91
|
source: shai-hulud-v2
|
|
35
92
|
- name: "posthog-js"
|
|
36
|
-
version: "
|
|
93
|
+
version: "1.297.3"
|
|
37
94
|
source: shai-hulud-v2
|
|
38
95
|
- name: "@postman/tunnel-agent"
|
|
39
|
-
version: "
|
|
96
|
+
version: "0.6.5"
|
|
97
|
+
source: shai-hulud-v2
|
|
98
|
+
- name: "@postman/tunnel-agent"
|
|
99
|
+
version: "0.6.6"
|
|
100
|
+
source: shai-hulud-v2
|
|
101
|
+
- name: "@postman/tunnel-agent"
|
|
102
|
+
version: "0.6.7"
|
|
40
103
|
source: shai-hulud-v2
|
|
41
104
|
- name: "@zapier/secret-scrubber"
|
|
42
|
-
version: "
|
|
105
|
+
version: "1.1.3"
|
|
106
|
+
source: shai-hulud-v2
|
|
107
|
+
- name: "@zapier/secret-scrubber"
|
|
108
|
+
version: "1.1.4"
|
|
109
|
+
source: shai-hulud-v2
|
|
110
|
+
- name: "@zapier/secret-scrubber"
|
|
111
|
+
version: "1.1.5"
|
|
43
112
|
source: shai-hulud-v2
|
|
44
113
|
|
|
45
114
|
# Shai-Hulud v3 Golden Path (28 decembre 2025)
|
package/iocs/packages.yaml
CHANGED
|
@@ -21,9 +21,21 @@ packages:
|
|
|
21
21
|
- https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack
|
|
22
22
|
mitre: T1195.002
|
|
23
23
|
|
|
24
|
-
- id: SHAI-HULUD-V1-
|
|
24
|
+
- id: SHAI-HULUD-V1-002a
|
|
25
25
|
name: "ng2-file-upload"
|
|
26
|
-
version: "
|
|
26
|
+
version: "7.0.2"
|
|
27
|
+
severity: critical
|
|
28
|
+
confidence: high
|
|
29
|
+
source: shai-hulud-v1
|
|
30
|
+
introduced: "2025-09-01"
|
|
31
|
+
description: "Package compromis par Shai-Hulud v1"
|
|
32
|
+
references:
|
|
33
|
+
- https://blog.phylum.io/shai-hulud-npm-worm
|
|
34
|
+
mitre: T1195.002
|
|
35
|
+
|
|
36
|
+
- id: SHAI-HULUD-V1-002b
|
|
37
|
+
name: "ng2-file-upload"
|
|
38
|
+
version: "7.0.3"
|
|
27
39
|
severity: critical
|
|
28
40
|
confidence: high
|
|
29
41
|
source: shai-hulud-v1
|
|
@@ -33,9 +45,129 @@ packages:
|
|
|
33
45
|
- https://blog.phylum.io/shai-hulud-npm-worm
|
|
34
46
|
mitre: T1195.002
|
|
35
47
|
|
|
36
|
-
- id: SHAI-HULUD-V1-
|
|
48
|
+
- id: SHAI-HULUD-V1-002c
|
|
49
|
+
name: "ng2-file-upload"
|
|
50
|
+
version: "8.0.1"
|
|
51
|
+
severity: critical
|
|
52
|
+
confidence: high
|
|
53
|
+
source: shai-hulud-v1
|
|
54
|
+
introduced: "2025-09-01"
|
|
55
|
+
description: "Package compromis par Shai-Hulud v1"
|
|
56
|
+
references:
|
|
57
|
+
- https://blog.phylum.io/shai-hulud-npm-worm
|
|
58
|
+
mitre: T1195.002
|
|
59
|
+
|
|
60
|
+
- id: SHAI-HULUD-V1-002d
|
|
61
|
+
name: "ng2-file-upload"
|
|
62
|
+
version: "8.0.2"
|
|
63
|
+
severity: critical
|
|
64
|
+
confidence: high
|
|
65
|
+
source: shai-hulud-v1
|
|
66
|
+
introduced: "2025-09-01"
|
|
67
|
+
description: "Package compromis par Shai-Hulud v1"
|
|
68
|
+
references:
|
|
69
|
+
- https://blog.phylum.io/shai-hulud-npm-worm
|
|
70
|
+
mitre: T1195.002
|
|
71
|
+
|
|
72
|
+
- id: SHAI-HULUD-V1-002e
|
|
73
|
+
name: "ng2-file-upload"
|
|
74
|
+
version: "8.0.3"
|
|
75
|
+
severity: critical
|
|
76
|
+
confidence: high
|
|
77
|
+
source: shai-hulud-v1
|
|
78
|
+
introduced: "2025-09-01"
|
|
79
|
+
description: "Package compromis par Shai-Hulud v1"
|
|
80
|
+
references:
|
|
81
|
+
- https://blog.phylum.io/shai-hulud-npm-worm
|
|
82
|
+
mitre: T1195.002
|
|
83
|
+
|
|
84
|
+
- id: SHAI-HULUD-V1-002f
|
|
85
|
+
name: "ng2-file-upload"
|
|
86
|
+
version: "9.0.1"
|
|
87
|
+
severity: critical
|
|
88
|
+
confidence: high
|
|
89
|
+
source: shai-hulud-v1
|
|
90
|
+
introduced: "2025-09-01"
|
|
91
|
+
description: "Package compromis par Shai-Hulud v1"
|
|
92
|
+
references:
|
|
93
|
+
- https://blog.phylum.io/shai-hulud-npm-worm
|
|
94
|
+
mitre: T1195.002
|
|
95
|
+
|
|
96
|
+
- id: SHAI-HULUD-V1-003a
|
|
37
97
|
name: "ngx-bootstrap"
|
|
38
|
-
version: "
|
|
98
|
+
version: "18.1.4"
|
|
99
|
+
severity: critical
|
|
100
|
+
confidence: high
|
|
101
|
+
source: shai-hulud-v1
|
|
102
|
+
introduced: "2025-09-01"
|
|
103
|
+
description: "Package compromis par Shai-Hulud v1"
|
|
104
|
+
references:
|
|
105
|
+
- https://blog.phylum.io/shai-hulud-npm-worm
|
|
106
|
+
mitre: T1195.002
|
|
107
|
+
|
|
108
|
+
- id: SHAI-HULUD-V1-003b
|
|
109
|
+
name: "ngx-bootstrap"
|
|
110
|
+
version: "19.0.3"
|
|
111
|
+
severity: critical
|
|
112
|
+
confidence: high
|
|
113
|
+
source: shai-hulud-v1
|
|
114
|
+
introduced: "2025-09-01"
|
|
115
|
+
description: "Package compromis par Shai-Hulud v1"
|
|
116
|
+
references:
|
|
117
|
+
- https://blog.phylum.io/shai-hulud-npm-worm
|
|
118
|
+
mitre: T1195.002
|
|
119
|
+
|
|
120
|
+
- id: SHAI-HULUD-V1-003c
|
|
121
|
+
name: "ngx-bootstrap"
|
|
122
|
+
version: "19.0.4"
|
|
123
|
+
severity: critical
|
|
124
|
+
confidence: high
|
|
125
|
+
source: shai-hulud-v1
|
|
126
|
+
introduced: "2025-09-01"
|
|
127
|
+
description: "Package compromis par Shai-Hulud v1"
|
|
128
|
+
references:
|
|
129
|
+
- https://blog.phylum.io/shai-hulud-npm-worm
|
|
130
|
+
mitre: T1195.002
|
|
131
|
+
|
|
132
|
+
- id: SHAI-HULUD-V1-003d
|
|
133
|
+
name: "ngx-bootstrap"
|
|
134
|
+
version: "20.0.3"
|
|
135
|
+
severity: critical
|
|
136
|
+
confidence: high
|
|
137
|
+
source: shai-hulud-v1
|
|
138
|
+
introduced: "2025-09-01"
|
|
139
|
+
description: "Package compromis par Shai-Hulud v1"
|
|
140
|
+
references:
|
|
141
|
+
- https://blog.phylum.io/shai-hulud-npm-worm
|
|
142
|
+
mitre: T1195.002
|
|
143
|
+
|
|
144
|
+
- id: SHAI-HULUD-V1-003e
|
|
145
|
+
name: "ngx-bootstrap"
|
|
146
|
+
version: "20.0.4"
|
|
147
|
+
severity: critical
|
|
148
|
+
confidence: high
|
|
149
|
+
source: shai-hulud-v1
|
|
150
|
+
introduced: "2025-09-01"
|
|
151
|
+
description: "Package compromis par Shai-Hulud v1"
|
|
152
|
+
references:
|
|
153
|
+
- https://blog.phylum.io/shai-hulud-npm-worm
|
|
154
|
+
mitre: T1195.002
|
|
155
|
+
|
|
156
|
+
- id: SHAI-HULUD-V1-003f
|
|
157
|
+
name: "ngx-bootstrap"
|
|
158
|
+
version: "20.0.5"
|
|
159
|
+
severity: critical
|
|
160
|
+
confidence: high
|
|
161
|
+
source: shai-hulud-v1
|
|
162
|
+
introduced: "2025-09-01"
|
|
163
|
+
description: "Package compromis par Shai-Hulud v1"
|
|
164
|
+
references:
|
|
165
|
+
- https://blog.phylum.io/shai-hulud-npm-worm
|
|
166
|
+
mitre: T1195.002
|
|
167
|
+
|
|
168
|
+
- id: SHAI-HULUD-V1-003g
|
|
169
|
+
name: "ngx-bootstrap"
|
|
170
|
+
version: "20.0.6"
|
|
39
171
|
severity: critical
|
|
40
172
|
confidence: high
|
|
41
173
|
source: shai-hulud-v1
|
|
@@ -48,9 +180,45 @@ packages:
|
|
|
48
180
|
# ============================================
|
|
49
181
|
# SHAI-HULUD v2 "The Second Coming" (Novembre 2025)
|
|
50
182
|
# ============================================
|
|
51
|
-
- id: SHAI-HULUD-V2-
|
|
183
|
+
- id: SHAI-HULUD-V2-001a
|
|
52
184
|
name: "@asyncapi/specs"
|
|
53
|
-
version: "
|
|
185
|
+
version: "6.8.2"
|
|
186
|
+
severity: critical
|
|
187
|
+
confidence: high
|
|
188
|
+
source: shai-hulud-v2
|
|
189
|
+
introduced: "2025-11-01"
|
|
190
|
+
description: "Package compromis par Shai-Hulud v2 - inclut dead man's switch"
|
|
191
|
+
references:
|
|
192
|
+
- https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack
|
|
193
|
+
mitre: T1195.002
|
|
194
|
+
|
|
195
|
+
- id: SHAI-HULUD-V2-001b
|
|
196
|
+
name: "@asyncapi/specs"
|
|
197
|
+
version: "6.8.3"
|
|
198
|
+
severity: critical
|
|
199
|
+
confidence: high
|
|
200
|
+
source: shai-hulud-v2
|
|
201
|
+
introduced: "2025-11-01"
|
|
202
|
+
description: "Package compromis par Shai-Hulud v2 - inclut dead man's switch"
|
|
203
|
+
references:
|
|
204
|
+
- https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack
|
|
205
|
+
mitre: T1195.002
|
|
206
|
+
|
|
207
|
+
- id: SHAI-HULUD-V2-001c
|
|
208
|
+
name: "@asyncapi/specs"
|
|
209
|
+
version: "6.9.1"
|
|
210
|
+
severity: critical
|
|
211
|
+
confidence: high
|
|
212
|
+
source: shai-hulud-v2
|
|
213
|
+
introduced: "2025-11-01"
|
|
214
|
+
description: "Package compromis par Shai-Hulud v2 - inclut dead man's switch"
|
|
215
|
+
references:
|
|
216
|
+
- https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack
|
|
217
|
+
mitre: T1195.002
|
|
218
|
+
|
|
219
|
+
- id: SHAI-HULUD-V2-001d
|
|
220
|
+
name: "@asyncapi/specs"
|
|
221
|
+
version: "6.10.1"
|
|
54
222
|
severity: critical
|
|
55
223
|
confidence: high
|
|
56
224
|
source: shai-hulud-v2
|
|
@@ -62,7 +230,7 @@ packages:
|
|
|
62
230
|
|
|
63
231
|
- id: SHAI-HULUD-V2-002
|
|
64
232
|
name: "get-them-args"
|
|
65
|
-
version: "
|
|
233
|
+
version: "1.3.3"
|
|
66
234
|
severity: critical
|
|
67
235
|
confidence: high
|
|
68
236
|
source: shai-hulud-v2
|
|
@@ -72,9 +240,21 @@ packages:
|
|
|
72
240
|
- https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack
|
|
73
241
|
mitre: T1195.002
|
|
74
242
|
|
|
75
|
-
- id: SHAI-HULUD-V2-
|
|
243
|
+
- id: SHAI-HULUD-V2-003a
|
|
76
244
|
name: "kill-port"
|
|
77
|
-
version: "
|
|
245
|
+
version: "2.0.2"
|
|
246
|
+
severity: critical
|
|
247
|
+
confidence: high
|
|
248
|
+
source: shai-hulud-v2
|
|
249
|
+
introduced: "2025-11-01"
|
|
250
|
+
description: "Package compromis par Shai-Hulud v2"
|
|
251
|
+
references:
|
|
252
|
+
- https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack
|
|
253
|
+
mitre: T1195.002
|
|
254
|
+
|
|
255
|
+
- id: SHAI-HULUD-V2-003b
|
|
256
|
+
name: "kill-port"
|
|
257
|
+
version: "2.0.3"
|
|
78
258
|
severity: critical
|
|
79
259
|
confidence: high
|
|
80
260
|
source: shai-hulud-v2
|
|
@@ -84,9 +264,33 @@ packages:
|
|
|
84
264
|
- https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack
|
|
85
265
|
mitre: T1195.002
|
|
86
266
|
|
|
87
|
-
- id: SHAI-HULUD-V2-
|
|
267
|
+
- id: SHAI-HULUD-V2-004a
|
|
88
268
|
name: "posthog-node"
|
|
89
|
-
version: "
|
|
269
|
+
version: "4.18.1"
|
|
270
|
+
severity: critical
|
|
271
|
+
confidence: high
|
|
272
|
+
source: shai-hulud-v2
|
|
273
|
+
introduced: "2025-11-01"
|
|
274
|
+
description: "Package compromis par Shai-Hulud v2"
|
|
275
|
+
references:
|
|
276
|
+
- https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack
|
|
277
|
+
mitre: T1195.002
|
|
278
|
+
|
|
279
|
+
- id: SHAI-HULUD-V2-004b
|
|
280
|
+
name: "posthog-node"
|
|
281
|
+
version: "5.11.3"
|
|
282
|
+
severity: critical
|
|
283
|
+
confidence: high
|
|
284
|
+
source: shai-hulud-v2
|
|
285
|
+
introduced: "2025-11-01"
|
|
286
|
+
description: "Package compromis par Shai-Hulud v2"
|
|
287
|
+
references:
|
|
288
|
+
- https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack
|
|
289
|
+
mitre: T1195.002
|
|
290
|
+
|
|
291
|
+
- id: SHAI-HULUD-V2-004c
|
|
292
|
+
name: "posthog-node"
|
|
293
|
+
version: "5.13.3"
|
|
90
294
|
severity: critical
|
|
91
295
|
confidence: high
|
|
92
296
|
source: shai-hulud-v2
|
|
@@ -98,7 +302,7 @@ packages:
|
|
|
98
302
|
|
|
99
303
|
- id: SHAI-HULUD-V2-005
|
|
100
304
|
name: "posthog-js"
|
|
101
|
-
version: "
|
|
305
|
+
version: "1.297.3"
|
|
102
306
|
severity: critical
|
|
103
307
|
confidence: high
|
|
104
308
|
source: shai-hulud-v2
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"target": "npm/evil-pkg@1.0.0",
|
|
3
|
+
"timestamp": "2026-03-06T19:26:22.192Z",
|
|
4
|
+
"ecosystem": "npm",
|
|
5
|
+
"summary": {
|
|
6
|
+
"critical": 1,
|
|
7
|
+
"high": 0,
|
|
8
|
+
"medium": 0,
|
|
9
|
+
"low": 0,
|
|
10
|
+
"total": 1,
|
|
11
|
+
"riskLevel": "CRITICAL",
|
|
12
|
+
"riskScore": 25
|
|
13
|
+
},
|
|
14
|
+
"threats": [
|
|
15
|
+
{
|
|
16
|
+
"type": "known_malicious_package",
|
|
17
|
+
"severity": "CRITICAL"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"target": "npm/suspect-pkg@1.0",
|
|
3
|
+
"timestamp": "2026-03-06T19:26:22.192Z",
|
|
4
|
+
"ecosystem": "npm",
|
|
5
|
+
"summary": {
|
|
6
|
+
"critical": 0,
|
|
7
|
+
"high": 1,
|
|
8
|
+
"medium": 0,
|
|
9
|
+
"low": 0,
|
|
10
|
+
"total": 1,
|
|
11
|
+
"riskLevel": "HIGH",
|
|
12
|
+
"riskScore": 15
|
|
13
|
+
},
|
|
14
|
+
"threats": [
|
|
15
|
+
{
|
|
16
|
+
"type": "dynamic_require",
|
|
17
|
+
"severity": "HIGH"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"sandbox": {
|
|
21
|
+
"score": 75,
|
|
22
|
+
"severity": "HIGH"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"target": "npm/evil-pkg@1.0.0",
|
|
3
|
+
"timestamp": "2026-03-06T19:26:22.193Z",
|
|
4
|
+
"ecosystem": "npm",
|
|
5
|
+
"summary": {
|
|
6
|
+
"critical": 1,
|
|
7
|
+
"high": 0,
|
|
8
|
+
"medium": 0,
|
|
9
|
+
"low": 0,
|
|
10
|
+
"total": 1,
|
|
11
|
+
"riskLevel": "CRITICAL",
|
|
12
|
+
"riskScore": 25
|
|
13
|
+
},
|
|
14
|
+
"threats": [
|
|
15
|
+
{
|
|
16
|
+
"type": "known_malicious_package",
|
|
17
|
+
"severity": "CRITICAL"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"target": "npm/evil-pkg@2.0.0",
|
|
3
|
+
"timestamp": "2026-03-06T19:26:22.572Z",
|
|
4
|
+
"ecosystem": "npm",
|
|
5
|
+
"summary": {
|
|
6
|
+
"critical": 1,
|
|
7
|
+
"high": 0,
|
|
8
|
+
"medium": 0,
|
|
9
|
+
"low": 0,
|
|
10
|
+
"total": 1,
|
|
11
|
+
"riskLevel": "CRITICAL",
|
|
12
|
+
"riskScore": 25
|
|
13
|
+
},
|
|
14
|
+
"threats": [
|
|
15
|
+
{
|
|
16
|
+
"type": "known_malicious_package",
|
|
17
|
+
"severity": "CRITICAL"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"sandbox": {
|
|
21
|
+
"score": 85,
|
|
22
|
+
"severity": "CRITICAL"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"date": "2026-03-06",
|
|
3
|
+
"timestamp": "2026-03-06T19:26:22.664Z",
|
|
4
|
+
"embed": {
|
|
5
|
+
"embeds": [
|
|
6
|
+
{
|
|
7
|
+
"title": "📊 MUAD'DIB Daily Report",
|
|
8
|
+
"color": 3447003,
|
|
9
|
+
"fields": [
|
|
10
|
+
{
|
|
11
|
+
"name": "Packages Scanned",
|
|
12
|
+
"value": "3",
|
|
13
|
+
"inline": true
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "Clean",
|
|
17
|
+
"value": "1",
|
|
18
|
+
"inline": true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "Suspects",
|
|
22
|
+
"value": "0",
|
|
23
|
+
"inline": true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "Errors",
|
|
27
|
+
"value": "0",
|
|
28
|
+
"inline": true
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "Avg Scan Time",
|
|
32
|
+
"value": "2.0s/pkg",
|
|
33
|
+
"inline": true
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "Top Suspects",
|
|
37
|
+
"value": "1. **npm/test-dedup-detection-1772825182189@1.0.0** — 1 finding(s)",
|
|
38
|
+
"inline": false
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"footer": {
|
|
42
|
+
"text": "MUAD'DIB - Daily summary | 2026-03-06 19:26:22 UTC"
|
|
43
|
+
},
|
|
44
|
+
"timestamp": "2026-03-06T19:26:22.664Z"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"metrics": {
|
|
49
|
+
"scanned": 3,
|
|
50
|
+
"clean": 1,
|
|
51
|
+
"suspect": 0,
|
|
52
|
+
"errors": 0,
|
|
53
|
+
"avgScanTimeMs": 2000,
|
|
54
|
+
"suspectByTier": {
|
|
55
|
+
"t1": 0,
|
|
56
|
+
"t2": 0,
|
|
57
|
+
"t3": 0
|
|
58
|
+
},
|
|
59
|
+
"topSuspects": []
|
|
60
|
+
}
|
|
61
|
+
}
|
package/package.json
CHANGED
package/src/ioc/scraper.js
CHANGED
|
@@ -958,7 +958,7 @@ async function scrapeSnykMalware() {
|
|
|
958
958
|
{ name: 'event-stream', version: '3.3.6', description: 'Flatmap-stream backdoor (2018)' },
|
|
959
959
|
{ name: 'flatmap-stream', version: '*', description: 'Malicious dependency of event-stream' },
|
|
960
960
|
{ name: 'eslint-scope', version: '3.7.2', description: 'Credential theft (2018)' },
|
|
961
|
-
{ name: 'eslint-config-eslint', version: '
|
|
961
|
+
{ name: 'eslint-config-eslint', version: '5.0.2', description: 'Credential theft (2018)' },
|
|
962
962
|
{ name: 'getcookies', version: '*', description: 'Backdoor malware' },
|
|
963
963
|
{ name: 'mailparser', version: '2.3.0', description: 'Compromised version' },
|
|
964
964
|
{ name: 'node-ipc', version: '10.1.1', description: 'Protestware - file deletion' },
|
|
@@ -56,6 +56,14 @@ const SAFE_STRINGS = [
|
|
|
56
56
|
'npmjs.com'
|
|
57
57
|
];
|
|
58
58
|
|
|
59
|
+
// Domains where fetch is legitimate (not C2) — used to suppress download_exec_binary compound
|
|
60
|
+
const SAFE_FETCH_DOMAINS = [
|
|
61
|
+
'registry.npmjs.org', 'npmjs.com',
|
|
62
|
+
'github.com', 'objects.githubusercontent.com', 'raw.githubusercontent.com',
|
|
63
|
+
'nodejs.org', 'yarnpkg.com',
|
|
64
|
+
'pypi.org', 'files.pythonhosted.org'
|
|
65
|
+
];
|
|
66
|
+
|
|
59
67
|
// Credential-stealing CLI commands (s1ngularity/Nx, Shai-Hulud)
|
|
60
68
|
const CREDENTIAL_CLI_COMMANDS = [
|
|
61
69
|
'gh auth token',
|
|
@@ -850,12 +858,24 @@ function handleCallExpression(node, ctx) {
|
|
|
850
858
|
});
|
|
851
859
|
} else {
|
|
852
860
|
const isConstant = hasOnlyStringLiteralArgs(node);
|
|
861
|
+
let severity = isConstant ? 'LOW' : 'HIGH';
|
|
862
|
+
let message = isConstant
|
|
863
|
+
? 'eval() with constant string literal (low risk, globalThis polyfill pattern).'
|
|
864
|
+
: 'Dangerous call "eval" with dynamic expression detected.';
|
|
865
|
+
|
|
866
|
+
// Audit fix: even string-literal eval is dangerous if content contains dangerous APIs
|
|
867
|
+
if (isConstant && node.arguments[0]?.value) {
|
|
868
|
+
const val = node.arguments[0].value;
|
|
869
|
+
if (/\b(require|import|exec|execSync|spawn|child_process|\.readFile|\.writeFile|process\.env|\.homedir)\b/.test(val)) {
|
|
870
|
+
severity = 'HIGH';
|
|
871
|
+
message = `eval() with dangerous API in string literal: "${val.substring(0, 100)}"`;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
853
875
|
ctx.threats.push({
|
|
854
876
|
type: 'dangerous_call_eval',
|
|
855
|
-
severity
|
|
856
|
-
message
|
|
857
|
-
? 'eval() with constant string literal (low risk, globalThis polyfill pattern).'
|
|
858
|
-
: 'Dangerous call "eval" with dynamic expression detected.',
|
|
877
|
+
severity,
|
|
878
|
+
message,
|
|
859
879
|
file: ctx.relFile
|
|
860
880
|
});
|
|
861
881
|
}
|
|
@@ -1182,15 +1202,22 @@ function handleLiteral(node, ctx) {
|
|
|
1182
1202
|
}
|
|
1183
1203
|
}
|
|
1184
1204
|
|
|
1185
|
-
// Detect AI agent dangerous flags as string literals
|
|
1205
|
+
// Detect AI agent dangerous flags as string literals (MEDIUM signal only —
|
|
1206
|
+
// CRITICAL reserved for CallExpression context where flag is actually used in exec/spawn)
|
|
1186
1207
|
for (const flag of AI_AGENT_DANGEROUS_FLAGS) {
|
|
1187
1208
|
if (node.value === flag) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
severity
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1209
|
+
// Skip if already detected in a CallExpression context (avoid double-counting)
|
|
1210
|
+
const alreadyDetected = ctx.threats.some(t =>
|
|
1211
|
+
t.type === 'ai_agent_abuse' && t.severity === 'CRITICAL' && t.file === ctx.relFile
|
|
1212
|
+
);
|
|
1213
|
+
if (!alreadyDetected) {
|
|
1214
|
+
ctx.threats.push({
|
|
1215
|
+
type: 'ai_agent_abuse',
|
|
1216
|
+
severity: 'MEDIUM',
|
|
1217
|
+
message: `AI agent security bypass flag "${flag}" referenced in code — verify it is not used in exec/spawn invocations.`,
|
|
1218
|
+
file: ctx.relFile
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1194
1221
|
}
|
|
1195
1222
|
}
|
|
1196
1223
|
|
|
@@ -1495,7 +1522,8 @@ function handlePostWalk(ctx) {
|
|
|
1495
1522
|
}
|
|
1496
1523
|
|
|
1497
1524
|
// Wave 4: Download-execute-cleanup — https download + chmod executable + execSync + unlink
|
|
1498
|
-
|
|
1525
|
+
// Exclude when all URLs in the file point to safe registries (npm, GitHub, nodejs.org)
|
|
1526
|
+
if (ctx.hasRemoteFetch && ctx.hasChmodExecutable && ctx.hasExecSyncCall && !ctx.fetchOnlySafeDomains) {
|
|
1499
1527
|
ctx.threats.push({
|
|
1500
1528
|
type: 'download_exec_binary',
|
|
1501
1529
|
severity: 'CRITICAL',
|
package/src/scanner/ast.js
CHANGED
|
@@ -101,6 +101,8 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
101
101
|
ideConfigPathVars: new Map(),
|
|
102
102
|
// Wave 4: compound detection — fetch + decrypt + eval chain
|
|
103
103
|
hasRemoteFetch: /\bhttps?\.(get|request)\b/.test(content) || /\bfetch\s*\(/.test(content),
|
|
104
|
+
// Safe domain exclusion: if ALL URLs in file are from known registries, suppress download_exec_binary
|
|
105
|
+
fetchOnlySafeDomains: false, // computed below after URL extraction
|
|
104
106
|
hasCryptoDecipher: /\bcreateDecipher(iv)?\s*\(/.test(content),
|
|
105
107
|
// Wave 4: native addon camouflage signals
|
|
106
108
|
hasRequireNodeFile: false,
|
|
@@ -111,10 +113,33 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
111
113
|
hasRunOnInContent: /\brunOn\b|\bfolderOpen\b/.test(content),
|
|
112
114
|
hasWriteFileSyncInContent: /\bwriteFileSync\b|\bwriteFile\s*\(/.test(content),
|
|
113
115
|
// Wave 4: MCP content keyword detection (must also have writeFileSync in same file)
|
|
116
|
+
// Content-level MCP detection: MCP keyword + writeFileSync + MCP config path in same file
|
|
117
|
+
// Path co-occurrence prevents FPs where a file reads MCP config but writes elsewhere.
|
|
118
|
+
// Read-only pattern (readFileSync without writeFileSync to MCP) is not injection.
|
|
114
119
|
hasMcpContentKeywords: (/\bmcpServers\b/.test(content) || /\bmcp\.json\b/.test(content) || /\bclaude_desktop_config\b/.test(content)) &&
|
|
115
|
-
/\bwriteFileSync\b|\bwriteFile\s*\(/.test(content)
|
|
120
|
+
/\bwriteFileSync\b|\bwriteFile\s*\(/.test(content) &&
|
|
121
|
+
(/\.claude[/\\]/.test(content) || /\.cursor[/\\]/.test(content) || /\.vscode[/\\]/.test(content) || /\.windsurf[/\\]/.test(content) || /\.codeium[/\\]/.test(content) || /\.continue[/\\]/.test(content) || /claude_desktop_config/.test(content) || /\bmcp\.json\b/.test(content))
|
|
116
122
|
};
|
|
117
123
|
|
|
124
|
+
// Compute fetchOnlySafeDomains: check if ALL URLs in file point to known registries
|
|
125
|
+
if (ctx.hasRemoteFetch) {
|
|
126
|
+
const urlMatches = content.match(/https?:\/\/[^\s'"`)]+/g) || [];
|
|
127
|
+
const SAFE_FETCH_DOMAINS = [
|
|
128
|
+
'registry.npmjs.org', 'npmjs.com',
|
|
129
|
+
'github.com', 'objects.githubusercontent.com', 'raw.githubusercontent.com',
|
|
130
|
+
'nodejs.org', 'yarnpkg.com',
|
|
131
|
+
'pypi.org', 'files.pythonhosted.org'
|
|
132
|
+
];
|
|
133
|
+
if (urlMatches.length > 0 && urlMatches.every(u => {
|
|
134
|
+
try {
|
|
135
|
+
const hostname = new URL(u).hostname;
|
|
136
|
+
return SAFE_FETCH_DOMAINS.some(d => hostname === d || hostname.endsWith('.' + d));
|
|
137
|
+
} catch { return false; }
|
|
138
|
+
})) {
|
|
139
|
+
ctx.fetchOnlySafeDomains = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
118
143
|
walk.simple(ast, {
|
|
119
144
|
VariableDeclarator(node) { handleVariableDeclarator(node, ctx); },
|
|
120
145
|
CallExpression(node) { handleCallExpression(node, ctx); },
|
package/src/scanner/typosquat.js
CHANGED
|
@@ -108,7 +108,11 @@ const WHITELIST = new Set([
|
|
|
108
108
|
'docdash', // resembles lodash
|
|
109
109
|
'yarpm', // resembles yargs
|
|
110
110
|
'canvg', // resembles canvas
|
|
111
|
-
'obug'
|
|
111
|
+
'obug', // internal sub-dependency
|
|
112
|
+
|
|
113
|
+
// FPR P4: Benign packages falsely flagged as typosquat in evaluation
|
|
114
|
+
'mocks', // karma dep, resembles mocha (wrong_char)
|
|
115
|
+
'reactor' // stencil dep, resembles react (suffix)
|
|
112
116
|
]);
|
|
113
117
|
|
|
114
118
|
|
package/src/scoring.js
CHANGED
|
@@ -106,26 +106,48 @@ const FP_COUNT_THRESHOLDS = {
|
|
|
106
106
|
dynamic_require: { maxCount: 10, from: 'HIGH', to: 'LOW' },
|
|
107
107
|
dangerous_call_function: { maxCount: 5, from: 'MEDIUM', to: 'LOW' },
|
|
108
108
|
require_cache_poison: { maxCount: 3, from: 'CRITICAL', to: 'LOW' },
|
|
109
|
-
suspicious_dataflow: { maxCount:
|
|
109
|
+
suspicious_dataflow: { maxCount: 3, to: 'LOW' },
|
|
110
110
|
obfuscation_detected: { maxCount: 3, to: 'LOW' },
|
|
111
111
|
module_compile_dynamic: { maxCount: 3, from: 'CRITICAL', to: 'LOW' },
|
|
112
112
|
module_compile: { maxCount: 3, from: 'CRITICAL', to: 'LOW' },
|
|
113
|
-
zlib_inflate_eval: { maxCount: 2, from: 'CRITICAL', to: 'LOW' }
|
|
113
|
+
zlib_inflate_eval: { maxCount: 2, from: 'CRITICAL', to: 'LOW' },
|
|
114
|
+
// P4: plugin loaders legitimately use many dynamic imports (webpack, eslint, knex, gatsby)
|
|
115
|
+
dynamic_import: { maxCount: 5, from: 'HIGH', to: 'LOW' },
|
|
116
|
+
// P4: hash algorithms contain bit manipulation that triggers obfuscation heuristics
|
|
117
|
+
js_obfuscation_pattern: { maxCount: 1, from: 'HIGH', to: 'LOW' },
|
|
118
|
+
// P4: bundled credential_tampering from minified alias resolution (jspdf, lerna)
|
|
119
|
+
credential_tampering: { maxCount: 5, to: 'LOW' }
|
|
114
120
|
};
|
|
115
121
|
|
|
116
|
-
// Types exempt from dist/ downgrade — IOC matches
|
|
122
|
+
// Types exempt from dist/ downgrade — IOC matches, lifecycle scripts, and
|
|
123
|
+
// high-confidence compound detections are always real even in dist/ files
|
|
117
124
|
const DIST_EXEMPT_TYPES = new Set([
|
|
118
125
|
'ioc_match', 'known_malicious_package', 'pypi_malicious_package', 'shai_hulud_marker',
|
|
119
126
|
'lifecycle_script', 'lifecycle_shell_pipe',
|
|
120
|
-
'lifecycle_added_critical', 'lifecycle_added_high', 'lifecycle_modified'
|
|
127
|
+
'lifecycle_added_critical', 'lifecycle_added_high', 'lifecycle_modified',
|
|
128
|
+
// Compound detections — require multiple correlated signals, not single-pattern FPs
|
|
129
|
+
'zlib_inflate_eval', // zlib + base64 + eval (event-stream pattern)
|
|
130
|
+
'fetch_decrypt_exec', // fetch + decrypt + eval (steganographic chain)
|
|
131
|
+
'download_exec_binary', // download + chmod + exec (binary dropper)
|
|
132
|
+
'cross_file_dataflow', // credential read → network exfil across files
|
|
133
|
+
'staged_eval_decode', // eval(atob(...)) (explicit payload staging)
|
|
134
|
+
'reverse_shell' // net.Socket + connect + pipe (always malicious)
|
|
121
135
|
]);
|
|
122
136
|
|
|
123
137
|
// Regex matching dist/build/minified/bundled file paths
|
|
124
138
|
const DIST_FILE_RE = /(?:^|[/\\])(?:dist|build)[/\\]|\.min\.js$|\.bundle\.js$/i;
|
|
125
139
|
|
|
126
|
-
// Types exempt from reachability downgrade — IOC matches, lifecycle, and package-level types
|
|
140
|
+
// Types exempt from reachability downgrade — IOC matches, lifecycle, and package-level types.
|
|
141
|
+
// NOTE: Uses the base IOC/lifecycle exempt set, NOT full DIST_EXEMPT_TYPES.
|
|
142
|
+
// Compound detections (zlib_inflate_eval, staged_eval_decode, etc.) should still be
|
|
143
|
+
// downgraded if the file is truly unreachable, since unreachable code cannot execute.
|
|
144
|
+
const REACHABILITY_BASE_EXEMPT = new Set([
|
|
145
|
+
'ioc_match', 'known_malicious_package', 'pypi_malicious_package', 'shai_hulud_marker',
|
|
146
|
+
'lifecycle_script', 'lifecycle_shell_pipe',
|
|
147
|
+
'lifecycle_added_critical', 'lifecycle_added_high', 'lifecycle_modified'
|
|
148
|
+
]);
|
|
127
149
|
const REACHABILITY_EXEMPT_TYPES = new Set([
|
|
128
|
-
...
|
|
150
|
+
...REACHABILITY_BASE_EXEMPT,
|
|
129
151
|
'cross_file_dataflow',
|
|
130
152
|
'typosquat_detected', 'pypi_typosquat_detected',
|
|
131
153
|
'pypi_malicious_package',
|
|
@@ -182,6 +204,18 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
182
204
|
|
|
183
205
|
const totalThreats = threats.length;
|
|
184
206
|
|
|
207
|
+
// P4: Plugin loader pattern — packages with 5+ dynamic_require + dynamic_import combined
|
|
208
|
+
// are legitimate plugin systems (webpack, eslint, karma, knex, jasmine, gatsby).
|
|
209
|
+
// Threshold raised from >1 to >4 (audit fix: >1 was trivially exploitable).
|
|
210
|
+
const pluginLoaderCount = (typeCounts.dynamic_require || 0) + (typeCounts.dynamic_import || 0);
|
|
211
|
+
if (pluginLoaderCount > 4) {
|
|
212
|
+
for (const t of threats) {
|
|
213
|
+
if ((t.type === 'dynamic_require' || t.type === 'dynamic_import') && t.severity === 'HIGH') {
|
|
214
|
+
t.severity = 'LOW';
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
185
219
|
for (const t of threats) {
|
|
186
220
|
// Count-based downgrade: if a threat type appears too many times,
|
|
187
221
|
// it's a framework/plugin system, not malware.
|
|
@@ -190,7 +224,11 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
190
224
|
const rule = FP_COUNT_THRESHOLDS[t.type];
|
|
191
225
|
if (rule && typeCounts[t.type] > rule.maxCount && (!rule.from || t.severity === rule.from)) {
|
|
192
226
|
const typeRatio = typeCounts[t.type] / totalThreats;
|
|
193
|
-
|
|
227
|
+
// suspicious_dataflow: partial bypass of percentage guard up to 80%.
|
|
228
|
+
// Complex apps (SMTP, monitoring) have 50-80% dataflow findings — still downgrade.
|
|
229
|
+
// But if dataflow is >80% of ALL findings, it may be real targeted exfiltration.
|
|
230
|
+
// (Audit fix: full bypass was exploitable — 4+ dataflow patterns = all LOW.)
|
|
231
|
+
if (typeRatio < 0.5 || (t.type === 'suspicious_dataflow' && typeRatio < 0.8)) {
|
|
194
232
|
t.severity = rule.to;
|
|
195
233
|
}
|
|
196
234
|
}
|
|
@@ -211,17 +249,19 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
211
249
|
}
|
|
212
250
|
|
|
213
251
|
// HTTP client prototype whitelist: packages with >20 prototype_hook hits
|
|
214
|
-
// targeting HTTP
|
|
252
|
+
// targeting HTTP class names are legitimate HTTP clients/frameworks.
|
|
253
|
+
// Audit fix: narrowed regex — 'get','delete','command' matched getCredentials, deleteAccount.
|
|
215
254
|
if (t.type === 'prototype_hook' && (t.severity === 'HIGH' || t.severity === 'CRITICAL') &&
|
|
216
255
|
typeCounts.prototype_hook > 20) {
|
|
217
|
-
const HTTP_PROTO_RE = /\b(Request|Response|
|
|
256
|
+
const HTTP_PROTO_RE = /\b(Request|Response|IncomingMessage|ClientRequest|ServerResponse|fetch)\b/i;
|
|
218
257
|
if (HTTP_PROTO_RE.test(t.message)) {
|
|
219
258
|
t.severity = 'MEDIUM';
|
|
220
259
|
}
|
|
221
260
|
}
|
|
222
261
|
|
|
223
262
|
// Dist/build/minified files: bundler artifacts get severity downgraded one notch.
|
|
224
|
-
//
|
|
263
|
+
// Reduced from two-notch (audit fix): 2-notch made dist/ attacks invisible (CRITICAL→MEDIUM=3pts).
|
|
264
|
+
// Compound detections are exempt (DIST_EXEMPT_TYPES).
|
|
225
265
|
if (t.file && !DIST_EXEMPT_TYPES.has(t.type) && DIST_FILE_RE.test(t.file)) {
|
|
226
266
|
if (t.severity === 'CRITICAL') t.severity = 'HIGH';
|
|
227
267
|
else if (t.severity === 'HIGH') t.severity = 'MEDIUM';
|
|
@@ -271,9 +311,11 @@ function calculateRiskScore(deduped) {
|
|
|
271
311
|
let maxFileScore = 0;
|
|
272
312
|
let mostSuspiciousFile = null;
|
|
273
313
|
const fileScores = {};
|
|
314
|
+
const fileHasMediumPlus = {}; // P4: track files with MEDIUM+ threats for cross-file bonus
|
|
274
315
|
for (const [file, fileThreats] of fileGroups) {
|
|
275
316
|
const score = computeGroupScore(fileThreats);
|
|
276
317
|
fileScores[file] = score;
|
|
318
|
+
fileHasMediumPlus[file] = fileThreats.some(t => t.severity !== 'LOW');
|
|
277
319
|
if (score > maxFileScore) {
|
|
278
320
|
maxFileScore = score;
|
|
279
321
|
mostSuspiciousFile = file;
|
|
@@ -286,11 +328,16 @@ function calculateRiskScore(deduped) {
|
|
|
286
328
|
// 5. Cross-file bonus: aggregate signal from non-max files
|
|
287
329
|
// A package with 3 files each scoring 20 is more suspicious than 1 file scoring 20.
|
|
288
330
|
// Add 25% of each non-max file's score as a bonus, capped at 25.
|
|
289
|
-
|
|
331
|
+
// P4: Only count files that have at least one MEDIUM+ threat.
|
|
332
|
+
// Files with only LOW findings are noise in large packages and shouldn't amplify the score.
|
|
333
|
+
const bonusEligibleScores = Object.entries(fileScores)
|
|
334
|
+
.filter(([file]) => fileHasMediumPlus[file])
|
|
335
|
+
.map(([, score]) => score)
|
|
336
|
+
.sort((a, b) => b - a);
|
|
290
337
|
let crossFileBonus = 0;
|
|
291
|
-
if (
|
|
292
|
-
for (let i = 1; i <
|
|
293
|
-
crossFileBonus += Math.ceil(
|
|
338
|
+
if (bonusEligibleScores.length > 1) {
|
|
339
|
+
for (let i = 1; i < bonusEligibleScores.length; i++) {
|
|
340
|
+
crossFileBonus += Math.ceil(bonusEligibleScores[i] * 0.25);
|
|
294
341
|
}
|
|
295
342
|
crossFileBonus = Math.min(crossFileBonus, 25);
|
|
296
343
|
}
|