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/dataflow.js
CHANGED
|
@@ -28,6 +28,13 @@ async function analyzeDataFlow(targetPath) {
|
|
|
28
28
|
} catch {
|
|
29
29
|
continue;
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
// Respect // muaddib-ignore directive in first 5 lines (like eslint-disable)
|
|
33
|
+
const headerLines = content.slice(0, 1024).split('\n').slice(0, 5);
|
|
34
|
+
if (headerLines.some(line => line.includes('muaddib-ignore'))) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
const fileThreats = analyzeFile(content, file, targetPath);
|
|
32
39
|
threats.push(...fileThreats);
|
|
33
40
|
}
|
|
@@ -53,14 +60,25 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
53
60
|
const sources = [];
|
|
54
61
|
const sinks = [];
|
|
55
62
|
|
|
63
|
+
// Track variables assigned from sensitive path expressions
|
|
64
|
+
const sensitivePathVars = new Set();
|
|
65
|
+
|
|
56
66
|
walk.simple(ast, {
|
|
67
|
+
VariableDeclarator(node) {
|
|
68
|
+
if (node.id?.type === 'Identifier' && node.init) {
|
|
69
|
+
if (containsSensitiveLiteral(node.init)) {
|
|
70
|
+
sensitivePathVars.add(node.id.name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
57
75
|
CallExpression(node) {
|
|
58
76
|
const callName = getCallName(node);
|
|
59
|
-
|
|
77
|
+
|
|
60
78
|
if (callName === 'readFileSync' || callName === 'readFile' ||
|
|
61
79
|
callName === 'fs.readFileSync' || callName === 'fs.readFile') {
|
|
62
80
|
const arg = node.arguments[0];
|
|
63
|
-
if (arg && isCredentialPath(arg)) {
|
|
81
|
+
if (arg && isCredentialPath(arg, sensitivePathVars)) {
|
|
64
82
|
sources.push({
|
|
65
83
|
type: 'credential_read',
|
|
66
84
|
name: callName,
|
|
@@ -89,6 +107,65 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
89
107
|
}
|
|
90
108
|
}
|
|
91
109
|
}
|
|
110
|
+
|
|
111
|
+
// os.hostname(), os.networkInterfaces(), os.userInfo() as fingerprint sources
|
|
112
|
+
if (node.callee.type === 'MemberExpression') {
|
|
113
|
+
const obj = node.callee.object;
|
|
114
|
+
const prop = node.callee.property;
|
|
115
|
+
if (obj?.type === 'Identifier' && obj.name === 'os' && prop?.type === 'Identifier') {
|
|
116
|
+
if (['hostname', 'networkInterfaces', 'userInfo', 'cpus', 'totalmem', 'platform', 'arch', 'homedir'].includes(prop.name)) {
|
|
117
|
+
sources.push({
|
|
118
|
+
type: 'fingerprint_read',
|
|
119
|
+
name: `os.${prop.name}`,
|
|
120
|
+
line: node.loc?.start?.line
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// fs.readdirSync as credential source when reading sensitive directories
|
|
127
|
+
if (node.callee.type === 'MemberExpression') {
|
|
128
|
+
const prop = node.callee.property;
|
|
129
|
+
if (prop?.type === 'Identifier' && (prop.name === 'readdirSync' || prop.name === 'readdir')) {
|
|
130
|
+
const arg = node.arguments[0];
|
|
131
|
+
if (arg && isCredentialPath(arg, sensitivePathVars)) {
|
|
132
|
+
sources.push({
|
|
133
|
+
type: 'credential_read',
|
|
134
|
+
name: prop.name,
|
|
135
|
+
line: node.loc?.start?.line
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// MemberExpression network sinks: http.request, https.get, dns.resolve, net.connect, etc.
|
|
142
|
+
if (node.callee.type === 'MemberExpression') {
|
|
143
|
+
const obj = node.callee.object;
|
|
144
|
+
const prop = node.callee.property;
|
|
145
|
+
if (obj.type === 'Identifier' && prop.type === 'Identifier') {
|
|
146
|
+
// DNS resolution as exfiltration sink
|
|
147
|
+
if (obj.name === 'dns' && ['resolve', 'lookup', 'resolve4', 'resolve6'].includes(prop.name)) {
|
|
148
|
+
sinks.push({ type: 'network_send', name: `dns.${prop.name}`, line: node.loc?.start?.line });
|
|
149
|
+
}
|
|
150
|
+
// HTTP/HTTPS request/get as network sink
|
|
151
|
+
if ((obj.name === 'http' || obj.name === 'https') && ['request', 'get'].includes(prop.name)) {
|
|
152
|
+
sinks.push({ type: 'network_send', name: `${obj.name}.${prop.name}`, line: node.loc?.start?.line });
|
|
153
|
+
}
|
|
154
|
+
// net.connect / net.createConnection / tls.connect as network sink
|
|
155
|
+
if ((obj.name === 'net' || obj.name === 'tls') && ['connect', 'createConnection'].includes(prop.name)) {
|
|
156
|
+
sinks.push({ type: 'network_send', name: `${obj.name}.${prop.name}`, line: node.loc?.start?.line });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Track eval calls for staged payload detection
|
|
162
|
+
if (callName === 'eval') {
|
|
163
|
+
sinks.push({
|
|
164
|
+
type: 'eval_exec',
|
|
165
|
+
name: 'eval',
|
|
166
|
+
line: node.loc?.start?.line
|
|
167
|
+
});
|
|
168
|
+
}
|
|
92
169
|
},
|
|
93
170
|
|
|
94
171
|
MemberExpression(node) {
|
|
@@ -108,6 +185,18 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
108
185
|
}
|
|
109
186
|
});
|
|
110
187
|
|
|
188
|
+
// Detect staged payload: network fetch + eval in same file (no credential source needed)
|
|
189
|
+
const hasNetworkSink = sinks.some(s => s.type === 'network_send' || s.type === 'exec_network');
|
|
190
|
+
const hasEvalSink = sinks.some(s => s.type === 'eval_exec');
|
|
191
|
+
if (hasNetworkSink && hasEvalSink) {
|
|
192
|
+
threats.push({
|
|
193
|
+
type: 'staged_payload',
|
|
194
|
+
severity: 'CRITICAL',
|
|
195
|
+
message: 'Network fetch + eval() in same file (staged payload execution).',
|
|
196
|
+
file: path.relative(basePath, filePath)
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
111
200
|
if (sources.length > 0 && sinks.length > 0) {
|
|
112
201
|
// Determine severity by scope proximity: if source and sink are < 50 lines apart -> CRITICAL, else HIGH
|
|
113
202
|
let severity = 'HIGH';
|
|
@@ -132,23 +221,65 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
132
221
|
return threats;
|
|
133
222
|
}
|
|
134
223
|
|
|
135
|
-
|
|
224
|
+
const SENSITIVE_PATH_PATTERNS = [
|
|
225
|
+
'.npmrc', '.ssh', '.aws', '.gitconfig', '.env',
|
|
226
|
+
'/etc/passwd', '/etc/shadow', '/etc/hosts',
|
|
227
|
+
'.ethereum', '.electrum', '.config/solana', '.exodus',
|
|
228
|
+
'.atomic', '.metamask', '.ledger-live', '.trezor',
|
|
229
|
+
'.bitcoin', '.monero', '.gnupg'
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
function isSensitivePath(val) {
|
|
233
|
+
const lower = val.toLowerCase();
|
|
234
|
+
return SENSITIVE_PATH_PATTERNS.some(p => lower.includes(p));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Checks if an expression tree contains any sensitive path literal.
|
|
239
|
+
* Used to determine if a variable assignment should be tracked.
|
|
240
|
+
*/
|
|
241
|
+
function containsSensitiveLiteral(node) {
|
|
242
|
+
if (!node || typeof node !== 'object') return false;
|
|
243
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
244
|
+
return isSensitivePath(node.value);
|
|
245
|
+
}
|
|
246
|
+
if (node.type === 'TemplateLiteral') {
|
|
247
|
+
const quasiText = (node.quasis || []).map(q => q.value.raw).join('');
|
|
248
|
+
return isSensitivePath(quasiText);
|
|
249
|
+
}
|
|
250
|
+
if (node.type === 'BinaryExpression' && node.operator === '+') {
|
|
251
|
+
return containsSensitiveLiteral(node.left) || containsSensitiveLiteral(node.right);
|
|
252
|
+
}
|
|
253
|
+
if (node.type === 'CallExpression' && node.arguments) {
|
|
254
|
+
return node.arguments.some(a => containsSensitiveLiteral(a));
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function isCredentialPath(arg, sensitivePathVars) {
|
|
136
260
|
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
137
|
-
|
|
138
|
-
return val.includes('.npmrc') ||
|
|
139
|
-
val.includes('.ssh') ||
|
|
140
|
-
val.includes('.aws') ||
|
|
141
|
-
val.includes('.gitconfig') ||
|
|
142
|
-
val.includes('.env');
|
|
261
|
+
return isSensitivePath(arg.value);
|
|
143
262
|
}
|
|
144
263
|
if (arg.type === 'TemplateLiteral') {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
264
|
+
const quasiText = (arg.quasis || []).map(q => q.value.raw).join('');
|
|
265
|
+
return isSensitivePath(quasiText);
|
|
266
|
+
}
|
|
267
|
+
// Handle string concatenation: homedir() + '/.npmrc'
|
|
268
|
+
if (arg.type === 'BinaryExpression' && arg.operator === '+') {
|
|
269
|
+
return isCredentialPath(arg.left, sensitivePathVars) || isCredentialPath(arg.right, sensitivePathVars);
|
|
270
|
+
}
|
|
271
|
+
// Handle variable references: fs.readFileSync(npmrcPath) where npmrcPath was assigned a sensitive path
|
|
272
|
+
if (arg.type === 'Identifier' && sensitivePathVars && sensitivePathVars.has(arg.name)) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
// Handle path.join(dir, '.npmrc') or path.join(sshDir, 'id_rsa') where sshDir is tracked
|
|
276
|
+
if (arg.type === 'CallExpression' && arg.callee.type === 'MemberExpression') {
|
|
277
|
+
const obj = arg.callee.object;
|
|
278
|
+
const prop = arg.callee.property;
|
|
279
|
+
if (obj.type === 'Identifier' && obj.name === 'path' &&
|
|
280
|
+
prop.type === 'Identifier' && (prop.name === 'join' || prop.name === 'resolve')) {
|
|
281
|
+
return arg.arguments.some(a => isCredentialPath(a, sensitivePathVars));
|
|
282
|
+
}
|
|
152
283
|
}
|
|
153
284
|
return false;
|
|
154
285
|
}
|
package/src/scanner/package.js
CHANGED
|
@@ -19,7 +19,9 @@ const DANGEROUS_PATTERNS = [
|
|
|
19
19
|
{ pattern: /\.npmrc/, name: 'npmrc_access' },
|
|
20
20
|
{ pattern: /GITHUB_TOKEN/, name: 'github_token_access' },
|
|
21
21
|
{ pattern: /AWS_/, name: 'aws_credential_access' },
|
|
22
|
-
{ pattern: /base64/, name: 'base64_encoding' }
|
|
22
|
+
{ pattern: /base64/, name: 'base64_encoding' },
|
|
23
|
+
{ pattern: /require\s*\(\s*['"]https?['"]\)/, name: 'network_require' },
|
|
24
|
+
{ pattern: /node\s+-e\s/, name: 'node_inline_exec' }
|
|
23
25
|
];
|
|
24
26
|
|
|
25
27
|
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype', 'toString', 'valueOf']);
|
package/src/utils.js
CHANGED
|
@@ -12,9 +12,11 @@ const EXCLUDED_DIRS = ['node_modules', '.git', '.muaddib-cache'];
|
|
|
12
12
|
* Merged into every findFiles() call on top of the caller's excludedDirs.
|
|
13
13
|
*/
|
|
14
14
|
let _extraExcludedDirs = [];
|
|
15
|
+
let _scanRoot = '';
|
|
15
16
|
|
|
16
|
-
function setExtraExcludes(dirs) {
|
|
17
|
+
function setExtraExcludes(dirs, scanRoot) {
|
|
17
18
|
_extraExcludedDirs = Array.isArray(dirs) ? dirs : [];
|
|
19
|
+
_scanRoot = scanRoot || '';
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
function getExtraExcludes() {
|
|
@@ -91,10 +93,15 @@ function findFiles(dir, options = {}) {
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
for (const item of items) {
|
|
94
|
-
if (allExcludedDirs.includes(item)) continue;
|
|
95
|
-
|
|
96
96
|
const fullPath = path.join(dir, item);
|
|
97
97
|
|
|
98
|
+
// Check both bare name ("tests") and relative path ("src/scanner")
|
|
99
|
+
if (allExcludedDirs.includes(item)) continue;
|
|
100
|
+
if (_extraExcludedDirs.length > 0 && _scanRoot) {
|
|
101
|
+
const rel = path.relative(_scanRoot, fullPath).replace(/\\/g, '/');
|
|
102
|
+
if (_extraExcludedDirs.some(ex => rel === ex || rel.startsWith(ex + '/'))) continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
98
105
|
try {
|
|
99
106
|
const lstat = fs.lstatSync(fullPath);
|
|
100
107
|
|