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.
- package/package.json +1 -1
- package/src/integrations/canary-tokens.js +53 -0
- package/src/monitor/classify.js +1 -0
- package/src/response/playbooks.js +9 -0
- package/src/rules/index.js +23 -0
- package/src/sandbox/gvisor-parser.js +348 -0
- package/src/sandbox/index.js +133 -21
- package/src/sandbox/network-allowlist.js +162 -0
- package/iocs/builtin.yaml +0 -239
- package/iocs/hashes.yaml +0 -214
- package/iocs/packages.yaml +0 -481
- package/scripts/analyze-score0.js +0 -190
- package/scripts/archive-cleanup.sh +0 -7
- package/scripts/audit-archive.sh +0 -45
- package/scripts/benchmark.js +0 -326
- package/scripts/cleanup-fp-labels.js +0 -81
- package/scripts/ossf-benchmark.js +0 -548
- package/scripts/sample-npm-random.js +0 -339
- package/src/ioc/data/.ossf-tree-sha +0 -1
package/src/sandbox/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
733
|
+
// 4a. DNS queries — classify via network allowlist
|
|
650
734
|
for (const domain of (report.network?.dns_queries || [])) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
669
|
-
|
|
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 —
|
|
772
|
+
// 5b. TLS connections — classify via network allowlist
|
|
673
773
|
for (const tls of (report.network?.tls_connections || [])) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
|
801
|
+
// 5d. HTTP requests — classify via network allowlist
|
|
696
802
|
for (const req of (report.network?.http_requests || [])) {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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"
|