muaddib-scanner 2.5.13 → 2.5.14
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/logs/alerts/{2026-03-07T17-50-50-825-evil-pkg.json → 2026-03-07T19-15-13-572-evil-pkg.json} +1 -1
- package/logs/alerts/{2026-03-07T17-50-50-826-suspect-pkg.json → 2026-03-07T19-15-13-572-suspect-pkg.json} +1 -1
- package/logs/alerts/{2026-03-07T17-50-50-827-evil-pkg.json → 2026-03-07T19-15-13-573-evil-pkg.json} +1 -1
- package/logs/alerts/{2026-03-07T17-50-51-268-evil-pkg.json → 2026-03-07T19-15-13-948-evil-pkg.json} +1 -1
- package/logs/daily-reports/2026-03-07.json +4 -4
- package/package.json +1 -1
- package/src/ioc/updater.js +7 -0
- package/src/response/playbooks.js +13 -0
- package/src/rules/index.js +36 -0
- package/src/scanner/ast-detectors.js +144 -15
- package/src/scanner/ast.js +3 -0
- package/src/scanner/dataflow.js +27 -0
- package/src/scanner/entropy.js +37 -1
- package/src/scanner/shell.js +4 -1
- package/src/scanner/typosquat.js +23 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"date": "2026-03-07",
|
|
3
|
-
"timestamp": "2026-03-
|
|
3
|
+
"timestamp": "2026-03-07T19:15:14.082Z",
|
|
4
4
|
"embed": {
|
|
5
5
|
"embeds": [
|
|
6
6
|
{
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"name": "Top Suspects",
|
|
37
|
-
"value": "1. **npm/test-dedup-detection-
|
|
37
|
+
"value": "1. **npm/test-dedup-detection-1772910913569@1.0.0** — 1 finding(s)",
|
|
38
38
|
"inline": false
|
|
39
39
|
}
|
|
40
40
|
],
|
|
41
41
|
"footer": {
|
|
42
|
-
"text": "MUAD'DIB - Daily summary | 2026-03-07
|
|
42
|
+
"text": "MUAD'DIB - Daily summary | 2026-03-07 19:15:14 UTC"
|
|
43
43
|
},
|
|
44
|
-
"timestamp": "2026-03-
|
|
44
|
+
"timestamp": "2026-03-07T19:15:14.081Z"
|
|
45
45
|
}
|
|
46
46
|
]
|
|
47
47
|
},
|
package/package.json
CHANGED
package/src/ioc/updater.js
CHANGED
|
@@ -455,6 +455,13 @@ function invalidateCache() {
|
|
|
455
455
|
// ============================================
|
|
456
456
|
// Key is derived from a stable machine-specific seed + hardcoded salt.
|
|
457
457
|
// This protects against local file tampering by unauthorized processes.
|
|
458
|
+
//
|
|
459
|
+
// RISK ACCEPTED (v2.5.14): Full cryptographic signing of IOC updates (e.g., Ed25519
|
|
460
|
+
// signatures verified against a pinned public key) was evaluated but not implemented.
|
|
461
|
+
// Current mitigations: HTTPS-only downloads + domain allowlist in src/shared/download.js
|
|
462
|
+
// + HMAC-SHA256 integrity for cached data. The HMAC key is machine-local, so it does
|
|
463
|
+
// not protect against a compromised upstream source — that risk is accepted given the
|
|
464
|
+
// cost/benefit trade-off and the existing HTTPS + domain pinning controls.
|
|
458
465
|
const IOC_HMAC_SALT = 'muaddib-ioc-integrity-v1';
|
|
459
466
|
|
|
460
467
|
function getIOCHMACKey() {
|
|
@@ -165,6 +165,15 @@ const PLAYBOOKS = {
|
|
|
165
165
|
fifo_reverse_shell:
|
|
166
166
|
'CRITIQUE: Reverse shell FIFO/named pipe detecte. Machine potentiellement compromise. Isoler immediatement.',
|
|
167
167
|
|
|
168
|
+
fifo_nc_reverse_shell:
|
|
169
|
+
'CRITIQUE: Reverse shell via mkfifo + netcat detecte. Machine potentiellement compromise. Isoler immediatement. Verifier les connexions sortantes actives.',
|
|
170
|
+
|
|
171
|
+
base64_decode_exec:
|
|
172
|
+
'CRITIQUE: Payload encode en base64 pipe vers un shell. Decoder le payload pour analyse: echo "<payload>" | base64 -d. Isoler la machine si deja execute.',
|
|
173
|
+
|
|
174
|
+
wget_base64_decode:
|
|
175
|
+
'Telechargement + decodage base64 detecte. Verifier l\'URL de telechargement et decoder le contenu. Pattern de staging malveillant en deux etapes.',
|
|
176
|
+
|
|
168
177
|
shai_hulud_backdoor:
|
|
169
178
|
'CRITIQUE: Backdoor Shai-Hulud dans GitHub Actions. Supprimer le workflow et auditer les runs precedents.',
|
|
170
179
|
|
|
@@ -448,6 +457,10 @@ const PLAYBOOKS = {
|
|
|
448
457
|
'new Worker() avec eval:true detecte. Le code s\'execute dans un thread worker separe, contournant la detection AST du thread principal. ' +
|
|
449
458
|
'Verifier le contenu du code passe au Worker. Si dynamique ou obfusque, supprimer le package. ' +
|
|
450
459
|
'Analyser les communications inter-threads (parentPort, workerData) pour identifier le payload.',
|
|
460
|
+
|
|
461
|
+
fragmented_high_entropy_cluster:
|
|
462
|
+
'Cluster de chaines courtes a haute entropie detecte. Possible fragmentation de payload pour eviter la detection. ' +
|
|
463
|
+
'Reconstituer les fragments et analyser le contenu combine. Verifier si les chaines sont concatenees ou reassemblees a l\'execution.',
|
|
451
464
|
};
|
|
452
465
|
|
|
453
466
|
function getPlaybook(threatType) {
|
package/src/rules/index.js
CHANGED
|
@@ -385,6 +385,33 @@ const RULES = {
|
|
|
385
385
|
references: ['https://attack.mitre.org/techniques/T1059/004/'],
|
|
386
386
|
mitre: 'T1059.004'
|
|
387
387
|
},
|
|
388
|
+
fifo_nc_reverse_shell: {
|
|
389
|
+
id: 'MUADDIB-SHELL-013',
|
|
390
|
+
name: 'FIFO + Netcat Reverse Shell',
|
|
391
|
+
severity: 'CRITICAL',
|
|
392
|
+
confidence: 'high',
|
|
393
|
+
description: 'Reverse shell via mkfifo + netcat (sans /dev/tcp). Technique alternative de reverse shell utilisant un named pipe.',
|
|
394
|
+
references: ['https://attack.mitre.org/techniques/T1059/004/'],
|
|
395
|
+
mitre: 'T1059.004'
|
|
396
|
+
},
|
|
397
|
+
base64_decode_exec: {
|
|
398
|
+
id: 'MUADDIB-SHELL-014',
|
|
399
|
+
name: 'Base64 Decode Pipe to Shell',
|
|
400
|
+
severity: 'CRITICAL',
|
|
401
|
+
confidence: 'high',
|
|
402
|
+
description: 'Payload encode en base64 decode et pipe vers bash/sh. Technique d\'obfuscation courante pour cacher des commandes malveillantes.',
|
|
403
|
+
references: ['https://attack.mitre.org/techniques/T1140/'],
|
|
404
|
+
mitre: 'T1140'
|
|
405
|
+
},
|
|
406
|
+
wget_base64_decode: {
|
|
407
|
+
id: 'MUADDIB-SHELL-015',
|
|
408
|
+
name: 'Wget + Base64 Decode',
|
|
409
|
+
severity: 'HIGH',
|
|
410
|
+
confidence: 'high',
|
|
411
|
+
description: 'Telechargement via wget suivi de decodage base64. Pattern de staging en deux etapes pour dropper un payload.',
|
|
412
|
+
references: ['https://attack.mitre.org/techniques/T1105/'],
|
|
413
|
+
mitre: 'T1105'
|
|
414
|
+
},
|
|
388
415
|
|
|
389
416
|
// AST additional patterns
|
|
390
417
|
possible_obfuscation: {
|
|
@@ -958,6 +985,15 @@ const RULES = {
|
|
|
958
985
|
references: ['https://attack.mitre.org/techniques/T1027/'],
|
|
959
986
|
mitre: 'T1027'
|
|
960
987
|
},
|
|
988
|
+
fragmented_high_entropy_cluster: {
|
|
989
|
+
id: 'MUADDIB-ENTROPY-004',
|
|
990
|
+
name: 'Fragmented High Entropy Cluster',
|
|
991
|
+
severity: 'MEDIUM',
|
|
992
|
+
confidence: 'medium',
|
|
993
|
+
description: 'Cluster de chaines courtes a haute entropie (8-49 chars) detecte. Technique de fragmentation de payload pour contourner le seuil de longueur minimum d\'analyse entropique.',
|
|
994
|
+
references: ['https://attack.mitre.org/techniques/T1027/'],
|
|
995
|
+
mitre: 'T1027'
|
|
996
|
+
},
|
|
961
997
|
js_obfuscation_pattern: {
|
|
962
998
|
id: 'MUADDIB-ENTROPY-003',
|
|
963
999
|
name: 'JS Obfuscation Pattern',
|
|
@@ -330,6 +330,44 @@ function handleVariableDeclarator(node, ctx) {
|
|
|
330
330
|
ctx.globalThisAliases.add(node.id.name);
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
// B1: const E = eval; const F = Function;
|
|
334
|
+
if (node.init?.type === 'Identifier' &&
|
|
335
|
+
(node.init.name === 'eval' || node.init.name === 'Function')) {
|
|
336
|
+
ctx.evalAliases.set(node.id.name, node.init.name);
|
|
337
|
+
}
|
|
338
|
+
// B1: const E = (x) => eval(x); const E = function(x) { return eval(x); }
|
|
339
|
+
if ((node.init?.type === 'ArrowFunctionExpression' || node.init?.type === 'FunctionExpression') &&
|
|
340
|
+
node.init.params?.length >= 1) {
|
|
341
|
+
const body = node.init.body;
|
|
342
|
+
if (body?.type === 'CallExpression') {
|
|
343
|
+
const cn = getCallName(body);
|
|
344
|
+
if (cn === 'eval' || cn === 'Function') ctx.evalAliases.set(node.id.name, cn);
|
|
345
|
+
}
|
|
346
|
+
if (body?.type === 'BlockStatement' && body.body?.length === 1 &&
|
|
347
|
+
body.body[0].type === 'ReturnStatement' && body.body[0].argument?.type === 'CallExpression') {
|
|
348
|
+
const cn = getCallName(body.body[0].argument);
|
|
349
|
+
if (cn === 'eval' || cn === 'Function') ctx.evalAliases.set(node.id.name, cn);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// B5: Track object literal string properties
|
|
354
|
+
if (node.init?.type === 'ObjectExpression') {
|
|
355
|
+
const propMap = new Map();
|
|
356
|
+
for (const prop of node.init.properties) {
|
|
357
|
+
if (prop.type !== 'Property') continue;
|
|
358
|
+
const key = prop.key?.type === 'Identifier' ? prop.key.name :
|
|
359
|
+
(prop.key?.type === 'Literal' ? String(prop.key.value) : null);
|
|
360
|
+
const val = extractStringValueDeep(prop.value);
|
|
361
|
+
if (key && val) propMap.set(key, val);
|
|
362
|
+
}
|
|
363
|
+
if (propMap.size > 0) ctx.objectPropertyMap.set(node.id.name, propMap);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Track initial string values for reassignment tracking
|
|
367
|
+
if (strVal !== null && strVal !== undefined) {
|
|
368
|
+
ctx.stringVarValues.set(node.id.name, strVal);
|
|
369
|
+
}
|
|
370
|
+
|
|
333
371
|
// Track variables assigned from path.join containing .github/workflows
|
|
334
372
|
if (node.init?.type === 'CallExpression' && node.init.callee?.type === 'MemberExpression') {
|
|
335
373
|
const obj = node.init.callee.object;
|
|
@@ -442,17 +480,68 @@ function handleCallExpression(node, ctx) {
|
|
|
442
480
|
});
|
|
443
481
|
}
|
|
444
482
|
} else if (arg.type === 'Identifier') {
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
483
|
+
// Check if variable was reassignment-tracked to a dangerous module
|
|
484
|
+
const DANGEROUS_MODS_REQ = ['child_process', 'fs', 'net', 'dns', 'http', 'https', 'tls'];
|
|
485
|
+
const resolvedVal = ctx.stringVarValues?.get(arg.name);
|
|
486
|
+
if (resolvedVal) {
|
|
487
|
+
const norm = resolvedVal.startsWith('node:') ? resolvedVal.slice(5) : resolvedVal;
|
|
488
|
+
if (DANGEROUS_MODS_REQ.includes(norm)) {
|
|
489
|
+
ctx.threats.push({
|
|
490
|
+
type: 'dynamic_require', severity: 'CRITICAL',
|
|
491
|
+
message: `require(${arg.name}) resolves to "${norm}" via variable reassignment — module name obfuscation.`,
|
|
492
|
+
file: ctx.relFile
|
|
493
|
+
});
|
|
494
|
+
} else {
|
|
495
|
+
// If the variable was assigned from a static value (string literal,
|
|
496
|
+
// array of strings, object with string values), it's a plugin loader pattern
|
|
497
|
+
const severity = ctx.staticAssignments.has(arg.name) ? 'LOW' : 'HIGH';
|
|
498
|
+
ctx.threats.push({
|
|
499
|
+
type: 'dynamic_require',
|
|
500
|
+
severity,
|
|
501
|
+
message: severity === 'LOW'
|
|
502
|
+
? `Dynamic require() with statically-assigned variable "${arg.name}" (plugin loader pattern).`
|
|
503
|
+
: 'Dynamic require() with variable argument (module name obfuscation).',
|
|
504
|
+
file: ctx.relFile
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
// If the variable was assigned from a static value (string literal,
|
|
509
|
+
// array of strings, object with string values), it's a plugin loader pattern
|
|
510
|
+
const severity = ctx.staticAssignments.has(arg.name) ? 'LOW' : 'HIGH';
|
|
511
|
+
ctx.threats.push({
|
|
512
|
+
type: 'dynamic_require',
|
|
513
|
+
severity,
|
|
514
|
+
message: severity === 'LOW'
|
|
515
|
+
? `Dynamic require() with statically-assigned variable "${arg.name}" (plugin loader pattern).`
|
|
516
|
+
: 'Dynamic require() with variable argument (module name obfuscation).',
|
|
517
|
+
file: ctx.relFile
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// B5: require(obj.prop) — MemberExpression argument
|
|
522
|
+
else if (arg.type === 'MemberExpression') {
|
|
523
|
+
const objName = arg.object?.type === 'Identifier' ? arg.object.name : null;
|
|
524
|
+
const propName = arg.property?.type === 'Identifier' ? arg.property.name :
|
|
525
|
+
(arg.property?.type === 'Literal' ? String(arg.property.value) : null);
|
|
526
|
+
const DANGEROUS_MODS = ['child_process', 'fs', 'net', 'dns', 'http', 'https', 'tls'];
|
|
527
|
+
let resolved = false;
|
|
528
|
+
if (objName && propName && ctx.objectPropertyMap?.has(objName)) {
|
|
529
|
+
const val = ctx.objectPropertyMap.get(objName).get(propName);
|
|
530
|
+
if (val) {
|
|
531
|
+
const norm = val.startsWith('node:') ? val.slice(5) : val;
|
|
532
|
+
if (DANGEROUS_MODS.includes(norm)) {
|
|
533
|
+
ctx.threats.push({ type: 'dynamic_require', severity: 'CRITICAL',
|
|
534
|
+
message: `require(${objName}.${propName}) resolves to "${norm}" — object property indirection.`,
|
|
535
|
+
file: ctx.relFile });
|
|
536
|
+
resolved = true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!resolved) {
|
|
541
|
+
ctx.threats.push({ type: 'dynamic_require', severity: 'HIGH',
|
|
542
|
+
message: 'Dynamic require() with member expression argument (object property obfuscation).',
|
|
543
|
+
file: ctx.relFile });
|
|
544
|
+
}
|
|
456
545
|
}
|
|
457
546
|
// Wave 4: detect require() of .node binary files (native addon camouflage)
|
|
458
547
|
const reqStr = extractStringValueDeep(arg);
|
|
@@ -881,6 +970,19 @@ function handleCallExpression(node, ctx) {
|
|
|
881
970
|
}
|
|
882
971
|
}
|
|
883
972
|
|
|
973
|
+
// B1: Alias call — E('code') where E = eval or F = Function
|
|
974
|
+
if (node.callee.type === 'Identifier' && ctx.evalAliases?.has(node.callee.name)) {
|
|
975
|
+
const aliased = ctx.evalAliases.get(node.callee.name);
|
|
976
|
+
ctx.hasEvalInFile = true;
|
|
977
|
+
ctx.hasDynamicExec = true;
|
|
978
|
+
ctx.threats.push({
|
|
979
|
+
type: aliased === 'eval' ? 'dangerous_call_eval' : 'dangerous_call_function',
|
|
980
|
+
severity: 'HIGH',
|
|
981
|
+
message: `Indirect ${aliased} via alias "${node.callee.name}" — eval wrapper evasion.`,
|
|
982
|
+
file: ctx.relFile
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
|
|
884
986
|
if (callName === 'eval') {
|
|
885
987
|
ctx.hasEvalInFile = true;
|
|
886
988
|
ctx.hasDynamicExec = true;
|
|
@@ -1432,6 +1534,29 @@ function handleLiteral(node, ctx) {
|
|
|
1432
1534
|
}
|
|
1433
1535
|
|
|
1434
1536
|
function handleAssignmentExpression(node, ctx) {
|
|
1537
|
+
// Variable reassignment: x += 'process' or x = x + 'process'
|
|
1538
|
+
if (node.left?.type === 'Identifier') {
|
|
1539
|
+
if (node.operator === '+=' && ctx.stringVarValues.has(node.left.name)) {
|
|
1540
|
+
const rightVal = extractStringValueDeep(node.right);
|
|
1541
|
+
if (rightVal !== null) {
|
|
1542
|
+
const combined = ctx.stringVarValues.get(node.left.name) + rightVal;
|
|
1543
|
+
ctx.stringVarValues.set(node.left.name, combined);
|
|
1544
|
+
if (DANGEROUS_CMD_PATTERNS.some(p => p.test(combined))) {
|
|
1545
|
+
ctx.dangerousCmdVars.set(node.left.name, combined);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (node.operator === '=' && node.right?.type === 'BinaryExpression') {
|
|
1550
|
+
const resolved = resolveStringConcat(node.right);
|
|
1551
|
+
if (resolved) {
|
|
1552
|
+
ctx.stringVarValues.set(node.left.name, resolved);
|
|
1553
|
+
if (DANGEROUS_CMD_PATTERNS.some(p => p.test(resolved))) {
|
|
1554
|
+
ctx.dangerousCmdVars.set(node.left.name, resolved);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1435
1560
|
// Detect object property indirection: obj.exec = require('child_process').exec
|
|
1436
1561
|
// or obj.fn = eval — stashing dangerous functions in object properties
|
|
1437
1562
|
if (node.left?.type === 'MemberExpression' && node.right) {
|
|
@@ -1489,14 +1614,17 @@ function handleAssignmentExpression(node, ctx) {
|
|
|
1489
1614
|
if (node.left?.type === 'MemberExpression') {
|
|
1490
1615
|
const left = node.left;
|
|
1491
1616
|
|
|
1492
|
-
// globalThis.fetch = ... or globalThis.XMLHttpRequest = ...
|
|
1493
|
-
if (left.object?.type === 'Identifier' &&
|
|
1617
|
+
// globalThis.fetch = ... or globalThis.XMLHttpRequest = ... (B2: include aliases)
|
|
1618
|
+
if (left.object?.type === 'Identifier' &&
|
|
1619
|
+
(left.object.name === 'globalThis' || left.object.name === 'global' ||
|
|
1620
|
+
left.object.name === 'window' || left.object.name === 'self' ||
|
|
1621
|
+
ctx.globalThisAliases.has(left.object.name)) &&
|
|
1494
1622
|
left.property?.type === 'Identifier') {
|
|
1495
1623
|
if (HOOKABLE_NATIVES.includes(left.property.name)) {
|
|
1496
1624
|
ctx.threats.push({
|
|
1497
1625
|
type: 'prototype_hook',
|
|
1498
1626
|
severity: 'HIGH',
|
|
1499
|
-
message:
|
|
1627
|
+
message: `${left.object.name}.${left.property.name} overridden — native API hooking for traffic interception.`,
|
|
1500
1628
|
file: ctx.relFile
|
|
1501
1629
|
});
|
|
1502
1630
|
}
|
|
@@ -1712,7 +1840,8 @@ function handlePostWalk(ctx) {
|
|
|
1712
1840
|
|
|
1713
1841
|
// Wave 4: Download-execute-cleanup — https download + chmod executable + execSync + unlink
|
|
1714
1842
|
// Exclude when all URLs in the file point to safe registries (npm, GitHub, nodejs.org)
|
|
1715
|
-
|
|
1843
|
+
// B4: removed fetchOnlySafeDomains guard — compound requires fetch+chmod+exec, which is never legitimate
|
|
1844
|
+
if (ctx.hasRemoteFetch && ctx.hasChmodExecutable && ctx.hasExecSyncCall) {
|
|
1716
1845
|
ctx.threats.push({
|
|
1717
1846
|
type: 'download_exec_binary',
|
|
1718
1847
|
severity: 'CRITICAL',
|
package/src/scanner/ast.js
CHANGED
|
@@ -70,6 +70,9 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
70
70
|
workflowPathVars: new Set(),
|
|
71
71
|
execPathVars: new Map(),
|
|
72
72
|
globalThisAliases: new Set(),
|
|
73
|
+
evalAliases: new Map(), // B1: variable name → 'eval'|'Function'
|
|
74
|
+
objectPropertyMap: new Map(), // B5: objName → Map<propName, stringValue>
|
|
75
|
+
stringVarValues: new Map(), // Variable reassignment tracking: varName → string value
|
|
73
76
|
hasFromCharCode: content.includes('fromCharCode'),
|
|
74
77
|
hasJsReverseShell: /\bnet\.Socket\b/.test(content) &&
|
|
75
78
|
/\.connect\s*\(/.test(content) &&
|
package/src/scanner/dataflow.js
CHANGED
|
@@ -203,6 +203,33 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
+
// B7: Taint propagation through data-preserving wrappers
|
|
207
|
+
if (initNode.type === 'CallExpression') {
|
|
208
|
+
const callee = initNode.callee;
|
|
209
|
+
let isTaintWrapper = false;
|
|
210
|
+
// JSON.stringify(x) / JSON.parse(x)
|
|
211
|
+
if (callee?.type === 'MemberExpression' &&
|
|
212
|
+
callee.object?.type === 'Identifier' && callee.object.name === 'JSON' &&
|
|
213
|
+
callee.property?.type === 'Identifier' &&
|
|
214
|
+
(callee.property.name === 'stringify' || callee.property.name === 'parse')) {
|
|
215
|
+
isTaintWrapper = true;
|
|
216
|
+
}
|
|
217
|
+
// x.toString() / String(x) / Buffer.from(x)
|
|
218
|
+
if (callee?.type === 'MemberExpression' &&
|
|
219
|
+
callee.property?.type === 'Identifier' && callee.property.name === 'toString') {
|
|
220
|
+
isTaintWrapper = true;
|
|
221
|
+
}
|
|
222
|
+
if (callee?.type === 'Identifier' && callee.name === 'String') {
|
|
223
|
+
isTaintWrapper = true;
|
|
224
|
+
}
|
|
225
|
+
if (isTaintWrapper && initNode.arguments.length >= 1) {
|
|
226
|
+
const wrappedArg = initNode.arguments[0];
|
|
227
|
+
if (wrappedArg.type === 'Identifier' && sensitivePathVars.has(wrappedArg.name)) {
|
|
228
|
+
sensitivePathVars.add(node.id.name);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
206
233
|
// Track exec result capture: const output = execSync('cmd')
|
|
207
234
|
if (initNode.type === 'CallExpression') {
|
|
208
235
|
let execName = null;
|
package/src/scanner/entropy.js
CHANGED
|
@@ -228,7 +228,26 @@ function scanEntropy(targetPath, options = {}) {
|
|
|
228
228
|
const strings = extractStringLiterals(content);
|
|
229
229
|
for (const str of strings) {
|
|
230
230
|
if (str.length < MIN_STRING_LENGTH) continue;
|
|
231
|
-
|
|
231
|
+
|
|
232
|
+
// B12: Windowed analysis for strings > MAX_STRING_LENGTH
|
|
233
|
+
if (str.length > MAX_STRING_LENGTH) {
|
|
234
|
+
if (SOURCE_MAP_REGEX.test(str) || SHA256_HEX_REGEX.test(str)) continue;
|
|
235
|
+
const WINDOW = 500, WIN_THRESHOLD = 6.0;
|
|
236
|
+
for (let i = 0; i < str.length; i += WINDOW) {
|
|
237
|
+
const w = str.slice(i, i + WINDOW);
|
|
238
|
+
if (w.length < 20) continue;
|
|
239
|
+
if (calculateShannonEntropy(w) > WIN_THRESHOLD) {
|
|
240
|
+
threats.push({
|
|
241
|
+
type: 'high_entropy_string',
|
|
242
|
+
severity: ENCODING_TABLE_RE.test(relativePath) ? 'LOW' : 'MEDIUM',
|
|
243
|
+
message: `High entropy window in long string (${str.length} chars, offset ${i}) — possible padded payload`,
|
|
244
|
+
file: relativePath
|
|
245
|
+
});
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
232
251
|
|
|
233
252
|
// Skip whitelisted patterns
|
|
234
253
|
if (isWhitelistedString(str, relativePath)) continue;
|
|
@@ -245,6 +264,23 @@ function scanEntropy(targetPath, options = {}) {
|
|
|
245
264
|
});
|
|
246
265
|
}
|
|
247
266
|
}
|
|
267
|
+
|
|
268
|
+
// B11: Fragment cluster — many short high-entropy strings = payload fragmentation
|
|
269
|
+
const FRAG_MIN = 8, FRAG_MAX = 49, FRAG_COUNT = 5, FRAG_ENTROPY = 4.5;
|
|
270
|
+
const frags = strings.filter(s =>
|
|
271
|
+
s.length >= FRAG_MIN && s.length <= FRAG_MAX &&
|
|
272
|
+
!SOURCE_MAP_REGEX.test(s) && !SHA256_HEX_REGEX.test(s) && !MD5_HEX_REGEX.test(s) &&
|
|
273
|
+
!UUID_REGEX.test(s) && !JWT_REGEX.test(s) &&
|
|
274
|
+
calculateShannonEntropy(s) > FRAG_ENTROPY
|
|
275
|
+
);
|
|
276
|
+
if (frags.length >= FRAG_COUNT) {
|
|
277
|
+
threats.push({
|
|
278
|
+
type: 'fragmented_high_entropy_cluster',
|
|
279
|
+
severity: ENCODING_TABLE_RE.test(relativePath) ? 'LOW' : 'MEDIUM',
|
|
280
|
+
message: `Fragment cluster: ${frags.length} short high-entropy strings (8-49 chars) — possible payload fragmentation.`,
|
|
281
|
+
file: relativePath
|
|
282
|
+
});
|
|
283
|
+
}
|
|
248
284
|
});
|
|
249
285
|
|
|
250
286
|
return threats;
|
package/src/scanner/shell.js
CHANGED
|
@@ -16,7 +16,10 @@ const MALICIOUS_PATTERNS = [
|
|
|
16
16
|
{ pattern: /(?:cat|readFile|cp|mv|curl\s+file:\/\/|tar\s+.*|scp\s+).*\.ssh/m, name: 'ssh_access', severity: 'HIGH' },
|
|
17
17
|
{ pattern: /python\s+-c.*import\s+socket/m, name: 'python_reverse_shell', severity: 'CRITICAL' },
|
|
18
18
|
{ pattern: /perl\s+-e.*socket/m, name: 'perl_reverse_shell', severity: 'CRITICAL' },
|
|
19
|
-
{ pattern: /mkfifo.*\/dev\/tcp/m, name: 'fifo_reverse_shell', severity: 'CRITICAL' }
|
|
19
|
+
{ pattern: /mkfifo.*\/dev\/tcp/m, name: 'fifo_reverse_shell', severity: 'CRITICAL' },
|
|
20
|
+
{ pattern: /mkfifo\s+\S+.*(?:\|\s*nc\s|nc\s+\S+.*>\s*\/tmp\/)/m, name: 'fifo_nc_reverse_shell', severity: 'CRITICAL' },
|
|
21
|
+
{ pattern: /base64\s+-d\b.*\|\s*(ba)?sh/m, name: 'base64_decode_exec', severity: 'CRITICAL' },
|
|
22
|
+
{ pattern: /wget\s+\S+.*&&.*base64\s+-d/m, name: 'wget_base64_decode', severity: 'HIGH' }
|
|
20
23
|
];
|
|
21
24
|
|
|
22
25
|
async function scanShellScripts(targetPath) {
|
package/src/scanner/typosquat.js
CHANGED
|
@@ -116,6 +116,20 @@ const WHITELIST = new Set([
|
|
|
116
116
|
]);
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
// B13: Pair-aware whitelist — only skip comparison with the specific popular package
|
|
120
|
+
const WHITELIST_PAIRS = new Map([
|
|
121
|
+
['chai', 'chalk'], ['pino', 'sinon'], ['ioredis', 'redis'],
|
|
122
|
+
['bcryptjs', 'bcrypt'], ['recast', 'react'], ['asyncdi', 'async'],
|
|
123
|
+
['redux', 'redis'], ['args', 'yargs'], ['oxlint', 'eslint'], ['vasync', 'async'],
|
|
124
|
+
['conf', 'config'], ['defu', 'debug'], ['ohash', 'lodash'], ['cors', 'colors'],
|
|
125
|
+
['meant', 'react'], ['whelk', 'chalk'], ['tslog', 'tslib'], ['mkdist', 'mkdirp'],
|
|
126
|
+
['jshint', 'eslint'], ['dtslint', 'eslint'], ['redis', 'redux'],
|
|
127
|
+
['cypress', 'express'], ['colord', 'colors'], ['read', 'react'],
|
|
128
|
+
['ulid', 'uuid'], ['tslint', 'eslint'], ['jison', 'sinon'],
|
|
129
|
+
['reds', 'redis'], ['docdash', 'lodash'], ['yarpm', 'yargs'],
|
|
130
|
+
['canvg', 'canvas'], ['mocks', 'mocha'], ['reactor', 'react']
|
|
131
|
+
]);
|
|
132
|
+
|
|
119
133
|
// Pre-computed lowercase versions for performance
|
|
120
134
|
const POPULAR_PACKAGES_LOWER = POPULAR_PACKAGES.map(p => p.toLowerCase());
|
|
121
135
|
|
|
@@ -317,9 +331,9 @@ async function scanTyposquatting(targetPath) {
|
|
|
317
331
|
function findTyposquatMatch(name) {
|
|
318
332
|
const nameLower = name.toLowerCase();
|
|
319
333
|
|
|
320
|
-
// Ignore les packages whitelistes
|
|
321
|
-
if (WHITELIST.has(nameLower)) return null;
|
|
322
|
-
|
|
334
|
+
// Ignore les packages whitelistes (B13: only skip entirely if not in pair-aware map)
|
|
335
|
+
if (WHITELIST.has(nameLower) && !WHITELIST_PAIRS.has(nameLower)) return null;
|
|
336
|
+
|
|
323
337
|
// Ignore les packages scoped (@org/package)
|
|
324
338
|
if (name.startsWith('@')) return null;
|
|
325
339
|
|
|
@@ -329,6 +343,9 @@ function findTyposquatMatch(name) {
|
|
|
329
343
|
// Ignore les packages avec suffixes legitimes courants
|
|
330
344
|
if (isLegitimateVariant(nameLower)) return null;
|
|
331
345
|
|
|
346
|
+
// B13: Get the specific popular package this whitelisted name is paired with
|
|
347
|
+
const pairedTarget = WHITELIST_PAIRS.get(nameLower);
|
|
348
|
+
|
|
332
349
|
for (let i = 0; i < POPULAR_PACKAGES.length; i++) {
|
|
333
350
|
const popularLower = POPULAR_PACKAGES_LOWER[i];
|
|
334
351
|
const popular = POPULAR_PACKAGES[i];
|
|
@@ -336,6 +353,9 @@ function findTyposquatMatch(name) {
|
|
|
336
353
|
// Ignore si c'est exactement le meme
|
|
337
354
|
if (nameLower === popularLower) continue;
|
|
338
355
|
|
|
356
|
+
// B13: Skip only the intended pair for whitelisted packages
|
|
357
|
+
if (pairedTarget && pairedTarget === popularLower) continue;
|
|
358
|
+
|
|
339
359
|
// Ignore si le package populaire est trop court
|
|
340
360
|
if (popular.length < MIN_PACKAGE_LENGTH) continue;
|
|
341
361
|
|