muaddib-scanner 2.10.39 → 2.10.41

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.
@@ -15,6 +15,8 @@ const {
15
15
 
16
16
  const { NPM_PACKAGE_REGEX } = require('../shared/constants.js');
17
17
  const { analyzePreloadLog } = require('./analyzer.js');
18
+ const { classifyDomain } = require('./network-allowlist.js');
19
+ const { parseGvisorLogs, cleanupGvisorLogs } = require('./gvisor-parser.js');
18
20
 
19
21
  const DOCKER_IMAGE = 'muaddib-sandbox';
20
22
  const CONTAINER_TIMEOUT = 120000; // 120 seconds
@@ -134,6 +136,17 @@ function imageExists() {
134
136
  }
135
137
  }
136
138
 
139
+ // ── gVisor availability check ──
140
+
141
+ function isGvisorAvailable() {
142
+ try {
143
+ const info = execSync('docker info', { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
144
+ return /\brunsc\b/.test(info);
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
137
150
  // ── Build image (with cache) ──
138
151
 
139
152
  async function buildSandboxImage() {
@@ -185,6 +198,7 @@ async function runSingleSandbox(packageName, options = {}) {
185
198
  const mode = strict ? 'strict' : 'permissive';
186
199
  const timeOffset = options.timeOffset || 0;
187
200
  const runTimeout = options.runTimeout || CONTAINER_TIMEOUT;
201
+ const gvisorMode = options.gvisor || false;
188
202
 
189
203
  return new Promise((resolve) => {
190
204
  let stdout = '';
@@ -208,6 +222,12 @@ async function runSingleSandbox(packageName, options = {}) {
208
222
  '--cap-drop=ALL'
209
223
  ];
210
224
 
225
+ // gVisor runtime: use runsc instead of default runc
226
+ if (gvisorMode) {
227
+ dockerArgs.push('--runtime=runsc');
228
+ dockerArgs.push('-e', 'MUADDIB_GVISOR=1');
229
+ }
230
+
211
231
  // Inject canary tokens as environment variables
212
232
  if (canaryTokens) {
213
233
  for (const [key, value] of Object.entries(canaryTokens)) {
@@ -238,11 +258,14 @@ async function runSingleSandbox(packageName, options = {}) {
238
258
  }
239
259
 
240
260
  // Both modes need NET_RAW for tcpdump (runs as root in entrypoint).
261
+ // gVisor mode: no tcpdump needed — gVisor captures via --strace/--log-packets.
241
262
  // Strict mode also needs NET_ADMIN for iptables network blocking.
242
263
  // SYS_PTRACE is not needed: strace traces its own child (npm install via su).
243
264
  // SETUID + SETGID required for su (privilege drop to sandboxuser).
244
265
  // CHOWN required for chown in sandbox-runner.sh.
245
- dockerArgs.push('--cap-add=NET_RAW');
266
+ if (!gvisorMode) {
267
+ dockerArgs.push('--cap-add=NET_RAW');
268
+ }
246
269
  dockerArgs.push('--cap-add=SETUID');
247
270
  dockerArgs.push('--cap-add=SETGID');
248
271
  dockerArgs.push('--cap-add=CHOWN');
@@ -269,6 +292,7 @@ async function runSingleSandbox(packageName, options = {}) {
269
292
  dockerArgs.push(mode);
270
293
 
271
294
  const proc = spawn('docker', dockerArgs);
295
+ let gvisorContainerId = null;
272
296
 
273
297
  // Timeout: kill container
274
298
  const timer = setTimeout(() => {
@@ -293,6 +317,16 @@ async function runSingleSandbox(packageName, options = {}) {
293
317
 
294
318
  proc.stderr.on('data', (data) => {
295
319
  stderr += data.toString();
320
+
321
+ // Capture container ID for gVisor log retrieval (once, while container is running)
322
+ if (gvisorMode && !gvisorContainerId) {
323
+ try {
324
+ gvisorContainerId = execFileSync('docker', ['inspect', '--format={{.Id}}', containerName], {
325
+ encoding: 'utf8', stdio: 'pipe', timeout: 5000
326
+ }).trim();
327
+ } catch { /* container not yet ready, will retry on next data event */ }
328
+ }
329
+
296
330
  // Forward sandbox progress logs (sanitize ANSI escape sequences)
297
331
  const text = data.toString().replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
298
332
  for (const line of text.split(/\r?\n/)) {
@@ -365,6 +399,43 @@ async function runSingleSandbox(packageName, options = {}) {
365
399
  return;
366
400
  }
367
401
 
402
+ // In gVisor mode, merge kernel-level strace data from gVisor debug logs.
403
+ // sandbox-runner.sh skips strace/tcpdump in gVisor mode, so file access,
404
+ // connections, and process data come from gVisor's kernel-level tracing.
405
+ if (gvisorMode && gvisorContainerId) {
406
+ const gvisorLogDir = process.env.MUADDIB_GVISOR_LOG_DIR || '/tmp/runsc';
407
+ const gvisorData = parseGvisorLogs(gvisorContainerId, gvisorLogDir);
408
+
409
+ // Merge gVisor findings into report without duplicating
410
+ if (!report.sensitive_files) report.sensitive_files = { read: [], written: [] };
411
+ if (!report.network) report.network = {};
412
+ if (!report.processes) report.processes = { spawned: [] };
413
+
414
+ const existingReads = new Set(report.sensitive_files.read || []);
415
+ for (const f of gvisorData.sensitive_files.read) {
416
+ if (!existingReads.has(f)) report.sensitive_files.read.push(f);
417
+ }
418
+
419
+ const existingWrites = new Set(report.sensitive_files.written || []);
420
+ for (const f of gvisorData.sensitive_files.written) {
421
+ if (!existingWrites.has(f)) report.sensitive_files.written.push(f);
422
+ }
423
+
424
+ const existingConns = new Set((report.network.http_connections || []).map(c => `${c.host}:${c.port}`));
425
+ if (!report.network.http_connections) report.network.http_connections = [];
426
+ for (const c of gvisorData.network.http_connections) {
427
+ if (!existingConns.has(`${c.host}:${c.port}`)) report.network.http_connections.push(c);
428
+ }
429
+
430
+ const existingProcs = new Set((report.processes.spawned || []).map(p => p.command));
431
+ for (const p of gvisorData.processes.spawned) {
432
+ if (!existingProcs.has(p.command)) report.processes.spawned.push(p);
433
+ }
434
+
435
+ // Cleanup gVisor logs to prevent disk fill
436
+ cleanupGvisorLogs(gvisorContainerId, gvisorLogDir);
437
+ }
438
+
368
439
  const { score, findings } = scoreFindings(report);
369
440
 
370
441
  // Analyze preload log for behavioral findings
@@ -474,6 +545,17 @@ async function runSandbox(packageName, options = {}) {
474
545
  return cleanResult;
475
546
  }
476
547
 
548
+ // Detect sandbox runtime (gVisor or default Docker/runc)
549
+ let useGvisor = process.env.MUADDIB_SANDBOX_RUNTIME === 'gvisor';
550
+ if (useGvisor) {
551
+ if (isGvisorAvailable()) {
552
+ console.log('[SANDBOX] Runtime: gvisor (runsc)');
553
+ } else {
554
+ console.log('[SANDBOX] Runtime: gvisor requested but runsc not configured in Docker. Falling back to Docker standard.');
555
+ useGvisor = false;
556
+ }
557
+ }
558
+
477
559
  // Generate canary tokens for this sandbox session
478
560
  let canaryTokens = null;
479
561
  if (canaryEnabled) {
@@ -491,7 +573,8 @@ async function runSandbox(packageName, options = {}) {
491
573
  await acquireSandboxSlot();
492
574
 
493
575
  try {
494
- console.log(`[SANDBOX] Analyzing "${displayName}" in isolated container (mode: ${mode}${canaryEnabled ? ', canary: on' : ''}${local ? ', local' : ''}, runs: ${TIME_OFFSETS.length}, slots: ${_sandboxSemaphore.active}/${SANDBOX_CONCURRENCY_MAX})...`);
576
+ const runtimeLabel = useGvisor ? 'gvisor' : 'docker';
577
+ console.log(`[SANDBOX] Analyzing "${displayName}" in isolated container (mode: ${mode}, runtime: ${runtimeLabel}${canaryEnabled ? ', canary: on' : ''}${local ? ', local' : ''}, runs: ${TIME_OFFSETS.length}, slots: ${_sandboxSemaphore.active}/${SANDBOX_CONCURRENCY_MAX})...`);
495
578
 
496
579
  const allRuns = [];
497
580
  let bestResult = cleanResult;
@@ -507,7 +590,8 @@ async function runSandbox(packageName, options = {}) {
507
590
  localAbsPath,
508
591
  displayName,
509
592
  timeOffset: offset,
510
- runTimeout: SINGLE_RUN_TIMEOUT
593
+ runTimeout: SINGLE_RUN_TIMEOUT,
594
+ gvisor: useGvisor
511
595
  });
512
596
 
513
597
  allRuns.push({
@@ -646,34 +730,56 @@ function scoreFindings(report) {
646
730
  }
647
731
  }
648
732
 
649
- // 4a. DNS queries (exclude safe domains)
733
+ // 4a. DNS queries classify via network allowlist
650
734
  for (const domain of (report.network?.dns_queries || [])) {
651
- if (isSafeDomain(domain)) continue;
652
- score += 20;
653
- findings.push({ type: 'suspicious_dns', severity: 'HIGH', detail: `DNS query to non-registry domain: ${domain}`, evidence: domain });
735
+ const cls = classifyDomain(domain);
736
+ if (cls === 'safe') continue;
737
+ if (cls === 'blacklisted') {
738
+ score += 50;
739
+ findings.push({ type: 'sandbox_known_exfil_domain', severity: 'CRITICAL', detail: `DNS query to known exfiltration domain: ${domain}`, evidence: domain });
740
+ } else if (cls === 'tunnel') {
741
+ score += 30;
742
+ findings.push({ type: 'sandbox_network_outlier', severity: 'HIGH', detail: `DNS query to tunnel/proxy domain: ${domain}`, evidence: domain });
743
+ } else {
744
+ score += 20;
745
+ findings.push({ type: 'sandbox_network_outlier', severity: 'HIGH', detail: `DNS query to non-registry domain: ${domain}`, evidence: domain });
746
+ }
654
747
  }
655
748
 
656
749
  // 4b. DNS resolutions — extra detail
657
750
  for (const res of (report.network?.dns_resolutions || [])) {
658
- if (isSafeDomain(res.domain)) continue;
751
+ const cls = classifyDomain(res.domain);
752
+ if (cls === 'safe') continue;
659
753
  // Already scored in 4a via dns_queries, but flag the resolution for reporting
660
754
  findings.push({ type: 'dns_resolution', severity: 'INFO', detail: `${res.domain} → ${res.ip}`, evidence: `${res.domain}:${res.ip}` });
661
755
  }
662
756
 
663
- // 5a. TCP connections (exclude safe hosts, probe ports, localhost)
757
+ // 5a. TCP connections classify via network allowlist
664
758
  for (const conn of (report.network?.http_connections || [])) {
665
- if (isSafeHost(conn.host)) continue;
666
759
  if (SAFE_IPS.includes(conn.host)) continue;
667
760
  if (PROBE_PORTS.includes(conn.port)) continue;
668
- score += 25;
669
- findings.push({ type: 'suspicious_connection', severity: 'HIGH', detail: `TCP connection to ${conn.host}:${conn.port}`, evidence: `${conn.host}:${conn.port}` });
761
+ const cls = classifyDomain(conn.host);
762
+ if (cls === 'safe') continue;
763
+ if (cls === 'blacklisted') {
764
+ score += 50;
765
+ findings.push({ type: 'sandbox_known_exfil_domain', severity: 'CRITICAL', detail: `TCP connection to known exfiltration host: ${conn.host}:${conn.port}`, evidence: `${conn.host}:${conn.port}` });
766
+ } else {
767
+ score += 25;
768
+ findings.push({ type: 'suspicious_connection', severity: 'HIGH', detail: `TCP connection to ${conn.host}:${conn.port}`, evidence: `${conn.host}:${conn.port}` });
769
+ }
670
770
  }
671
771
 
672
- // 5b. TLS connections — non-safe domains
772
+ // 5b. TLS connections — classify via network allowlist
673
773
  for (const tls of (report.network?.tls_connections || [])) {
674
- if (isSafeDomain(tls.domain)) continue;
675
- score += 20;
676
- findings.push({ type: 'suspicious_tls', severity: 'HIGH', detail: `TLS connection to ${tls.domain} (${tls.ip}:${tls.port})`, evidence: tls.domain });
774
+ const cls = classifyDomain(tls.domain);
775
+ if (cls === 'safe') continue;
776
+ if (cls === 'blacklisted') {
777
+ score += 50;
778
+ findings.push({ type: 'sandbox_known_exfil_domain', severity: 'CRITICAL', detail: `TLS to known exfiltration domain: ${tls.domain} (${tls.ip}:${tls.port})`, evidence: tls.domain });
779
+ } else {
780
+ score += 20;
781
+ findings.push({ type: 'suspicious_tls', severity: 'HIGH', detail: `TLS connection to ${tls.domain} (${tls.ip}:${tls.port})`, evidence: tls.domain });
782
+ }
677
783
  }
678
784
 
679
785
  // 5c. HTTP exfiltration detection — scan body snippets for sensitive data
@@ -692,11 +798,17 @@ function scoreFindings(report) {
692
798
  }
693
799
  }
694
800
 
695
- // 5d. HTTP requests to non-safe hosts
801
+ // 5d. HTTP requests classify via network allowlist
696
802
  for (const req of (report.network?.http_requests || [])) {
697
- if (isSafeDomain(req.host)) continue;
698
- score += 20;
699
- findings.push({ type: 'suspicious_http_request', severity: 'HIGH', detail: `${req.method} ${req.host}${req.path}`, evidence: `${req.method} ${req.host}${req.path}` });
803
+ const cls = classifyDomain(req.host);
804
+ if (cls === 'safe') continue;
805
+ if (cls === 'blacklisted') {
806
+ score += 50;
807
+ findings.push({ type: 'sandbox_known_exfil_domain', severity: 'CRITICAL', detail: `HTTP request to known exfiltration host: ${req.method} ${req.host}${req.path}`, evidence: `${req.method} ${req.host}${req.path}` });
808
+ } else {
809
+ score += 20;
810
+ findings.push({ type: 'suspicious_http_request', severity: 'HIGH', detail: `${req.method} ${req.host}${req.path}`, evidence: `${req.method} ${req.host}${req.path}` });
811
+ }
700
812
  }
701
813
 
702
814
  // 5e. Blocked connections (strict mode)
@@ -865,4 +977,4 @@ function displayResults(result) {
865
977
  }
866
978
  }
867
979
 
868
- module.exports = { buildSandboxImage, runSandbox, runSingleSandbox, scoreFindings, generateNetworkReport, EXFIL_PATTERNS, SAFE_DOMAINS, getSeverity, displayResults, isDockerAvailable, imageExists, STATIC_CANARY_TOKENS, detectStaticCanaryExfiltration, analyzePreloadLog, TIME_OFFSETS, SAFE_SANDBOX_CMDS, SANDBOX_CONCURRENCY_MAX, acquireSandboxSlot, releaseSandboxSlot, resetSandboxLimiter, getSandboxSemaphore };
980
+ module.exports = { buildSandboxImage, runSandbox, runSingleSandbox, scoreFindings, generateNetworkReport, EXFIL_PATTERNS, SAFE_DOMAINS, getSeverity, displayResults, isDockerAvailable, imageExists, isGvisorAvailable, STATIC_CANARY_TOKENS, detectStaticCanaryExfiltration, analyzePreloadLog, TIME_OFFSETS, SAFE_SANDBOX_CMDS, SANDBOX_CONCURRENCY_MAX, acquireSandboxSlot, releaseSandboxSlot, resetSandboxLimiter, getSandboxSemaphore };
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+
3
+ // ── Network Allowlist & Blacklist for Sandbox Analysis ──
4
+ //
5
+ // Classifies domains/IPs contacted during npm install into three categories:
6
+ // - safe: legitimate install-time traffic (registries, CDNs, GitHub)
7
+ // - blacklisted: known exfiltration/C2 infrastructure (OAST, webhook sinks, campaign IPs)
8
+ // - unknown: everything else — potential outlier requiring investigation
9
+ //
10
+ // Threat model: SafeDep found only 0.027% of 3M+ packages make DNS queries to
11
+ // non-npm domains during install. Network outliers are the highest-precision
12
+ // signal available for detecting supply chain attacks at install time.
13
+
14
+ // ── Safe domains: legitimate traffic during npm install ──
15
+ // These domains are expected during normal package installation.
16
+ // Subdomains are matched (e.g., foo.github.com matches github.com).
17
+ const SAFE_INSTALL_DOMAINS = [
18
+ // npm registry
19
+ 'registry.npmjs.org',
20
+ 'npmjs.com',
21
+ 'npmjs.org',
22
+ // yarn registry
23
+ 'registry.yarnpkg.com',
24
+ 'yarnpkg.com',
25
+ // GitHub (source tarballs, git deps)
26
+ 'github.com',
27
+ 'api.github.com',
28
+ 'objects.githubusercontent.com',
29
+ 'raw.githubusercontent.com',
30
+ 'codeload.github.com',
31
+ 'github.githubassets.com',
32
+ // CDNs (native binary downloads via node-gyp, prebuild)
33
+ 'cdn.jsdelivr.net',
34
+ 'unpkg.com',
35
+ 'cdnjs.cloudflare.com',
36
+ 'cloudflare.com',
37
+ // AWS S3 (prebuild binaries: sharp, canvas, sqlite3, etc.)
38
+ 'amazonaws.com',
39
+ // Google (googleapis client, protobuf downloads)
40
+ 'googleapis.com',
41
+ 'storage.googleapis.com',
42
+ // Node.js (node-gyp headers)
43
+ 'nodejs.org',
44
+ // GitLab (git deps)
45
+ 'gitlab.com',
46
+ // Bitbucket (git deps)
47
+ 'bitbucket.org'
48
+ ];
49
+
50
+ // ── Known exfiltration / C2 domains ──
51
+ // Any contact during install is near-certain malicious (quasi-zero FP).
52
+ // Sources: OAST tooling, known campaign C2, webhook sink services.
53
+ const KNOWN_EXFIL_DOMAINS = [
54
+ // OAST / Interactsh / BurpSuite
55
+ 'oastify.com',
56
+ 'oast.fun',
57
+ 'oast.me',
58
+ 'oast.live',
59
+ 'oast.online',
60
+ 'oast.site',
61
+ 'burpcollaborator.net',
62
+ 'interact.sh',
63
+ // Webhook sink services
64
+ 'webhook.site',
65
+ 'pipedream.net',
66
+ 'requestbin.com',
67
+ 'hookbin.com',
68
+ 'canarytokens.com',
69
+ // GlassWorm C2 IPs (mars 2026, 433+ packages)
70
+ '217.69.3.218',
71
+ '217.69.3.152',
72
+ '199.247.10.166',
73
+ '199.247.13.106',
74
+ '140.82.52.31',
75
+ '45.32.150.251',
76
+ // TeamPCP / CanisterWorm C2 (mars 2026)
77
+ 'icp0.io',
78
+ 'raw.icp0.io',
79
+ 'ic0.app',
80
+ 'hackmoltrepeat.com',
81
+ 'recv.hackmoltrepeat.com',
82
+ 'scan.aquasecurtiy.org', // Trivy exfil C2 (typosquat of aquasecurity)
83
+ 'api.telegram.org', // Telegram bot exfiltration
84
+ 'checkmarx.zone',
85
+ '45.148.10.212',
86
+ '83.142.209.11'
87
+ ];
88
+
89
+ // ── Regex patterns for wildcard exfil domains ──
90
+ // Matches subdomains of OAST/exfil infrastructure.
91
+ const KNOWN_EXFIL_PATTERNS = [
92
+ /\.oast\.(online|site|live|fun|me)$/i,
93
+ /\.oastify\.com$/i,
94
+ /\.burpcollaborator\.net$/i,
95
+ /\.interact\.sh$/i,
96
+ /\.webhook\.site$/i,
97
+ /\.pipedream\.net$/i,
98
+ /\.requestbin\.com$/i
99
+ ];
100
+
101
+ // ── Suspicious tunnel/proxy domains (not blacklisted, but escalate unknown → suspicious) ──
102
+ const TUNNEL_DOMAINS = [
103
+ 'ngrok.io',
104
+ 'ngrok-free.app',
105
+ 'serveo.net',
106
+ 'localhost.run',
107
+ 'loca.lt',
108
+ 'trycloudflare.com'
109
+ ];
110
+
111
+ // Parse MUADDIB_SANDBOX_NETWORK_ALLOWLIST env var (comma-separated domains)
112
+ function getCustomAllowlist() {
113
+ const envVal = process.env.MUADDIB_SANDBOX_NETWORK_ALLOWLIST;
114
+ if (!envVal) return [];
115
+ return envVal.split(',')
116
+ .map(d => d.trim().toLowerCase())
117
+ .filter(d => d.length > 0 && d.length < 256);
118
+ }
119
+
120
+ /**
121
+ * Classify a domain/IP contacted during sandbox install.
122
+ *
123
+ * @param {string} domain - Domain name or IP address
124
+ * @returns {'safe'|'blacklisted'|'tunnel'|'unknown'} classification
125
+ */
126
+ function classifyDomain(domain) {
127
+ if (!domain || typeof domain !== 'string') return 'unknown';
128
+ const d = domain.toLowerCase().trim();
129
+ if (d.length === 0) return 'unknown';
130
+
131
+ // Check safe domains (exact or subdomain match)
132
+ const allSafe = SAFE_INSTALL_DOMAINS.concat(getCustomAllowlist());
133
+ for (const safe of allSafe) {
134
+ if (d === safe || d.endsWith('.' + safe)) return 'safe';
135
+ }
136
+
137
+ // Check blacklisted domains (exact match)
138
+ for (const exfil of KNOWN_EXFIL_DOMAINS) {
139
+ if (d === exfil || d.endsWith('.' + exfil)) return 'blacklisted';
140
+ }
141
+
142
+ // Check blacklisted patterns (regex — catches subdomains like abc123.oast.online)
143
+ for (const pat of KNOWN_EXFIL_PATTERNS) {
144
+ if (pat.test(d)) return 'blacklisted';
145
+ }
146
+
147
+ // Check tunnel domains
148
+ for (const tunnel of TUNNEL_DOMAINS) {
149
+ if (d === tunnel || d.endsWith('.' + tunnel)) return 'tunnel';
150
+ }
151
+
152
+ return 'unknown';
153
+ }
154
+
155
+ module.exports = {
156
+ SAFE_INSTALL_DOMAINS,
157
+ KNOWN_EXFIL_DOMAINS,
158
+ KNOWN_EXFIL_PATTERNS,
159
+ TUNNEL_DOMAINS,
160
+ classifyDomain,
161
+ getCustomAllowlist
162
+ };
package/iocs/builtin.yaml DELETED
@@ -1,239 +0,0 @@
1
- version: "1.1.0"
2
- updated: "2026-01-08"
3
-
4
- packages:
5
- # Shai-Hulud v1 (septembre 2025)
6
- - name: "@ctrl/tinycolor"
7
- version: "4.1.1"
8
- source: shai-hulud-v1
9
- - name: "ng2-file-upload"
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"
26
- source: shai-hulud-v1
27
- - name: "ngx-bootstrap"
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"
47
- source: shai-hulud-v1
48
-
49
- # Shai-Hulud v2 (novembre 2025)
50
- - name: "@asyncapi/specs"
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"
61
- source: shai-hulud-v2
62
- - name: "@asyncapi/openapi-schema-parser"
63
- version: "3.0.25"
64
- source: shai-hulud-v2
65
- - name: "@asyncapi/openapi-schema-parser"
66
- version: "3.0.26"
67
- source: shai-hulud-v2
68
- - name: "get-them-args"
69
- version: "1.3.3"
70
- source: shai-hulud-v2
71
- - name: "kill-port"
72
- version: "2.0.2"
73
- source: shai-hulud-v2
74
- - name: "kill-port"
75
- version: "2.0.3"
76
- source: shai-hulud-v2
77
- - name: "shell-exec"
78
- version: "1.1.3"
79
- source: shai-hulud-v2
80
- - name: "shell-exec"
81
- version: "1.1.4"
82
- source: shai-hulud-v2
83
- - name: "posthog-node"
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"
91
- source: shai-hulud-v2
92
- - name: "posthog-js"
93
- version: "1.297.3"
94
- source: shai-hulud-v2
95
- - name: "@postman/tunnel-agent"
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"
103
- source: shai-hulud-v2
104
- - name: "@zapier/secret-scrubber"
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"
112
- source: shai-hulud-v2
113
-
114
- # Shai-Hulud v3 Golden Path (28 decembre 2025)
115
- - name: "@vietmoney/react-big-calendar"
116
- version: "0.26.2"
117
- source: shai-hulud-v3
118
- description: "First confirmed v3 payload - testing phase"
119
-
120
- # GlassWorm hijacked packages (mars 2026)
121
- - name: "@aifabrix/miso-client"
122
- version: "4.7.2"
123
- source: glassworm
124
- - name: "@iflow-mcp/watercrawl-watercrawl-mcp"
125
- version: "1.3.0"
126
- source: glassworm
127
- - name: "@iflow-mcp/watercrawl-watercrawl-mcp"
128
- version: "1.3.1"
129
- source: glassworm
130
- - name: "@iflow-mcp/watercrawl-watercrawl-mcp"
131
- version: "1.3.2"
132
- source: glassworm
133
- - name: "@iflow-mcp/watercrawl-watercrawl-mcp"
134
- version: "1.3.3"
135
- source: glassworm
136
- - name: "@iflow-mcp/watercrawl-watercrawl-mcp"
137
- version: "1.3.4"
138
- source: glassworm
139
- - name: "react-native-country-select"
140
- version: "0.3.91"
141
- source: glassworm
142
- - name: "react-native-international-phone-number"
143
- version: "0.11.8"
144
- source: glassworm
145
-
146
- # Attaques historiques
147
- - name: "flatmap-stream"
148
- version: "0.1.1"
149
- source: event-stream-2018
150
- - name: "event-stream"
151
- version: "3.3.6"
152
- source: event-stream-2018
153
- - name: "eslint-scope"
154
- version: "3.7.2"
155
- source: eslint-scope-2018
156
-
157
- # Protestware
158
- - name: "node-ipc"
159
- version: "10.1.1"
160
- source: protestware
161
- - name: "node-ipc"
162
- version: "10.1.2"
163
- source: protestware
164
- - name: "node-ipc"
165
- version: "10.1.3"
166
- source: protestware
167
- - name: "colors"
168
- version: "1.4.1"
169
- source: protestware
170
- - name: "colors"
171
- version: "1.4.2"
172
- source: protestware
173
- - name: "faker"
174
- version: "6.6.6"
175
- source: protestware
176
-
177
- # Typosquats historiques confirmes
178
- - name: "crossenv"
179
- version: "*"
180
- source: typosquat
181
- - name: "cross-env.js"
182
- version: "*"
183
- source: typosquat
184
- - name: "mongose"
185
- version: "*"
186
- source: typosquat
187
- - name: "babelcli"
188
- version: "*"
189
- source: typosquat
190
-
191
- files:
192
- # Shai-Hulud v2
193
- - setup_bun.js
194
- - bun_environment.js
195
- - node-gyp.dll
196
- # Shai-Hulud v3 (nouveaux noms)
197
- - bun_installer.js
198
- - environment_source.js
199
- - cl0vd.json
200
- - pigS3cr3ts.json
201
- - actionsSecrets.json
202
- # Artefacts exfiltration v3
203
- - 3nvir0nm3nt.json
204
- - c9nt3nts.json
205
- - c0nt3nts.json
206
- # GlassWorm (mars 2026)
207
- - i.js
208
- - init.json
209
- # LiteLLM/Checkmarx (mars 2026) — .pth = Python auto-exec persistence
210
- - litellm_init.pth
211
-
212
- hashes:
213
- # Shai-Hulud v2 payloads
214
- - "62ee164b9b306250c1172583f138c9614139264f889fa99614903c12755468d0"
215
- - "cbb9bc5a8496243e02f3cc080efbe3e4a1430ba0671f2e43a202bf45b05479cd"
216
- - "f099c5d9ec417d4445a0328ac0ada9cde79fc37410914103ae9c609cbc0ee068"
217
- - "a3894003ad1d293ba96d77881ccd2071446dc3f65f434669b49b3da92421901a"
218
- - "4b2399646573bb737c4969563303d8ee2e9ddbd1b271f1ca9e35ea78062538db"
219
- # GlassWorm install.js (React Native hijack)
220
- - "59221aa9623d86c930357dba7e3f54138c7ccbd0daa9c483d766cd8ce1b6ad26"
221
-
222
- markers:
223
- # Shai-Hulud v1/v2
224
- - "Shai-Hulud"
225
- - "Sha1-Hulud"
226
- - "The Second Coming"
227
- # Shai-Hulud v3
228
- - "Goldox-T3chs"
229
- - "Only Happy Girl"
230
- - "SHA1HULUD"
231
- # Protestware
232
- - "peacenotwar"
233
- # Generic malicious
234
- - "/dev/tcp"
235
- # GlassWorm (mars 2026)
236
- - "lzcdrtfxyqiplpd"
237
- - "28PKnu7RzizxBzFPoLp69HLXp9bJL3JFtT2s5QzHsEA2"
238
- - "BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC"
239
- - "6YGcuyFRJKZtcaYCCFba9fScNUvPkGXodXE1mJiSzqDJ"