muaddib-scanner 2.2.27 → 2.3.0
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/bin/muaddib.js +6 -1
- package/package.json +1 -1
- package/src/index.js +15 -1
- package/src/response/playbooks.js +25 -0
- package/src/rules/index.js +60 -0
- package/src/scanner/ast-detectors.js +172 -9
- package/src/scanner/ast.js +40 -1
- package/src/scanner/module-graph.js +886 -883
- package/src/scanner/reachability.js +329 -0
- package/src/scanner/typosquat.js +535 -511
- package/src/scoring.js +253 -213
package/bin/muaddib.js
CHANGED
|
@@ -33,6 +33,7 @@ let temporalFullMode = false;
|
|
|
33
33
|
let breakdownMode = false;
|
|
34
34
|
let noDeobfuscate = false;
|
|
35
35
|
let noModuleGraph = false;
|
|
36
|
+
let noReachability = false;
|
|
36
37
|
let feedLimit = null;
|
|
37
38
|
let feedSeverity = null;
|
|
38
39
|
let feedSince = null;
|
|
@@ -122,6 +123,8 @@ for (let i = 0; i < options.length; i++) {
|
|
|
122
123
|
noDeobfuscate = true;
|
|
123
124
|
} else if (options[i] === '--no-module-graph') {
|
|
124
125
|
noModuleGraph = true;
|
|
126
|
+
} else if (options[i] === '--no-reachability') {
|
|
127
|
+
noReachability = true;
|
|
125
128
|
} else if (options[i] === '--temporal') {
|
|
126
129
|
temporalMode = true;
|
|
127
130
|
} else if (options[i] === '--limit') {
|
|
@@ -415,6 +418,7 @@ const helpText = `
|
|
|
415
418
|
--no-canary Disable honey token injection in sandbox
|
|
416
419
|
--no-deobfuscate Disable deobfuscation pre-processing
|
|
417
420
|
--no-module-graph Disable cross-file dataflow analysis
|
|
421
|
+
--no-reachability Disable entry-point reachability analysis
|
|
418
422
|
--exclude [dir] Exclude directory from scan (repeatable)
|
|
419
423
|
--limit [n] Limit feed entries (default: 50)
|
|
420
424
|
--severity [level] Filter by severity (CRITICAL|HIGH|MEDIUM|LOW)
|
|
@@ -461,7 +465,8 @@ if (command === 'version' || command === '--version' || command === '-v') {
|
|
|
461
465
|
entropyThreshold: entropyThreshold,
|
|
462
466
|
breakdown: breakdownMode,
|
|
463
467
|
noDeobfuscate: noDeobfuscate,
|
|
464
|
-
noModuleGraph: noModuleGraph
|
|
468
|
+
noModuleGraph: noModuleGraph,
|
|
469
|
+
noReachability: noReachability
|
|
465
470
|
}).then(exitCode => {
|
|
466
471
|
process.exit(exitCode);
|
|
467
472
|
}).catch(err => {
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const { scanEntropy } = require('./scanner/entropy.js');
|
|
|
19
19
|
const { scanAIConfig } = require('./scanner/ai-config.js');
|
|
20
20
|
const { deobfuscate } = require('./scanner/deobfuscate.js');
|
|
21
21
|
const { buildModuleGraph, annotateTaintedExports, detectCrossFileFlows } = require('./scanner/module-graph.js');
|
|
22
|
+
const { computeReachableFiles } = require('./scanner/reachability.js');
|
|
22
23
|
const { runTemporalAnalyses } = require('./temporal-runner.js');
|
|
23
24
|
const { formatOutput } = require('./output-formatter.js');
|
|
24
25
|
const { setExtraExcludes, getExtraExcludes, Spinner, listInstalledPackages, clearFileListCache } = require('./utils.js');
|
|
@@ -333,9 +334,22 @@ async function run(targetPath, options = {}) {
|
|
|
333
334
|
}
|
|
334
335
|
}
|
|
335
336
|
|
|
337
|
+
// Reachability analysis: determine which files are reachable from entry points
|
|
338
|
+
let reachableFiles = null;
|
|
339
|
+
if (!options.noReachability) {
|
|
340
|
+
try {
|
|
341
|
+
const reachability = computeReachableFiles(targetPath);
|
|
342
|
+
if (!reachability.skipped) {
|
|
343
|
+
reachableFiles = reachability.reachableFiles;
|
|
344
|
+
}
|
|
345
|
+
} catch {
|
|
346
|
+
// Graceful fallback — treat all files as reachable
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
336
350
|
// FP reduction: legitimate frameworks produce high volumes of certain threat types.
|
|
337
351
|
// A malware package typically has 1-3 occurrences, not dozens.
|
|
338
|
-
applyFPReductions(deduped);
|
|
352
|
+
applyFPReductions(deduped, reachableFiles);
|
|
339
353
|
|
|
340
354
|
// Enrich each threat with rules
|
|
341
355
|
const enrichedThreats = deduped.map(t => {
|
|
@@ -354,6 +354,31 @@ const PLAYBOOKS = {
|
|
|
354
354
|
'execution via require() ou Module._compile(), puis suppression via unlinkSync/rmSync. ' +
|
|
355
355
|
'Ce pattern de staging est typique des malwares cherchant a eviter la detection post-mortem. ' +
|
|
356
356
|
'Isoler la machine et inspecter les artefacts temporaires avant nettoyage.',
|
|
357
|
+
|
|
358
|
+
mcp_config_injection:
|
|
359
|
+
'CRITIQUE: Ecriture dans les fichiers de configuration MCP d\'assistants IA (.claude/, .cursor/, .continue/, .vscode/). ' +
|
|
360
|
+
'Technique SANDWORM_MODE: le malware empoisonne la configuration MCP pour ajouter des serveurs malveillants. ' +
|
|
361
|
+
'Verifier immediatement les fichiers de config IA. Supprimer les entrees MCP non reconnues. Supprimer le package.',
|
|
362
|
+
|
|
363
|
+
git_hooks_injection:
|
|
364
|
+
'Injection de hooks Git detectee. Le package ecrit dans .git/hooks/ (pre-commit, pre-push, etc.) ' +
|
|
365
|
+
'ou modifie git config init.templateDir pour la persistence globale. ' +
|
|
366
|
+
'Verifier .git/hooks/ et git config --global --list. Supprimer les hooks non reconnus.',
|
|
367
|
+
|
|
368
|
+
env_harvesting_dynamic:
|
|
369
|
+
'Collecte dynamique de variables d\'environnement via Object.entries/keys/values(process.env) ' +
|
|
370
|
+
'avec filtrage par patterns sensibles. Technique de vol de credentials a grande echelle. ' +
|
|
371
|
+
'Verifier les tokens/secrets exposes. Revoquer immediatement les credentials compromis.',
|
|
372
|
+
|
|
373
|
+
dns_chunk_exfiltration:
|
|
374
|
+
'Exfiltration via requetes DNS detectee. Les donnees sont encodees en base64 et envoyees comme sous-domaines DNS. ' +
|
|
375
|
+
'Cette technique contourne les firewalls et proxies car le DNS est rarement filtre. ' +
|
|
376
|
+
'Bloquer les requetes DNS sortantes du package. Verifier les donnees exfiltrees.',
|
|
377
|
+
|
|
378
|
+
llm_api_key_harvesting:
|
|
379
|
+
'Acces a 3+ cles API de providers LLM (OpenAI, Anthropic, Google, etc.). Usage unique = legitime, ' +
|
|
380
|
+
'acces multiples = collecte pour revente ou abus. Verifier si le package a une raison ' +
|
|
381
|
+
'legitime d\'acceder a plusieurs providers. Revoquer les cles exposees si necessaire.',
|
|
357
382
|
};
|
|
358
383
|
|
|
359
384
|
function getPlaybook(threatType) {
|
package/src/rules/index.js
CHANGED
|
@@ -713,6 +713,66 @@ const RULES = {
|
|
|
713
713
|
mitre: 'T1070.004'
|
|
714
714
|
},
|
|
715
715
|
|
|
716
|
+
mcp_config_injection: {
|
|
717
|
+
id: 'MUADDIB-AST-027',
|
|
718
|
+
name: 'MCP Config Injection',
|
|
719
|
+
severity: 'CRITICAL',
|
|
720
|
+
confidence: 'high',
|
|
721
|
+
description: 'Injection de configuration MCP: ecriture dans les fichiers de configuration d\'assistants IA (.claude/, .cursor/, .continue/, .vscode/, .windsurf/). Technique SANDWORM_MODE pour empoisonner la chaine d\'outils IA.',
|
|
722
|
+
references: [
|
|
723
|
+
'https://attack.mitre.org/techniques/T1546/016/'
|
|
724
|
+
],
|
|
725
|
+
mitre: 'T1546.016'
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
git_hooks_injection: {
|
|
729
|
+
id: 'MUADDIB-AST-028',
|
|
730
|
+
name: 'Git Hooks Injection',
|
|
731
|
+
severity: 'HIGH',
|
|
732
|
+
confidence: 'high',
|
|
733
|
+
description: 'Injection de hooks Git: ecriture dans .git/hooks/ ou modification de git config init.templateDir. Technique de persistence via hooks pre-commit, pre-push, post-checkout.',
|
|
734
|
+
references: [
|
|
735
|
+
'https://attack.mitre.org/techniques/T1546/004/'
|
|
736
|
+
],
|
|
737
|
+
mitre: 'T1546.004'
|
|
738
|
+
},
|
|
739
|
+
|
|
740
|
+
env_harvesting_dynamic: {
|
|
741
|
+
id: 'MUADDIB-AST-029',
|
|
742
|
+
name: 'Dynamic Environment Variable Harvesting',
|
|
743
|
+
severity: 'HIGH',
|
|
744
|
+
confidence: 'high',
|
|
745
|
+
description: 'Collecte dynamique de variables d\'environnement via Object.entries/keys/values(process.env) avec filtrage par patterns sensibles (TOKEN, SECRET, KEY, PASSWORD, AWS, SSH). Technique de vol de credentials.',
|
|
746
|
+
references: [
|
|
747
|
+
'https://attack.mitre.org/techniques/T1552/001/'
|
|
748
|
+
],
|
|
749
|
+
mitre: 'T1552.001'
|
|
750
|
+
},
|
|
751
|
+
|
|
752
|
+
dns_chunk_exfiltration: {
|
|
753
|
+
id: 'MUADDIB-AST-030',
|
|
754
|
+
name: 'DNS Chunk Exfiltration',
|
|
755
|
+
severity: 'HIGH',
|
|
756
|
+
confidence: 'high',
|
|
757
|
+
description: 'Exfiltration DNS: donnees encodees en base64 dans les requetes DNS. Canal covert pour contourner les firewalls. Pattern: dns.resolve + Buffer.from().toString("base64").',
|
|
758
|
+
references: [
|
|
759
|
+
'https://attack.mitre.org/techniques/T1048/003/'
|
|
760
|
+
],
|
|
761
|
+
mitre: 'T1048.003'
|
|
762
|
+
},
|
|
763
|
+
|
|
764
|
+
llm_api_key_harvesting: {
|
|
765
|
+
id: 'MUADDIB-AST-031',
|
|
766
|
+
name: 'LLM API Key Harvesting',
|
|
767
|
+
severity: 'MEDIUM',
|
|
768
|
+
confidence: 'medium',
|
|
769
|
+
description: 'Collecte de cles API LLM: acces a 3+ variables d\'environnement de providers IA (OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, etc.). Vecteur de monetisation.',
|
|
770
|
+
references: [
|
|
771
|
+
'https://attack.mitre.org/techniques/T1552/001/'
|
|
772
|
+
],
|
|
773
|
+
mitre: 'T1552.001'
|
|
774
|
+
},
|
|
775
|
+
|
|
716
776
|
ai_agent_abuse: {
|
|
717
777
|
id: 'MUADDIB-AST-013',
|
|
718
778
|
name: 'AI Agent Weaponization',
|
|
@@ -83,6 +83,38 @@ const NODE_HOOKABLE_CLASSES = [
|
|
|
83
83
|
'OutgoingMessage', 'Socket', 'Server', 'Agent'
|
|
84
84
|
];
|
|
85
85
|
|
|
86
|
+
// AI/MCP config paths targeted for config injection (SANDWORM_MODE)
|
|
87
|
+
const MCP_CONFIG_PATHS = [
|
|
88
|
+
'.claude/', 'claude_desktop_config',
|
|
89
|
+
'.cursor/', 'mcp.json',
|
|
90
|
+
'.continue/',
|
|
91
|
+
'.vscode/',
|
|
92
|
+
'.windsurf/', '.codeium/'
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// MCP content indicators in written data
|
|
96
|
+
const MCP_CONTENT_PATTERNS = ['mcpServers', '"mcp"', '"server"', '"command"', '"args"'];
|
|
97
|
+
|
|
98
|
+
// Git hooks names
|
|
99
|
+
const GIT_HOOKS = [
|
|
100
|
+
'pre-commit', 'pre-push', 'post-checkout', 'post-merge',
|
|
101
|
+
'pre-receive', 'post-receive', 'prepare-commit-msg', 'commit-msg',
|
|
102
|
+
'pre-rebase', 'post-rewrite', 'pre-auto-gc'
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
// LLM API key environment variable names (3+ = harvesting)
|
|
106
|
+
const LLM_API_KEY_VARS = [
|
|
107
|
+
'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GOOGLE_API_KEY',
|
|
108
|
+
'GROQ_API_KEY', 'TOGETHER_API_KEY', 'FIREWORKS_API_KEY',
|
|
109
|
+
'REPLICATE_API_KEY', 'MISTRAL_API_KEY', 'COHERE_API_KEY'
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
// Env harvesting sensitive patterns (for Object.entries/keys/values filtering)
|
|
113
|
+
const ENV_HARVEST_PATTERNS = [
|
|
114
|
+
'KEY', 'SECRET', 'TOKEN', 'PASSWORD', 'CREDENTIAL',
|
|
115
|
+
'NPM', 'AWS', 'SSH', 'WEBHOOK'
|
|
116
|
+
];
|
|
117
|
+
|
|
86
118
|
// Paths indicating sandbox/container environment detection (anti-analysis)
|
|
87
119
|
const SANDBOX_INDICATORS = [
|
|
88
120
|
'/.dockerenv',
|
|
@@ -487,6 +519,84 @@ function handleCallExpression(node, ctx) {
|
|
|
487
519
|
}
|
|
488
520
|
}
|
|
489
521
|
|
|
522
|
+
// SANDWORM_MODE R5: MCP config injection — writeFileSync to AI config paths
|
|
523
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
524
|
+
const mcpWriteMethod = node.callee.property.name;
|
|
525
|
+
if (['writeFileSync', 'writeFile'].includes(mcpWriteMethod) && node.arguments.length >= 2) {
|
|
526
|
+
const mcpPathArg = node.arguments[0];
|
|
527
|
+
const mcpPathStr = extractStringValue(mcpPathArg);
|
|
528
|
+
// Also check path.join() calls
|
|
529
|
+
let mcpJoinedPath = '';
|
|
530
|
+
if (mcpPathArg?.type === 'CallExpression' && mcpPathArg.arguments) {
|
|
531
|
+
mcpJoinedPath = mcpPathArg.arguments.map(a => extractStringValue(a) || '').join('/');
|
|
532
|
+
}
|
|
533
|
+
const mcpCheckPath = (mcpPathStr || mcpJoinedPath).toLowerCase();
|
|
534
|
+
const isMcpPath = MCP_CONFIG_PATHS.some(p => mcpCheckPath.includes(p.toLowerCase()));
|
|
535
|
+
if (isMcpPath) {
|
|
536
|
+
// Check content argument for MCP-related patterns
|
|
537
|
+
const contentArg = node.arguments[1];
|
|
538
|
+
const contentStr = extractStringValue(contentArg);
|
|
539
|
+
const hasContentPattern = contentStr
|
|
540
|
+
? MCP_CONTENT_PATTERNS.some(p => contentStr.includes(p.replace(/"/g, '')))
|
|
541
|
+
: true; // dynamic content = suspicious by default for AI config paths
|
|
542
|
+
if (hasContentPattern) {
|
|
543
|
+
ctx.threats.push({
|
|
544
|
+
type: 'mcp_config_injection',
|
|
545
|
+
severity: 'CRITICAL',
|
|
546
|
+
message: `MCP config injection: ${mcpWriteMethod}() writes to AI assistant configuration (${mcpCheckPath}). SANDWORM_MODE technique for AI toolchain poisoning.`,
|
|
547
|
+
file: ctx.relFile
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// SANDWORM_MODE R6: Git hooks injection — writeFileSync to .git/hooks/
|
|
555
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
556
|
+
const gitWriteMethod = node.callee.property.name;
|
|
557
|
+
if (['writeFileSync', 'writeFile'].includes(gitWriteMethod) && node.arguments.length >= 1) {
|
|
558
|
+
const gitPathArg = node.arguments[0];
|
|
559
|
+
const gitPathStr = extractStringValue(gitPathArg);
|
|
560
|
+
let gitJoinedPath = '';
|
|
561
|
+
if (gitPathArg?.type === 'CallExpression' && gitPathArg.arguments) {
|
|
562
|
+
gitJoinedPath = gitPathArg.arguments.map(a => extractStringValue(a) || '').join('/');
|
|
563
|
+
}
|
|
564
|
+
const gitCheckPath = gitPathStr || gitJoinedPath;
|
|
565
|
+
if (/\.git[\\/]hooks[\\/]/i.test(gitCheckPath) ||
|
|
566
|
+
GIT_HOOKS.some(h => gitCheckPath.includes(h) && gitCheckPath.includes('.git'))) {
|
|
567
|
+
ctx.threats.push({
|
|
568
|
+
type: 'git_hooks_injection',
|
|
569
|
+
severity: 'HIGH',
|
|
570
|
+
message: `Git hook injection: ${gitWriteMethod}() writes to .git/hooks/. Persistence technique.`,
|
|
571
|
+
file: ctx.relFile
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// SANDWORM_MODE R6: Git hooks injection via exec — git config --global init.templateDir or git config hooks
|
|
578
|
+
if ((execName || memberExec) && node.arguments.length > 0) {
|
|
579
|
+
const gitExecArg = node.arguments[0];
|
|
580
|
+
const gitExecStr = extractStringValue(gitExecArg);
|
|
581
|
+
if (gitExecStr) {
|
|
582
|
+
if (/git\s+config\s+.*init\.templateDir/i.test(gitExecStr)) {
|
|
583
|
+
ctx.threats.push({
|
|
584
|
+
type: 'git_hooks_injection',
|
|
585
|
+
severity: 'HIGH',
|
|
586
|
+
message: 'Git hook injection: modifying global git template directory via "git config init.templateDir". Persistence technique.',
|
|
587
|
+
file: ctx.relFile
|
|
588
|
+
});
|
|
589
|
+
} else if (/git\s+config\b/.test(gitExecStr) && /hooks/i.test(gitExecStr)) {
|
|
590
|
+
ctx.threats.push({
|
|
591
|
+
type: 'git_hooks_injection',
|
|
592
|
+
severity: 'HIGH',
|
|
593
|
+
message: 'Git hook injection: modifying git hooks configuration. Persistence technique.',
|
|
594
|
+
file: ctx.relFile
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
490
600
|
// Detect fs.chmodSync with executable permissions (deferred to postWalk for compound check)
|
|
491
601
|
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
492
602
|
const chmodMethod = node.callee.property.name;
|
|
@@ -698,7 +808,7 @@ function handleCallExpression(node, ctx) {
|
|
|
698
808
|
});
|
|
699
809
|
}
|
|
700
810
|
// Module._compile counts as temp file exec for write-execute-delete pattern
|
|
701
|
-
ctx.hasTempFileExec = ctx.hasTempFileExec || ctx.
|
|
811
|
+
ctx.hasTempFileExec = ctx.hasTempFileExec || ctx.hasDevShmInContent;
|
|
702
812
|
}
|
|
703
813
|
|
|
704
814
|
// SANDWORM_MODE: Track writeFileSync/writeFile to temp paths
|
|
@@ -706,13 +816,13 @@ function handleCallExpression(node, ctx) {
|
|
|
706
816
|
const arg = node.arguments && node.arguments[0];
|
|
707
817
|
if (arg) {
|
|
708
818
|
const strVal = extractStringValue(arg);
|
|
709
|
-
if (strVal &&
|
|
819
|
+
if (strVal && /\/dev\/shm\b/.test(strVal)) {
|
|
710
820
|
ctx.hasTempFileWrite = true;
|
|
711
821
|
}
|
|
712
|
-
// Variable reference to
|
|
822
|
+
// Variable reference to /dev/shm path
|
|
713
823
|
if (!strVal && (arg.type === 'Identifier' || arg.type === 'CallExpression' || arg.type === 'MemberExpression')) {
|
|
714
|
-
// Dynamic path — check if file content involves
|
|
715
|
-
ctx.hasTempFileWrite = ctx.hasTempFileWrite || ctx.
|
|
824
|
+
// Dynamic path — check if file content involves /dev/shm
|
|
825
|
+
ctx.hasTempFileWrite = ctx.hasTempFileWrite || ctx.hasDevShmInContent;
|
|
716
826
|
}
|
|
717
827
|
}
|
|
718
828
|
}
|
|
@@ -727,13 +837,30 @@ function handleCallExpression(node, ctx) {
|
|
|
727
837
|
if (callName === 'require' && node.arguments.length > 0) {
|
|
728
838
|
const arg = node.arguments[0];
|
|
729
839
|
const strVal = extractStringValue(arg);
|
|
730
|
-
if (strVal &&
|
|
840
|
+
if (strVal && /\/dev\/shm\b/.test(strVal)) {
|
|
731
841
|
ctx.hasTempFileExec = true;
|
|
732
|
-
} else if (!strVal && ctx.
|
|
733
|
-
// Variable argument in a file that references
|
|
842
|
+
} else if (!strVal && ctx.hasDevShmInContent) {
|
|
843
|
+
// Variable argument in a file that references /dev/shm
|
|
734
844
|
ctx.hasTempFileExec = true;
|
|
735
845
|
}
|
|
736
846
|
}
|
|
847
|
+
|
|
848
|
+
// SANDWORM_MODE R7: Detect Object.entries/keys/values(process.env)
|
|
849
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
850
|
+
node.callee.object?.type === 'Identifier' && node.callee.object.name === 'Object' &&
|
|
851
|
+
node.callee.property?.type === 'Identifier' &&
|
|
852
|
+
['entries', 'keys', 'values'].includes(node.callee.property.name) &&
|
|
853
|
+
node.arguments.length >= 1) {
|
|
854
|
+
const enumArg = node.arguments[0];
|
|
855
|
+
// Check if argument is process.env
|
|
856
|
+
if (enumArg.type === 'MemberExpression' &&
|
|
857
|
+
enumArg.object?.type === 'Identifier' && enumArg.object.name === 'process' &&
|
|
858
|
+
enumArg.property?.type === 'Identifier' && enumArg.property.name === 'env') {
|
|
859
|
+
ctx.hasEnvEnumeration = true;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// SANDWORM_MODE R8: dns.resolve detection moved to walk.ancestor() in ast.js (FIX 5)
|
|
737
864
|
}
|
|
738
865
|
|
|
739
866
|
function handleImportExpression(node, ctx) {
|
|
@@ -939,6 +1066,10 @@ function handleMemberExpression(node, ctx) {
|
|
|
939
1066
|
file: ctx.relFile
|
|
940
1067
|
});
|
|
941
1068
|
}
|
|
1069
|
+
// SANDWORM_MODE R9: Count LLM API key accesses
|
|
1070
|
+
if (LLM_API_KEY_VARS.includes(envVar)) {
|
|
1071
|
+
ctx.llmApiKeyCount++;
|
|
1072
|
+
}
|
|
942
1073
|
}
|
|
943
1074
|
}
|
|
944
1075
|
}
|
|
@@ -946,9 +1077,11 @@ function handleMemberExpression(node, ctx) {
|
|
|
946
1077
|
function handlePostWalk(ctx) {
|
|
947
1078
|
// SANDWORM_MODE: zlib inflate + base64 decode + eval/Function/Module._compile = obfuscated payload
|
|
948
1079
|
if (ctx.hasZlibInflate && ctx.hasBase64Decode && ctx.hasDynamicExec) {
|
|
1080
|
+
// FIX 4: dist/build files get LOW severity (bundlers legitimately use zlib+base64+eval)
|
|
1081
|
+
const isDistFile = /^(dist|build)[/\\]/i.test(ctx.relFile) || /\.bundle\.js$/i.test(ctx.relFile);
|
|
949
1082
|
ctx.threats.push({
|
|
950
1083
|
type: 'zlib_inflate_eval',
|
|
951
|
-
severity: 'CRITICAL',
|
|
1084
|
+
severity: isDistFile ? 'LOW' : 'CRITICAL',
|
|
952
1085
|
message: 'Obfuscated payload: zlib inflate + base64 decode + dynamic execution. No legitimate package uses this pattern.',
|
|
953
1086
|
file: ctx.relFile
|
|
954
1087
|
});
|
|
@@ -964,6 +1097,36 @@ function handlePostWalk(ctx) {
|
|
|
964
1097
|
});
|
|
965
1098
|
}
|
|
966
1099
|
|
|
1100
|
+
// SANDWORM_MODE R7: env harvesting = Object.entries/keys/values(process.env) + sensitive pattern in file
|
|
1101
|
+
if (ctx.hasEnvEnumeration && ctx.hasEnvHarvestPattern && ctx.hasNetworkCallInFile) {
|
|
1102
|
+
ctx.threats.push({
|
|
1103
|
+
type: 'env_harvesting_dynamic',
|
|
1104
|
+
severity: 'HIGH',
|
|
1105
|
+
message: 'Dynamic environment variable harvesting with sensitive pattern matching. Credential theft technique.',
|
|
1106
|
+
file: ctx.relFile
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// SANDWORM_MODE R8: DNS exfiltration = dns require + base64 encode + dns call (loop implied by co-occurrence)
|
|
1111
|
+
if (ctx.hasDnsLoop) {
|
|
1112
|
+
ctx.threats.push({
|
|
1113
|
+
type: 'dns_chunk_exfiltration',
|
|
1114
|
+
severity: 'HIGH',
|
|
1115
|
+
message: 'DNS exfiltration: data encoded in DNS queries. Covert channel for firewall bypass.',
|
|
1116
|
+
file: ctx.relFile
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// SANDWORM_MODE R9: LLM API key harvesting (3+ different providers = harvesting)
|
|
1121
|
+
if (ctx.llmApiKeyCount >= 3) {
|
|
1122
|
+
ctx.threats.push({
|
|
1123
|
+
type: 'llm_api_key_harvesting',
|
|
1124
|
+
severity: 'MEDIUM',
|
|
1125
|
+
message: `LLM API key harvesting: accessing ${ctx.llmApiKeyCount} AI provider keys. Monetization vector.`,
|
|
1126
|
+
file: ctx.relFile
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
967
1130
|
// JS reverse shell pattern
|
|
968
1131
|
if (ctx.hasJsReverseShell) {
|
|
969
1132
|
ctx.threats.push({
|
package/src/scanner/ast.js
CHANGED
|
@@ -84,7 +84,17 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
84
84
|
hasTempFileWrite: false,
|
|
85
85
|
hasTempFileExec: false,
|
|
86
86
|
hasFileDelete: false,
|
|
87
|
-
|
|
87
|
+
hasDevShmInContent: /\/dev\/shm\b/.test(content),
|
|
88
|
+
// SANDWORM_MODE P2: env harvesting co-occurrence
|
|
89
|
+
hasEnvEnumeration: false, // Object.entries/keys/values(process.env)
|
|
90
|
+
hasEnvHarvestPattern: /\b(KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|NPM|AWS|SSH|WEBHOOK)\b/.test(content),
|
|
91
|
+
hasNetworkCallInFile: /\b(fetch|https?\.request|https?\.get|dns\.resolve)\b/.test(content),
|
|
92
|
+
// SANDWORM_MODE P2: DNS exfiltration co-occurrence
|
|
93
|
+
hasDnsRequire: /\brequire\s*\(\s*['"]dns['"]\s*\)/.test(content) || /\bdns\s*\.\s*resolve/.test(content),
|
|
94
|
+
hasBase64Encode: /\.toString\s*\(\s*['"]base64(url)?['"]\s*\)/.test(content),
|
|
95
|
+
hasDnsLoop: false, // set when dns call inside loop context detected
|
|
96
|
+
// SANDWORM_MODE P2: LLM API key harvesting
|
|
97
|
+
llmApiKeyCount: 0
|
|
88
98
|
};
|
|
89
99
|
|
|
90
100
|
walk.simple(ast, {
|
|
@@ -97,6 +107,35 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
97
107
|
MemberExpression(node) { handleMemberExpression(node, ctx); }
|
|
98
108
|
});
|
|
99
109
|
|
|
110
|
+
// FIX 5: DNS chunk exfiltration — verify dns.resolve* is inside a loop body
|
|
111
|
+
if (ctx.hasDnsRequire && ctx.hasBase64Encode && !ctx.hasDnsLoop) {
|
|
112
|
+
walk.ancestor(ast, {
|
|
113
|
+
CallExpression(node, _state, ancestors) {
|
|
114
|
+
if (ctx.hasDnsLoop) return;
|
|
115
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
116
|
+
const name = node.callee.property.name;
|
|
117
|
+
if (['resolve', 'resolve4', 'resolveTxt', 'resolveCname'].includes(name)) {
|
|
118
|
+
for (const anc of ancestors) {
|
|
119
|
+
if (['ForStatement', 'WhileStatement', 'ForOfStatement',
|
|
120
|
+
'ForInStatement', 'DoWhileStatement'].includes(anc.type)) {
|
|
121
|
+
ctx.hasDnsLoop = true;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// forEach/map callback = implicit loop
|
|
125
|
+
if (anc.type === 'CallExpression' && anc.callee?.type === 'MemberExpression') {
|
|
126
|
+
const m = anc.callee.property?.name;
|
|
127
|
+
if (['forEach', 'map', 'reduce', 'filter'].includes(m)) {
|
|
128
|
+
ctx.hasDnsLoop = true;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
100
139
|
handlePostWalk(ctx);
|
|
101
140
|
|
|
102
141
|
return threats;
|