muaddib-scanner 2.2.26 → 2.2.28
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/monitor.js +81 -18
- package/src/response/playbooks.js +387 -346
- package/src/rules/index.js +1136 -1038
- package/src/scanner/ast-detectors.js +237 -0
- package/src/scanner/ast.js +19 -1
|
@@ -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;
|
|
@@ -565,6 +675,7 @@ function handleCallExpression(node, ctx) {
|
|
|
565
675
|
|
|
566
676
|
if (callName === 'eval') {
|
|
567
677
|
ctx.hasEvalInFile = true;
|
|
678
|
+
ctx.hasDynamicExec = true;
|
|
568
679
|
// Detect staged eval decode
|
|
569
680
|
if (node.arguments.length === 1 && hasDecodeArg(node.arguments[0])) {
|
|
570
681
|
ctx.threats.push({
|
|
@@ -585,6 +696,7 @@ function handleCallExpression(node, ctx) {
|
|
|
585
696
|
});
|
|
586
697
|
}
|
|
587
698
|
} else if (callName === 'Function') {
|
|
699
|
+
ctx.hasDynamicExec = true;
|
|
588
700
|
// Detect staged Function decode
|
|
589
701
|
if (node.arguments.length >= 1 && hasDecodeArg(node.arguments[node.arguments.length - 1])) {
|
|
590
702
|
ctx.threats.push({
|
|
@@ -679,12 +791,83 @@ function handleCallExpression(node, ctx) {
|
|
|
679
791
|
});
|
|
680
792
|
}
|
|
681
793
|
if (propName === '_compile') {
|
|
794
|
+
ctx.hasDynamicExec = true;
|
|
682
795
|
ctx.threats.push({
|
|
683
796
|
type: 'module_compile',
|
|
684
797
|
severity: 'CRITICAL',
|
|
685
798
|
message: 'module._compile() detected — executes arbitrary code from string in module context (flatmap-stream pattern).',
|
|
686
799
|
file: ctx.relFile
|
|
687
800
|
});
|
|
801
|
+
// SANDWORM_MODE: Module._compile with non-literal argument = dynamic code execution
|
|
802
|
+
if (node.arguments.length >= 1 && !hasOnlyStringLiteralArgs(node)) {
|
|
803
|
+
ctx.threats.push({
|
|
804
|
+
type: 'module_compile_dynamic',
|
|
805
|
+
severity: 'CRITICAL',
|
|
806
|
+
message: 'In-memory code execution via Module._compile(). Common malware evasion technique.',
|
|
807
|
+
file: ctx.relFile
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
// Module._compile counts as temp file exec for write-execute-delete pattern
|
|
811
|
+
ctx.hasTempFileExec = ctx.hasTempFileExec || ctx.hasTmpdirInContent;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// SANDWORM_MODE: Track writeFileSync/writeFile to temp paths
|
|
815
|
+
if (propName === 'writeFileSync' || propName === 'writeFile') {
|
|
816
|
+
const arg = node.arguments && node.arguments[0];
|
|
817
|
+
if (arg) {
|
|
818
|
+
const strVal = extractStringValue(arg);
|
|
819
|
+
if (strVal && (/\/dev\/shm\b/.test(strVal) || /\btmp\b/i.test(strVal) || /\btemp\b/i.test(strVal))) {
|
|
820
|
+
ctx.hasTempFileWrite = true;
|
|
821
|
+
}
|
|
822
|
+
// Variable reference to tmpdir/temp path
|
|
823
|
+
if (!strVal && (arg.type === 'Identifier' || arg.type === 'CallExpression' || arg.type === 'MemberExpression')) {
|
|
824
|
+
// Dynamic path — check if file content involves tmpdir patterns
|
|
825
|
+
ctx.hasTempFileWrite = ctx.hasTempFileWrite || ctx.hasTmpdirInContent;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// SANDWORM_MODE: Track unlinkSync/rmSync (file deletion)
|
|
831
|
+
if (propName === 'unlinkSync' || propName === 'unlink' || propName === 'rmSync') {
|
|
832
|
+
ctx.hasFileDelete = true;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// SANDWORM_MODE: Track require() of temp path (execution of temp file)
|
|
837
|
+
if (callName === 'require' && node.arguments.length > 0) {
|
|
838
|
+
const arg = node.arguments[0];
|
|
839
|
+
const strVal = extractStringValue(arg);
|
|
840
|
+
if (strVal && (/\/dev\/shm\b/.test(strVal) || /\btmp\b/i.test(strVal) || /\btemp\b/i.test(strVal))) {
|
|
841
|
+
ctx.hasTempFileExec = true;
|
|
842
|
+
} else if (!strVal && ctx.hasTmpdirInContent) {
|
|
843
|
+
// Variable argument in a file that references tmpdir paths
|
|
844
|
+
ctx.hasTempFileExec = true;
|
|
845
|
+
}
|
|
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: Detect dns.resolve/resolve4/resolveTxt calls (flag for co-occurrence)
|
|
864
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
865
|
+
const dnsPropName = node.callee.property.name;
|
|
866
|
+
if (['resolve', 'resolve4', 'resolveTxt', 'resolveCname'].includes(dnsPropName)) {
|
|
867
|
+
// Set hasDnsLoop if file has dns require + base64 encoding (co-occurrence checked in postWalk)
|
|
868
|
+
if (ctx.hasDnsRequire && ctx.hasBase64Encode) {
|
|
869
|
+
ctx.hasDnsLoop = true;
|
|
870
|
+
}
|
|
688
871
|
}
|
|
689
872
|
}
|
|
690
873
|
}
|
|
@@ -892,11 +1075,65 @@ function handleMemberExpression(node, ctx) {
|
|
|
892
1075
|
file: ctx.relFile
|
|
893
1076
|
});
|
|
894
1077
|
}
|
|
1078
|
+
// SANDWORM_MODE R9: Count LLM API key accesses
|
|
1079
|
+
if (LLM_API_KEY_VARS.includes(envVar)) {
|
|
1080
|
+
ctx.llmApiKeyCount++;
|
|
1081
|
+
}
|
|
895
1082
|
}
|
|
896
1083
|
}
|
|
897
1084
|
}
|
|
898
1085
|
|
|
899
1086
|
function handlePostWalk(ctx) {
|
|
1087
|
+
// SANDWORM_MODE: zlib inflate + base64 decode + eval/Function/Module._compile = obfuscated payload
|
|
1088
|
+
if (ctx.hasZlibInflate && ctx.hasBase64Decode && ctx.hasDynamicExec) {
|
|
1089
|
+
ctx.threats.push({
|
|
1090
|
+
type: 'zlib_inflate_eval',
|
|
1091
|
+
severity: 'CRITICAL',
|
|
1092
|
+
message: 'Obfuscated payload: zlib inflate + base64 decode + dynamic execution. No legitimate package uses this pattern.',
|
|
1093
|
+
file: ctx.relFile
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// SANDWORM_MODE: write + execute + delete = anti-forensics staging
|
|
1098
|
+
if (ctx.hasTempFileWrite && ctx.hasTempFileExec && ctx.hasFileDelete) {
|
|
1099
|
+
ctx.threats.push({
|
|
1100
|
+
type: 'write_execute_delete',
|
|
1101
|
+
severity: 'HIGH',
|
|
1102
|
+
message: 'Anti-forensics: write, execute, then delete. Typical malware staging pattern.',
|
|
1103
|
+
file: ctx.relFile
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// SANDWORM_MODE R7: env harvesting = Object.entries/keys/values(process.env) + sensitive pattern in file
|
|
1108
|
+
if (ctx.hasEnvEnumeration && ctx.hasEnvHarvestPattern) {
|
|
1109
|
+
ctx.threats.push({
|
|
1110
|
+
type: 'env_harvesting_dynamic',
|
|
1111
|
+
severity: 'HIGH',
|
|
1112
|
+
message: 'Dynamic environment variable harvesting with sensitive pattern matching. Credential theft technique.',
|
|
1113
|
+
file: ctx.relFile
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// SANDWORM_MODE R8: DNS exfiltration = dns require + base64 encode + dns call (loop implied by co-occurrence)
|
|
1118
|
+
if (ctx.hasDnsLoop) {
|
|
1119
|
+
ctx.threats.push({
|
|
1120
|
+
type: 'dns_chunk_exfiltration',
|
|
1121
|
+
severity: 'HIGH',
|
|
1122
|
+
message: 'DNS exfiltration: data encoded in DNS queries. Covert channel for firewall bypass.',
|
|
1123
|
+
file: ctx.relFile
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// SANDWORM_MODE R9: LLM API key harvesting (3+ different providers = harvesting)
|
|
1128
|
+
if (ctx.llmApiKeyCount >= 3) {
|
|
1129
|
+
ctx.threats.push({
|
|
1130
|
+
type: 'llm_api_key_harvesting',
|
|
1131
|
+
severity: 'MEDIUM',
|
|
1132
|
+
message: `LLM API key harvesting: accessing ${ctx.llmApiKeyCount} AI provider keys. Monetization vector.`,
|
|
1133
|
+
file: ctx.relFile
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
900
1137
|
// JS reverse shell pattern
|
|
901
1138
|
if (ctx.hasJsReverseShell) {
|
|
902
1139
|
ctx.threats.push({
|
package/src/scanner/ast.js
CHANGED
|
@@ -75,7 +75,25 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
75
75
|
/\.pipe\b/.test(content) &&
|
|
76
76
|
(/\bspawn\b/.test(content) || /\bstdin\b/.test(content) || /\bstdout\b/.test(content)),
|
|
77
77
|
hasBinaryFileLiteral: /\.(png|jpg|jpeg|gif|bmp|ico|wasm)\b/i.test(content),
|
|
78
|
-
hasEvalInFile: false
|
|
78
|
+
hasEvalInFile: false,
|
|
79
|
+
// SANDWORM_MODE: zlib inflate + base64 + eval co-occurrence
|
|
80
|
+
hasZlibInflate: /\brequire\s*\(\s*['"]zlib['"]\s*\)/.test(content) || /\bzlib\s*\.\s*inflate/.test(content),
|
|
81
|
+
hasBase64Decode: /Buffer\.from\s*\([^)]*,\s*['"]base64['"]/.test(content),
|
|
82
|
+
hasDynamicExec: false, // set in handleCallExpression for eval/Function/Module._compile
|
|
83
|
+
// SANDWORM_MODE: write + execute + delete anti-forensics
|
|
84
|
+
hasTempFileWrite: false,
|
|
85
|
+
hasTempFileExec: false,
|
|
86
|
+
hasFileDelete: false,
|
|
87
|
+
hasTmpdirInContent: /\btmpdir\b|\/dev\/shm\b|\/tmp\b/i.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
|
+
// SANDWORM_MODE P2: DNS exfiltration co-occurrence
|
|
92
|
+
hasDnsRequire: /\brequire\s*\(\s*['"]dns['"]\s*\)/.test(content) || /\bdns\s*\.\s*resolve/.test(content),
|
|
93
|
+
hasBase64Encode: /\.toString\s*\(\s*['"]base64(url)?['"]\s*\)/.test(content),
|
|
94
|
+
hasDnsLoop: false, // set when dns call inside loop context detected
|
|
95
|
+
// SANDWORM_MODE P2: LLM API key harvesting
|
|
96
|
+
llmApiKeyCount: 0
|
|
79
97
|
};
|
|
80
98
|
|
|
81
99
|
walk.simple(ast, {
|