muaddib-scanner 2.1.5 → 2.2.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/README.fr.md +33 -8
- package/README.md +33 -8
- package/assets/logo2removebg.png +0 -0
- package/bin/muaddib.js +9 -0
- package/datasets/adversarial/README.md +23 -0
- package/datasets/adversarial/ai-agent-weaponization/index.js +5 -0
- package/datasets/adversarial/ai-agent-weaponization/package.json +9 -0
- package/datasets/adversarial/ai-agent-weaponization/setup.js +83 -0
- package/datasets/adversarial/ai-config-injection/.cursorrules +36 -0
- package/datasets/adversarial/ai-config-injection/index.js +16 -0
- package/datasets/adversarial/ai-config-injection/package.json +8 -0
- package/datasets/adversarial/browser-api-hook/index.js +66 -0
- package/datasets/adversarial/browser-api-hook/package.json +6 -0
- package/datasets/adversarial/bun-runtime-evasion/bun_environment.js +23 -0
- package/datasets/adversarial/bun-runtime-evasion/package.json +9 -0
- package/datasets/adversarial/bun-runtime-evasion/setup.js +10 -0
- package/datasets/adversarial/ci-trigger-exfil/index.js +17 -0
- package/datasets/adversarial/ci-trigger-exfil/package.json +9 -0
- package/datasets/adversarial/conditional-chain/index.js +14 -0
- package/datasets/adversarial/conditional-chain/package.json +9 -0
- package/datasets/adversarial/crypto-wallet-harvest/index.js +44 -0
- package/datasets/adversarial/crypto-wallet-harvest/package.json +6 -0
- package/datasets/adversarial/dead-mans-switch/index.js +35 -0
- package/datasets/adversarial/dead-mans-switch/package.json +9 -0
- package/datasets/adversarial/delayed-exfil/index.js +6 -0
- package/datasets/adversarial/delayed-exfil/package.json +6 -0
- package/datasets/adversarial/detached-background/launcher.js +11 -0
- package/datasets/adversarial/detached-background/package.json +9 -0
- package/datasets/adversarial/detached-background/worker.js +26 -0
- package/datasets/adversarial/discord-webhook-exfil/index.js +95 -0
- package/datasets/adversarial/discord-webhook-exfil/package.json +9 -0
- package/datasets/adversarial/dns-chunk-exfil/index.js +10 -0
- package/datasets/adversarial/dns-chunk-exfil/package.json +6 -0
- package/datasets/adversarial/docker-aware/index.js +10 -0
- package/datasets/adversarial/docker-aware/package.json +6 -0
- package/datasets/adversarial/double-base64-exfil/index.js +11 -0
- package/datasets/adversarial/double-base64-exfil/package.json +9 -0
- package/datasets/adversarial/dynamic-import/index.js +21 -0
- package/datasets/adversarial/dynamic-import/package.json +9 -0
- package/datasets/adversarial/dynamic-require/index.js +3 -0
- package/datasets/adversarial/dynamic-require/package.json +9 -0
- package/datasets/adversarial/fake-captcha-fingerprint/index.js +64 -0
- package/datasets/adversarial/fake-captcha-fingerprint/package.json +9 -0
- package/datasets/adversarial/gh-cli-token-steal/index.js +31 -0
- package/datasets/adversarial/gh-cli-token-steal/package.json +6 -0
- package/datasets/adversarial/github-exfil/index.js +33 -0
- package/datasets/adversarial/github-exfil/package.json +9 -0
- package/datasets/adversarial/iife-exfil/index.js +17 -0
- package/datasets/adversarial/iife-exfil/package.json +9 -0
- package/datasets/adversarial/nested-payload/index.js +3 -0
- package/datasets/adversarial/nested-payload/package.json +9 -0
- package/datasets/adversarial/nested-payload/utils/helper.js +6 -0
- package/datasets/adversarial/nested-payload/utils/lib/format.js +23 -0
- package/datasets/adversarial/postinstall-download/package.json +8 -0
- package/datasets/adversarial/preinstall-background-fork/bootstrap.js +16 -0
- package/datasets/adversarial/preinstall-background-fork/index.js +2 -0
- package/datasets/adversarial/preinstall-background-fork/package.json +9 -0
- package/datasets/adversarial/preinstall-background-fork/stealer.js +67 -0
- package/datasets/adversarial/preinstall-exec/package.json +9 -0
- package/datasets/adversarial/preinstall-exec/steal.js +24 -0
- package/datasets/adversarial/proxy-env-intercept/index.js +33 -0
- package/datasets/adversarial/proxy-env-intercept/package.json +9 -0
- package/datasets/adversarial/pyinstaller-dropper/index.js +25 -0
- package/datasets/adversarial/pyinstaller-dropper/package.json +9 -0
- package/datasets/adversarial/rdd-zero-deps/index.js +32 -0
- package/datasets/adversarial/rdd-zero-deps/init.js +15 -0
- package/datasets/adversarial/rdd-zero-deps/package.json +11 -0
- package/datasets/adversarial/remote-dynamic-dependency/index.js +15 -0
- package/datasets/adversarial/remote-dynamic-dependency/package.json +7 -0
- package/datasets/adversarial/self-hosted-runner-backdoor/index.js +28 -0
- package/datasets/adversarial/self-hosted-runner-backdoor/package.json +9 -0
- package/datasets/adversarial/silent-error-swallow/index.js +32 -0
- package/datasets/adversarial/silent-error-swallow/package.json +6 -0
- package/datasets/adversarial/staged-fetch/index.js +9 -0
- package/datasets/adversarial/staged-fetch/package.json +6 -0
- package/datasets/adversarial/string-concat-obfuscation/index.js +6 -0
- package/datasets/adversarial/string-concat-obfuscation/package.json +6 -0
- package/datasets/adversarial/template-literal-obfuscation/index.js +4 -0
- package/datasets/adversarial/template-literal-obfuscation/package.json +9 -0
- package/datasets/adversarial/triple-base64-github-push/index.js +38 -0
- package/datasets/adversarial/triple-base64-github-push/package.json +9 -0
- package/datasets/adversarial/websocket-exfil/index.js +34 -0
- package/datasets/adversarial/websocket-exfil/package.json +9 -0
- package/datasets/benign/README.md +20 -0
- package/datasets/benign/packages-npm.txt +98 -0
- package/datasets/benign/packages-pypi.txt +49 -0
- package/metrics/v2.1.5.json +753 -0
- package/metrics/v2.2.0.json +753 -0
- package/nul +0 -5
- package/package.json +1 -1
- package/src/commands/evaluate.js +270 -0
- package/src/index.js +12 -5
- package/src/ioc/bootstrap.js +1 -0
- package/src/response/playbooks.js +66 -0
- package/src/rules/index.js +181 -0
- package/src/scanner/ai-config.js +183 -0
- package/src/scanner/ast.js +496 -2
- package/src/scanner/dataflow.js +147 -16
- package/src/scanner/package.js +3 -1
- package/src/utils.js +10 -3
package/src/scanner/ast.js
CHANGED
|
@@ -23,7 +23,9 @@ const SENSITIVE_STRINGS = [
|
|
|
23
23
|
'.ssh',
|
|
24
24
|
'Shai-Hulud',
|
|
25
25
|
'The Second Coming',
|
|
26
|
-
'Goldox-T3chs'
|
|
26
|
+
'Goldox-T3chs',
|
|
27
|
+
'/etc/passwd',
|
|
28
|
+
'/etc/shadow'
|
|
27
29
|
];
|
|
28
30
|
|
|
29
31
|
// Env vars that are safe and should NOT be flagged (common config/runtime vars)
|
|
@@ -37,6 +39,18 @@ const ENV_SENSITIVE_KEYWORDS = [
|
|
|
37
39
|
'TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH'
|
|
38
40
|
];
|
|
39
41
|
|
|
42
|
+
// AI agent dangerous flags — disable security controls (s1ngularity/Nx, Aug 2025)
|
|
43
|
+
const AI_AGENT_DANGEROUS_FLAGS = [
|
|
44
|
+
'--dangerously-skip-permissions',
|
|
45
|
+
'--yolo',
|
|
46
|
+
'--trust-all-tools',
|
|
47
|
+
'--yes-always',
|
|
48
|
+
'--no-permission-check'
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// AI agent binary names
|
|
52
|
+
const AI_AGENT_BINARIES = ['claude', 'gemini', 'q', 'aider', 'copilot', 'cursor'];
|
|
53
|
+
|
|
40
54
|
// Strings that are NOT suspicious
|
|
41
55
|
const SAFE_STRINGS = [
|
|
42
56
|
'api.github.com',
|
|
@@ -44,6 +58,33 @@ const SAFE_STRINGS = [
|
|
|
44
58
|
'npmjs.com'
|
|
45
59
|
];
|
|
46
60
|
|
|
61
|
+
// Credential-stealing CLI commands (s1ngularity/Nx, Shai-Hulud)
|
|
62
|
+
const CREDENTIAL_CLI_COMMANDS = [
|
|
63
|
+
'gh auth token',
|
|
64
|
+
'gcloud auth print-access-token',
|
|
65
|
+
'aws sts get-session-token',
|
|
66
|
+
'az account get-access-token',
|
|
67
|
+
'heroku auth:token',
|
|
68
|
+
'netlify api --data',
|
|
69
|
+
'vercel whoami'
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// Dangerous shell command patterns for variable tracking
|
|
73
|
+
const DANGEROUS_CMD_PATTERNS = [/\bcurl\b/, /\bwget\b/, /\bnc\s+-/, /\/dev\/tcp\//, /\bbash\s+-i/];
|
|
74
|
+
|
|
75
|
+
// Native APIs targeted for prototype hooking (chalk Sept 2025, Sygnia)
|
|
76
|
+
const HOOKABLE_NATIVES = [
|
|
77
|
+
'fetch', 'XMLHttpRequest', 'Request', 'Response',
|
|
78
|
+
'WebSocket', 'EventSource'
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// Paths indicating sandbox/container environment detection (anti-analysis)
|
|
82
|
+
const SANDBOX_INDICATORS = [
|
|
83
|
+
'/.dockerenv',
|
|
84
|
+
'/proc/1/cgroup',
|
|
85
|
+
'/proc/self/cgroup'
|
|
86
|
+
];
|
|
87
|
+
|
|
47
88
|
async function analyzeAST(targetPath) {
|
|
48
89
|
const threats = [];
|
|
49
90
|
const files = findJsFiles(targetPath);
|
|
@@ -89,6 +130,27 @@ function hasOnlyStringLiteralArgs(node) {
|
|
|
89
130
|
return node.arguments.every(arg => arg.type === 'Literal' && typeof arg.value === 'string');
|
|
90
131
|
}
|
|
91
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Checks if an AST subtree contains decode patterns (base64, atob, fromCharCode).
|
|
135
|
+
*/
|
|
136
|
+
function containsDecodePattern(node) {
|
|
137
|
+
if (!node || typeof node !== 'object') return false;
|
|
138
|
+
if (node.type === 'Literal' && node.value === 'base64') return true;
|
|
139
|
+
if (node.type === 'Identifier' && (node.name === 'atob' || node.name === 'fromCharCode')) return true;
|
|
140
|
+
for (const key of Object.keys(node)) {
|
|
141
|
+
if (key === 'type' || key === 'start' || key === 'end' || key === 'loc') continue;
|
|
142
|
+
const child = node[key];
|
|
143
|
+
if (Array.isArray(child)) {
|
|
144
|
+
for (const item of child) {
|
|
145
|
+
if (item && typeof item.type === 'string' && containsDecodePattern(item)) return true;
|
|
146
|
+
}
|
|
147
|
+
} else if (child && typeof child === 'object' && typeof child.type === 'string') {
|
|
148
|
+
if (containsDecodePattern(child)) return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
92
154
|
function analyzeFile(content, filePath, basePath) {
|
|
93
155
|
const threats = [];
|
|
94
156
|
let ast;
|
|
@@ -111,10 +173,338 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
111
173
|
return threats;
|
|
112
174
|
}
|
|
113
175
|
|
|
176
|
+
// Track variables assigned from dynamic require() calls (non-literal arg)
|
|
177
|
+
const dynamicRequireVars = new Set();
|
|
178
|
+
// Track variables assigned dangerous command strings (curl, wget, etc.)
|
|
179
|
+
const dangerousCmdVars = new Map();
|
|
180
|
+
// Track variables assigned paths containing .github/workflows
|
|
181
|
+
const workflowPathVars = new Set();
|
|
182
|
+
// Track variables assigned temp/executable file paths
|
|
183
|
+
const execPathVars = new Map();
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Extract string value from a node if it's a Literal or TemplateLiteral with no expressions.
|
|
187
|
+
*/
|
|
188
|
+
function extractStringValue(node) {
|
|
189
|
+
if (!node) return null;
|
|
190
|
+
if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
|
|
191
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
192
|
+
return node.quasis.map(q => q.value.raw).join('');
|
|
193
|
+
}
|
|
194
|
+
// Template literal with expressions — concatenate what we can
|
|
195
|
+
if (node.type === 'TemplateLiteral') {
|
|
196
|
+
return node.quasis.map(q => q.value.raw).join('***');
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
114
201
|
walk.simple(ast, {
|
|
202
|
+
VariableDeclarator(node) {
|
|
203
|
+
if (node.id?.type === 'Identifier') {
|
|
204
|
+
// Track dynamic require vars
|
|
205
|
+
if (node.init?.type === 'CallExpression') {
|
|
206
|
+
const initCallName = getCallName(node.init);
|
|
207
|
+
if (initCallName === 'require' && node.init.arguments.length > 0) {
|
|
208
|
+
const arg = node.init.arguments[0];
|
|
209
|
+
if (arg.type !== 'Literal') {
|
|
210
|
+
dynamicRequireVars.add(node.id.name);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Track variables assigned dangerous command strings
|
|
215
|
+
const strVal = extractStringValue(node.init);
|
|
216
|
+
if (strVal && DANGEROUS_CMD_PATTERNS.some(p => p.test(strVal))) {
|
|
217
|
+
dangerousCmdVars.set(node.id.name, strVal);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Track variables assigned temp/executable file paths
|
|
221
|
+
if (strVal && /^\/tmp\/|^\/var\/tmp\/|\\temp\\/i.test(strVal)) {
|
|
222
|
+
execPathVars.set(node.id.name, strVal);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Track variables assigned from path.join containing .github/workflows
|
|
226
|
+
if (node.init?.type === 'CallExpression' && node.init.callee?.type === 'MemberExpression') {
|
|
227
|
+
const obj = node.init.callee.object;
|
|
228
|
+
const prop = node.init.callee.property;
|
|
229
|
+
if (obj?.type === 'Identifier' && obj.name === 'path' &&
|
|
230
|
+
prop?.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
|
|
231
|
+
const joinArgs = node.init.arguments.map(a => extractStringValue(a) || '').join('/');
|
|
232
|
+
if (/\.github[\\/\/]workflows/i.test(joinArgs) || /\.github[\\/\/]actions/i.test(joinArgs)) {
|
|
233
|
+
workflowPathVars.add(node.id.name);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
115
240
|
CallExpression(node) {
|
|
116
241
|
const callName = getCallName(node);
|
|
117
242
|
|
|
243
|
+
// Detect require() with non-literal argument (obfuscation)
|
|
244
|
+
if (callName === 'require' && node.arguments.length > 0) {
|
|
245
|
+
const arg = node.arguments[0];
|
|
246
|
+
if (arg.type === 'BinaryExpression' && arg.operator === '+') {
|
|
247
|
+
threats.push({
|
|
248
|
+
type: 'dynamic_require',
|
|
249
|
+
severity: 'HIGH',
|
|
250
|
+
message: 'Dynamic require() with string concatenation (module name obfuscation).',
|
|
251
|
+
file: path.relative(basePath, filePath)
|
|
252
|
+
});
|
|
253
|
+
} else if (arg.type === 'TemplateLiteral' && arg.expressions.length > 0) {
|
|
254
|
+
threats.push({
|
|
255
|
+
type: 'dynamic_require',
|
|
256
|
+
severity: 'HIGH',
|
|
257
|
+
message: 'Dynamic require() with template literal (module name obfuscation).',
|
|
258
|
+
file: path.relative(basePath, filePath)
|
|
259
|
+
});
|
|
260
|
+
} else if (arg.type === 'CallExpression') {
|
|
261
|
+
const argCallName = getCallName(arg);
|
|
262
|
+
// Skip safe patterns: require(path.join(...)), require(path.resolve(...))
|
|
263
|
+
if (argCallName !== 'path.join' && argCallName !== 'path.resolve') {
|
|
264
|
+
const hasDecode = containsDecodePattern(arg);
|
|
265
|
+
threats.push({
|
|
266
|
+
type: 'dynamic_require',
|
|
267
|
+
severity: hasDecode ? 'CRITICAL' : 'HIGH',
|
|
268
|
+
message: hasDecode
|
|
269
|
+
? 'Dynamic require() with runtime decode (base64/atob obfuscation).'
|
|
270
|
+
: 'Dynamic require() with computed argument (possible decode obfuscation).',
|
|
271
|
+
file: path.relative(basePath, filePath)
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
} else if (arg.type === 'Identifier') {
|
|
275
|
+
threats.push({
|
|
276
|
+
type: 'dynamic_require',
|
|
277
|
+
severity: 'HIGH',
|
|
278
|
+
message: 'Dynamic require() with variable argument (module name obfuscation).',
|
|
279
|
+
file: path.relative(basePath, filePath)
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Detect exec/execSync with dangerous shell commands (direct or via MemberExpression)
|
|
285
|
+
const execName = callName === 'exec' || callName === 'execSync' ? callName : null;
|
|
286
|
+
const memberExec = !execName && node.callee.type === 'MemberExpression' &&
|
|
287
|
+
node.callee.property?.type === 'Identifier' &&
|
|
288
|
+
(node.callee.property.name === 'exec' || node.callee.property.name === 'execSync')
|
|
289
|
+
? node.callee.property.name : null;
|
|
290
|
+
if ((execName || memberExec) && node.arguments.length > 0) {
|
|
291
|
+
const arg = node.arguments[0];
|
|
292
|
+
let cmdStr = null;
|
|
293
|
+
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
294
|
+
cmdStr = arg.value;
|
|
295
|
+
} else if (arg.type === 'Identifier' && dangerousCmdVars.has(arg.name)) {
|
|
296
|
+
// Variable was assigned a dangerous command string
|
|
297
|
+
cmdStr = dangerousCmdVars.get(arg.name);
|
|
298
|
+
} else if (arg.type === 'Identifier' && execPathVars.has(arg.name)) {
|
|
299
|
+
// Variable was assigned a temp/executable file path
|
|
300
|
+
cmdStr = execPathVars.get(arg.name);
|
|
301
|
+
} else if (arg.type === 'TemplateLiteral') {
|
|
302
|
+
cmdStr = arg.quasis.map(q => q.value.raw).join('***');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (cmdStr) {
|
|
306
|
+
// Check for dangerous shell patterns
|
|
307
|
+
if (/\|\s*(sh|bash)\b/.test(cmdStr) || /nc\s+-[elp]/.test(cmdStr) || /\/dev\/tcp\//.test(cmdStr) || /bash\s+-i/.test(cmdStr) || /\bcurl\b/.test(cmdStr) || /\bwget\b/.test(cmdStr)) {
|
|
308
|
+
threats.push({
|
|
309
|
+
type: 'dangerous_exec',
|
|
310
|
+
severity: 'CRITICAL',
|
|
311
|
+
message: `Dangerous shell command in exec(): "${cmdStr.substring(0, 80)}"`,
|
|
312
|
+
file: path.relative(basePath, filePath)
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Check for temp file execution (binary dropper pattern)
|
|
317
|
+
if (/^\/tmp\/|^\/var\/tmp\//i.test(cmdStr)) {
|
|
318
|
+
threats.push({
|
|
319
|
+
type: 'dangerous_exec',
|
|
320
|
+
severity: 'CRITICAL',
|
|
321
|
+
message: `Execution of temp file "${cmdStr.substring(0, 80)}" — binary dropper pattern.`,
|
|
322
|
+
file: path.relative(basePath, filePath)
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check for credential-stealing CLI commands
|
|
327
|
+
for (const credCmd of CREDENTIAL_CLI_COMMANDS) {
|
|
328
|
+
if (cmdStr.includes(credCmd)) {
|
|
329
|
+
threats.push({
|
|
330
|
+
type: 'credential_command_exec',
|
|
331
|
+
severity: 'CRITICAL',
|
|
332
|
+
message: `Credential theft via CLI tool: exec("${credCmd}") — steals auth tokens from installed tools.`,
|
|
333
|
+
file: path.relative(basePath, filePath)
|
|
334
|
+
});
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Detect exec/execSync called on a dynamically-required module variable
|
|
342
|
+
// Pattern: const mod = require(obfuscated); mod.exec(...) → obfuscated command execution
|
|
343
|
+
// Note: getCallName returns 'exec' for both exec() and mod.exec(), so we check execName too
|
|
344
|
+
if ((execName || memberExec) && node.callee.type === 'MemberExpression' && node.callee.object?.type === 'Identifier') {
|
|
345
|
+
if (dynamicRequireVars.has(node.callee.object.name)) {
|
|
346
|
+
const method = execName || memberExec;
|
|
347
|
+
threats.push({
|
|
348
|
+
type: 'dynamic_require_exec',
|
|
349
|
+
severity: 'CRITICAL',
|
|
350
|
+
message: `${method}() called on dynamically-required module "${node.callee.object.name}" — obfuscated command execution.`,
|
|
351
|
+
file: path.relative(basePath, filePath)
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Detect sandbox/container evasion: fs.accessSync('/.dockerenv'), fs.existsSync('/.dockerenv'), etc.
|
|
357
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
358
|
+
const fsMethod = node.callee.property.name;
|
|
359
|
+
if (['accessSync', 'existsSync', 'statSync', 'lstatSync', 'access', 'stat'].includes(fsMethod)) {
|
|
360
|
+
const arg = node.arguments[0];
|
|
361
|
+
if (arg?.type === 'Literal' && typeof arg.value === 'string') {
|
|
362
|
+
if (SANDBOX_INDICATORS.some(ind => arg.value.includes(ind))) {
|
|
363
|
+
threats.push({
|
|
364
|
+
type: 'sandbox_evasion',
|
|
365
|
+
severity: 'HIGH',
|
|
366
|
+
message: `Sandbox/container detection via ${fsMethod}("${arg.value}") — anti-analysis technique.`,
|
|
367
|
+
file: path.relative(basePath, filePath)
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Detect spawn/fork with {detached: true} — background process evasion
|
|
375
|
+
if ((callName === 'spawn' || callName === 'fork') && node.arguments.length >= 2) {
|
|
376
|
+
const lastArg = node.arguments[node.arguments.length - 1];
|
|
377
|
+
if (lastArg.type === 'ObjectExpression') {
|
|
378
|
+
const hasDetached = lastArg.properties.some(p =>
|
|
379
|
+
p.key?.type === 'Identifier' && p.key.name === 'detached' &&
|
|
380
|
+
p.value?.type === 'Literal' && p.value.value === true
|
|
381
|
+
);
|
|
382
|
+
if (hasDetached) {
|
|
383
|
+
threats.push({
|
|
384
|
+
type: 'detached_process',
|
|
385
|
+
severity: 'HIGH',
|
|
386
|
+
message: `${callName}() with {detached: true} — background process survives parent exit (evasion technique).`,
|
|
387
|
+
file: path.relative(basePath, filePath)
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Detect fs.writeFileSync/writeFile to .github/workflows — workflow injection persistence
|
|
394
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
395
|
+
const writeMethod = node.callee.property.name;
|
|
396
|
+
if (['writeFileSync', 'writeFile'].includes(writeMethod) && node.arguments.length > 0) {
|
|
397
|
+
const pathArg = node.arguments[0];
|
|
398
|
+
const pathStr = extractStringValue(pathArg);
|
|
399
|
+
// Also check path.join() arguments
|
|
400
|
+
let joinedPath = null;
|
|
401
|
+
let hasWorkflowVar = false;
|
|
402
|
+
if (pathArg?.type === 'CallExpression' && pathArg.arguments) {
|
|
403
|
+
joinedPath = pathArg.arguments.map(a => {
|
|
404
|
+
if (a.type === 'Identifier' && workflowPathVars.has(a.name)) {
|
|
405
|
+
hasWorkflowVar = true;
|
|
406
|
+
return '.github/workflows';
|
|
407
|
+
}
|
|
408
|
+
return extractStringValue(a) || '';
|
|
409
|
+
}).join('/');
|
|
410
|
+
}
|
|
411
|
+
// Direct Identifier reference to a tracked workflow path variable
|
|
412
|
+
if (pathArg?.type === 'Identifier' && workflowPathVars.has(pathArg.name)) {
|
|
413
|
+
hasWorkflowVar = true;
|
|
414
|
+
}
|
|
415
|
+
const checkPath = pathStr || joinedPath || '';
|
|
416
|
+
if (hasWorkflowVar || /\.github[\\/]workflows/i.test(checkPath) || /\.github[\\/]actions/i.test(checkPath)) {
|
|
417
|
+
threats.push({
|
|
418
|
+
type: 'workflow_write',
|
|
419
|
+
severity: 'HIGH',
|
|
420
|
+
message: `${writeMethod}() creates file in .github/workflows — GitHub Actions persistence technique.`,
|
|
421
|
+
file: path.relative(basePath, filePath)
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Detect fs.mkdirSync creating .github/workflows — part of workflow injection
|
|
428
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
429
|
+
const mkdirMethod = node.callee.property.name;
|
|
430
|
+
if ((mkdirMethod === 'mkdirSync' || mkdirMethod === 'mkdir') && node.arguments.length > 0) {
|
|
431
|
+
const pathArg = node.arguments[0];
|
|
432
|
+
// Check if it's a tracked workflow path variable
|
|
433
|
+
if (pathArg?.type === 'Identifier' && workflowPathVars.has(pathArg.name)) {
|
|
434
|
+
threats.push({
|
|
435
|
+
type: 'workflow_write',
|
|
436
|
+
severity: 'HIGH',
|
|
437
|
+
message: `${mkdirMethod}() creates .github/workflows directory — GitHub Actions persistence technique.`,
|
|
438
|
+
file: path.relative(basePath, filePath)
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Detect fs.chmodSync with executable permissions — binary dropper pattern
|
|
445
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
446
|
+
const chmodMethod = node.callee.property.name;
|
|
447
|
+
if ((chmodMethod === 'chmodSync' || chmodMethod === 'chmod') && node.arguments.length >= 2) {
|
|
448
|
+
const modeArg = node.arguments[1];
|
|
449
|
+
if (modeArg?.type === 'Literal' && typeof modeArg.value === 'number') {
|
|
450
|
+
// 0o755=493, 0o777=511, 0o700=448, 0o775=509
|
|
451
|
+
if (modeArg.value === 493 || modeArg.value === 511 || modeArg.value === 448 || modeArg.value === 509) {
|
|
452
|
+
threats.push({
|
|
453
|
+
type: 'binary_dropper',
|
|
454
|
+
severity: 'CRITICAL',
|
|
455
|
+
message: `${chmodMethod}() with executable permissions (0o${modeArg.value.toString(8)}) — binary dropper pattern.`,
|
|
456
|
+
file: path.relative(basePath, filePath)
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Detect AI agent weaponization: spawn/exec of AI agents with dangerous flags
|
|
464
|
+
// s1ngularity/Nx pattern (Aug 2025): invoke local AI coding agents to steal credentials
|
|
465
|
+
if ((callName === 'spawn' || callName === 'exec' || callName === 'execSync' ||
|
|
466
|
+
callName === 'execFile' || callName === 'execFileSync' || memberExec) &&
|
|
467
|
+
node.arguments.length > 0) {
|
|
468
|
+
// Collect all string literals in arguments (including arrays)
|
|
469
|
+
const argStrings = [];
|
|
470
|
+
for (const arg of node.arguments) {
|
|
471
|
+
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
472
|
+
argStrings.push(arg.value);
|
|
473
|
+
} else if (arg.type === 'ArrayExpression') {
|
|
474
|
+
for (const el of arg.elements) {
|
|
475
|
+
if (el && el.type === 'Literal' && typeof el.value === 'string') {
|
|
476
|
+
argStrings.push(el.value);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const allArgText = argStrings.join(' ');
|
|
483
|
+
const hasDangerousFlag = AI_AGENT_DANGEROUS_FLAGS.some(flag => allArgText.includes(flag));
|
|
484
|
+
const firstArg = node.arguments[0];
|
|
485
|
+
const cmdName = firstArg?.type === 'Literal' && typeof firstArg.value === 'string'
|
|
486
|
+
? firstArg.value.toLowerCase() : '';
|
|
487
|
+
const isAIAgent = AI_AGENT_BINARIES.some(bin => cmdName === bin || cmdName.endsWith('/' + bin));
|
|
488
|
+
|
|
489
|
+
if (hasDangerousFlag) {
|
|
490
|
+
const matchedFlag = AI_AGENT_DANGEROUS_FLAGS.find(flag => allArgText.includes(flag));
|
|
491
|
+
threats.push({
|
|
492
|
+
type: 'ai_agent_abuse',
|
|
493
|
+
severity: 'CRITICAL',
|
|
494
|
+
message: `AI agent invoked with security bypass flag "${matchedFlag}"${isAIAgent ? ` (agent: ${cmdName})` : ''} — weaponized AI coding assistant (s1ngularity/Nx pattern).`,
|
|
495
|
+
file: path.relative(basePath, filePath)
|
|
496
|
+
});
|
|
497
|
+
} else if (isAIAgent) {
|
|
498
|
+
// AI agent binary called without known dangerous flag — still suspicious in a package
|
|
499
|
+
threats.push({
|
|
500
|
+
type: 'ai_agent_abuse',
|
|
501
|
+
severity: 'HIGH',
|
|
502
|
+
message: `AI coding agent "${cmdName}" invoked from package — potential AI agent weaponization.`,
|
|
503
|
+
file: path.relative(basePath, filePath)
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
118
508
|
if (callName === 'eval') {
|
|
119
509
|
const isConstant = hasOnlyStringLiteralArgs(node);
|
|
120
510
|
threats.push({
|
|
@@ -140,6 +530,33 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
140
530
|
}
|
|
141
531
|
},
|
|
142
532
|
|
|
533
|
+
ImportExpression(node) {
|
|
534
|
+
// import() dynamic — same concern as dynamic require()
|
|
535
|
+
if (node.source) {
|
|
536
|
+
const src = node.source;
|
|
537
|
+
if (src.type === 'Literal' && typeof src.value === 'string') {
|
|
538
|
+
// Static import('fs') — flag dangerous modules
|
|
539
|
+
const dangerousModules = ['child_process', 'fs', 'http', 'https', 'net', 'dns', 'tls'];
|
|
540
|
+
if (dangerousModules.includes(src.value)) {
|
|
541
|
+
threats.push({
|
|
542
|
+
type: 'dynamic_import',
|
|
543
|
+
severity: 'HIGH',
|
|
544
|
+
message: `Dynamic import() of dangerous module "${src.value}".`,
|
|
545
|
+
file: path.relative(basePath, filePath)
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
// Non-literal import source — obfuscation
|
|
550
|
+
threats.push({
|
|
551
|
+
type: 'dynamic_import',
|
|
552
|
+
severity: 'HIGH',
|
|
553
|
+
message: 'Dynamic import() with computed argument (possible obfuscation).',
|
|
554
|
+
file: path.relative(basePath, filePath)
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
|
|
143
560
|
NewExpression(node) {
|
|
144
561
|
if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
|
|
145
562
|
const isConstant = hasOnlyStringLiteralArgs(node);
|
|
@@ -152,6 +569,21 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
152
569
|
file: path.relative(basePath, filePath)
|
|
153
570
|
});
|
|
154
571
|
}
|
|
572
|
+
|
|
573
|
+
// Detect new Proxy(process.env, handler) — env interception
|
|
574
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'Proxy' && node.arguments.length >= 2) {
|
|
575
|
+
const target = node.arguments[0];
|
|
576
|
+
if (target.type === 'MemberExpression' &&
|
|
577
|
+
target.object?.name === 'process' &&
|
|
578
|
+
target.property?.name === 'env') {
|
|
579
|
+
threats.push({
|
|
580
|
+
type: 'env_proxy_intercept',
|
|
581
|
+
severity: 'CRITICAL',
|
|
582
|
+
message: 'new Proxy(process.env) detected — intercepts all environment variable access.',
|
|
583
|
+
file: path.relative(basePath, filePath)
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
155
587
|
},
|
|
156
588
|
|
|
157
589
|
Literal(node) {
|
|
@@ -160,7 +592,7 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
160
592
|
if (SAFE_STRINGS.some(s => node.value.includes(s))) {
|
|
161
593
|
return;
|
|
162
594
|
}
|
|
163
|
-
|
|
595
|
+
|
|
164
596
|
for (const sensitive of SENSITIVE_STRINGS) {
|
|
165
597
|
if (node.value.includes(sensitive)) {
|
|
166
598
|
threats.push({
|
|
@@ -171,6 +603,68 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
171
603
|
});
|
|
172
604
|
}
|
|
173
605
|
}
|
|
606
|
+
|
|
607
|
+
// Detect AI agent dangerous flags as string literals anywhere in code
|
|
608
|
+
for (const flag of AI_AGENT_DANGEROUS_FLAGS) {
|
|
609
|
+
if (node.value === flag) {
|
|
610
|
+
threats.push({
|
|
611
|
+
type: 'ai_agent_abuse',
|
|
612
|
+
severity: 'CRITICAL',
|
|
613
|
+
message: `AI agent security bypass flag "${flag}" found — weaponized AI coding assistant (s1ngularity/Nx pattern).`,
|
|
614
|
+
file: path.relative(basePath, filePath)
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
|
|
621
|
+
AssignmentExpression(node) {
|
|
622
|
+
// Detect prototype hooking of native browser/network APIs
|
|
623
|
+
// Pattern: globalThis.fetch = function(...) or XMLHttpRequest.prototype.send = function(...)
|
|
624
|
+
if (node.left?.type === 'MemberExpression') {
|
|
625
|
+
const left = node.left;
|
|
626
|
+
|
|
627
|
+
// globalThis.fetch = ... or globalThis.XMLHttpRequest = ...
|
|
628
|
+
if (left.object?.type === 'Identifier' && left.object.name === 'globalThis' &&
|
|
629
|
+
left.property?.type === 'Identifier') {
|
|
630
|
+
if (HOOKABLE_NATIVES.includes(left.property.name)) {
|
|
631
|
+
threats.push({
|
|
632
|
+
type: 'prototype_hook',
|
|
633
|
+
severity: 'HIGH',
|
|
634
|
+
message: `globalThis.${left.property.name} overridden — native API hooking for traffic interception.`,
|
|
635
|
+
file: path.relative(basePath, filePath)
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// XMLHttpRequest.prototype.send = ... or Response.prototype.json = ...
|
|
641
|
+
if (left.object?.type === 'MemberExpression' &&
|
|
642
|
+
left.object.property?.type === 'Identifier' &&
|
|
643
|
+
left.object.property.name === 'prototype' &&
|
|
644
|
+
left.object.object?.type === 'Identifier') {
|
|
645
|
+
if (HOOKABLE_NATIVES.includes(left.object.object.name)) {
|
|
646
|
+
threats.push({
|
|
647
|
+
type: 'prototype_hook',
|
|
648
|
+
severity: 'HIGH',
|
|
649
|
+
message: `${left.object.object.name}.prototype.${left.property?.name || '?'} overridden — native API hooking for traffic interception.`,
|
|
650
|
+
file: path.relative(basePath, filePath)
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// http.request = ... or https.get = ... (Node.js module hooking)
|
|
656
|
+
if (left.object?.type === 'Identifier' &&
|
|
657
|
+
['http', 'https'].includes(left.object.name) &&
|
|
658
|
+
left.property?.type === 'Identifier' &&
|
|
659
|
+
['request', 'get', 'createServer'].includes(left.property.name) &&
|
|
660
|
+
node.right?.type === 'FunctionExpression') {
|
|
661
|
+
threats.push({
|
|
662
|
+
type: 'prototype_hook',
|
|
663
|
+
severity: 'HIGH',
|
|
664
|
+
message: `${left.object.name}.${left.property.name} overridden — Node.js network module hooking for traffic interception.`,
|
|
665
|
+
file: path.relative(basePath, filePath)
|
|
666
|
+
});
|
|
667
|
+
}
|
|
174
668
|
}
|
|
175
669
|
},
|
|
176
670
|
|