muaddib-scanner 2.10.21 → 2.10.22

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.
@@ -698,6 +698,48 @@ const PLAYBOOKS = {
698
698
  'dans ~/.config/sysmon/ et exfiltre vers checkmarx.zone. ' +
699
699
  'Verifier: find $(python -c "import site; print(site.getsitepackages()[0])") -name "*.pth" -exec cat {} \\; ' +
700
700
  'Supprimer tout fichier .pth non standard. Rotation des credentials.',
701
+
702
+ // Audit v3 Bypass Playbooks (AST-062 to AST-069)
703
+ reflect_apply_require:
704
+ 'CRITIQUE: Reflect.apply(require, null, [module]) detecte — contournement de require() via Reflect API. ' +
705
+ 'Aucun package legitime ne charge des modules via Reflect.apply. ' +
706
+ 'Supprimer le package. Auditer les modules charges dynamiquement.',
707
+
708
+ finalization_registry_exec:
709
+ 'CRITIQUE: FinalizationRegistry avec callback dangereux (child_process/exec/spawn). ' +
710
+ 'Le callback s\'execute apres le garbage collection, hors du flux synchrone — evasion sandbox. ' +
711
+ 'Aucun usage legitime ne combine FinalizationRegistry avec des appels systeme. Supprimer le package.',
712
+
713
+ function_prototype_constructor:
714
+ 'CRITIQUE: Acces au constructeur Function via chaine de prototypes — (function(){}).constructor(code) ' +
715
+ 'ou [].constructor.constructor(code). Equivalent a eval() mais invisible aux detections statiques de eval/Function. ' +
716
+ 'Supprimer le package. Aucun usage legitime.',
717
+
718
+ prototype_pollution:
719
+ 'HAUTE: Pollution de prototype detectee (__proto__, __defineGetter__, __defineSetter__). ' +
720
+ 'Peut detourner les proprietes heritees de tous les objets du runtime. ' +
721
+ 'Verifier si le code modifie Object.prototype ou des prototypes de classes natives. ' +
722
+ 'Si combine avec du code d\'execution dynamique, traiter comme CRITIQUE.',
723
+
724
+ module_wrap_override:
725
+ 'CRITIQUE: Module.wrap remplace — la fonction wrapper du module loader est detournee. ' +
726
+ 'Tout module charge apres cette modification execute du code injecte. ' +
727
+ 'Aucun package legitime ne remplace Module.wrap. Supprimer immediatement.',
728
+
729
+ symbol_property_hiding:
730
+ 'HAUTE: Module dangereux cache derriere une propriete Symbol. ' +
731
+ 'Invisible a Object.keys(), JSON.stringify() et for...in. Technique anti-forensics. ' +
732
+ 'Auditer toutes les proprietes Symbol du module. Supprimer si combine avec child_process/fs.',
733
+
734
+ with_body_dangerous:
735
+ 'HAUTE: with() statement avec require/exec/spawn dans le body. ' +
736
+ 'Le with() rend les identifiants ambigus, empechant l\'analyse statique. ' +
737
+ 'Aucun code moderne legitime n\'utilise with(). Supprimer le package.',
738
+
739
+ require_process_mainmodule:
740
+ 'CRITIQUE: require("process").mainModule.require() detecte — contournement de la detection ' +
741
+ 'process.mainModule.require() via require("process") au lieu de l\'objet global. ' +
742
+ 'Aucun package legitime n\'utilise ce pattern. Supprimer immediatement.',
701
743
  };
702
744
 
703
745
  function getPlaybook(threatType) {
@@ -1523,7 +1523,7 @@ const RULES = {
1523
1523
  id: 'MUADDIB-SHELL-019',
1524
1524
  name: 'Python Time Delay Execution',
1525
1525
  severity: 'HIGH',
1526
- confidence: 0.80,
1526
+ confidence: 'medium',
1527
1527
  description: 'Execution Python avec delai time.sleep() >= 100s via child process. Technique d\'evasion sandbox (T1497.003) : le malware attend que la sandbox expire avant d\'executer le payload.',
1528
1528
  references: ['https://attack.mitre.org/techniques/T1497/003/'],
1529
1529
  mitre: 'T1497.003'
@@ -1850,6 +1850,104 @@ const RULES = {
1850
1850
  ],
1851
1851
  mitre: 'T1071'
1852
1852
  },
1853
+
1854
+ // Audit v3 Bypass Detections (AST-062 to AST-069)
1855
+ reflect_apply_require: {
1856
+ id: 'MUADDIB-AST-062',
1857
+ name: 'Reflect.apply(require) Bypass',
1858
+ severity: 'CRITICAL',
1859
+ confidence: 'high',
1860
+ description: 'Reflect.apply(require, null, [module]) detecte — contourne la detection statique de require() en passant par l\'API Reflect. Permet de charger child_process/fs/net sans appel require() direct.',
1861
+ references: [
1862
+ 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/apply',
1863
+ 'https://attack.mitre.org/techniques/T1059/'
1864
+ ],
1865
+ mitre: 'T1059'
1866
+ },
1867
+ finalization_registry_exec: {
1868
+ id: 'MUADDIB-AST-063',
1869
+ name: 'FinalizationRegistry Deferred Execution',
1870
+ severity: 'CRITICAL',
1871
+ confidence: 'high',
1872
+ description: 'new FinalizationRegistry() avec callback contenant child_process/exec/spawn. Le callback s\'execute apres le garbage collection, hors du flux d\'execution normal — technique d\'evasion sandbox qui differe l\'execution malveillante.',
1873
+ references: [
1874
+ 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry',
1875
+ 'https://attack.mitre.org/techniques/T1497/003/'
1876
+ ],
1877
+ mitre: 'T1497.003'
1878
+ },
1879
+ function_prototype_constructor: {
1880
+ id: 'MUADDIB-AST-064',
1881
+ name: 'Function via Prototype Chain',
1882
+ severity: 'CRITICAL',
1883
+ confidence: 'high',
1884
+ description: '(function(){}).constructor(code) ou [].constructor.constructor(code) detecte — acces au constructeur Function via la chaine de prototypes, contourne les detections de new Function() et eval().',
1885
+ references: [
1886
+ 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function',
1887
+ 'https://attack.mitre.org/techniques/T1059/'
1888
+ ],
1889
+ mitre: 'T1059'
1890
+ },
1891
+ prototype_pollution: {
1892
+ id: 'MUADDIB-AST-065',
1893
+ name: 'Prototype Pollution',
1894
+ severity: 'HIGH',
1895
+ confidence: 'high',
1896
+ description: '__defineGetter__, __defineSetter__ ou assignation __proto__ detectee — pollution de prototype permettant de detourner les proprietes heritees de tous les objets. Vecteur d\'escalade pour injecter du code dans des chemins d\'execution inattendus.',
1897
+ references: [
1898
+ 'https://portswigger.net/web-security/prototype-pollution',
1899
+ 'https://attack.mitre.org/techniques/T1574/'
1900
+ ],
1901
+ mitre: 'T1574'
1902
+ },
1903
+ module_wrap_override: {
1904
+ id: 'MUADDIB-AST-066',
1905
+ name: 'Module.wrap Override',
1906
+ severity: 'CRITICAL',
1907
+ confidence: 'high',
1908
+ description: 'Module.wrap = ... detecte — remplacement de la fonction wrapper du module loader Node.js. Permet d\'injecter du code dans CHAQUE module charge apres le remplacement, technique de persistence systemique.',
1909
+ references: [
1910
+ 'https://nodejs.org/api/modules.html',
1911
+ 'https://attack.mitre.org/techniques/T1574/006/'
1912
+ ],
1913
+ mitre: 'T1574.006'
1914
+ },
1915
+ symbol_property_hiding: {
1916
+ id: 'MUADDIB-AST-067',
1917
+ name: 'Symbol Property Hiding',
1918
+ severity: 'HIGH',
1919
+ confidence: 'high',
1920
+ description: 'obj[Symbol(...)] = require(module_dangereux) detecte — dissimulation de modules dangereux derriere des proprietes Symbol, invisibles a Object.keys() et JSON.stringify(). Technique anti-forensics.',
1921
+ references: [
1922
+ 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol',
1923
+ 'https://attack.mitre.org/techniques/T1564/'
1924
+ ],
1925
+ mitre: 'T1564'
1926
+ },
1927
+ with_body_dangerous: {
1928
+ id: 'MUADDIB-AST-068',
1929
+ name: 'WithStatement Dangerous Body',
1930
+ severity: 'HIGH',
1931
+ confidence: 'high',
1932
+ description: 'with() statement dont le body contient require/exec/spawn/child_process — injection de scope pour obscurcir les appels dangereux. Le with() rend tous les identifiants ambigus, empechant l\'analyse statique de tracer les appels.',
1933
+ references: [
1934
+ 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with',
1935
+ 'https://attack.mitre.org/techniques/T1027/'
1936
+ ],
1937
+ mitre: 'T1027'
1938
+ },
1939
+ require_process_mainmodule: {
1940
+ id: 'MUADDIB-AST-069',
1941
+ name: 'require("process").mainModule Bypass',
1942
+ severity: 'CRITICAL',
1943
+ confidence: 'high',
1944
+ description: 'require("process").mainModule.require() detecte — acces indirect au mainModule via require("process") au lieu de l\'objet global process. Contourne la detection de process.mainModule.require() qui ne surveille que l\'identifiant "process".',
1945
+ references: [
1946
+ 'https://nodejs.org/api/process.html',
1947
+ 'https://attack.mitre.org/techniques/T1059/'
1948
+ ],
1949
+ mitre: 'T1059'
1950
+ },
1853
1951
  };
1854
1952
 
1855
1953
  function getRule(type) {
@@ -1912,4 +2010,23 @@ for (const [, rule] of Object.entries(PARANOID_RULES)) {
1912
2010
  PARANOID_RULES_BY_ID[rule.id] = rule;
1913
2011
  }
1914
2012
 
2013
+ // Validate all rules at load time
2014
+ const VALID_SEVERITIES = new Set(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']);
2015
+ const VALID_CONFIDENCES = new Set(['high', 'medium', 'low']);
2016
+
2017
+ for (const [key, rule] of Object.entries(RULES)) {
2018
+ if (!VALID_SEVERITIES.has(rule.severity)) {
2019
+ throw new Error(`Rule "${key}" has invalid severity: ${JSON.stringify(rule.severity)} (expected CRITICAL|HIGH|MEDIUM|LOW)`);
2020
+ }
2021
+ if (!VALID_CONFIDENCES.has(rule.confidence)) {
2022
+ throw new Error(`Rule "${key}" has invalid confidence: ${JSON.stringify(rule.confidence)} (expected high|medium|low)`);
2023
+ }
2024
+ }
2025
+ // PARANOID_RULES use a different schema (patterns/message, no confidence field)
2026
+ for (const [key, rule] of Object.entries(PARANOID_RULES)) {
2027
+ if (!VALID_SEVERITIES.has(rule.severity)) {
2028
+ throw new Error(`Paranoid rule "${key}" has invalid severity: ${JSON.stringify(rule.severity)} (expected CRITICAL|HIGH|MEDIUM|LOW)`);
2029
+ }
2030
+ }
2031
+
1915
2032
  module.exports = { RULES, getRule, PARANOID_RULES };
@@ -921,6 +921,28 @@ function handleCallExpression(node, ctx) {
921
921
  }
922
922
  }
923
923
 
924
+ // B8: require('process').mainModule.require('child_process') — indirect process access
925
+ // Pattern: require('process').mainModule.require(mod)
926
+ if (node.callee.type === 'MemberExpression' &&
927
+ node.callee.property?.type === 'Identifier' && node.callee.property.name === 'require' &&
928
+ node.callee.object?.type === 'MemberExpression' &&
929
+ node.callee.object.property?.type === 'Identifier' && node.callee.object.property.name === 'mainModule' &&
930
+ node.callee.object.object?.type === 'CallExpression' &&
931
+ getCallName(node.callee.object.object) === 'require' &&
932
+ node.callee.object.object.arguments?.[0]?.type === 'Literal' &&
933
+ node.callee.object.object.arguments[0].value === 'process') {
934
+ const arg = node.arguments?.[0];
935
+ const modName = arg ? extractStringValueDeep(arg) : null;
936
+ const DANGEROUS_MODS = ['child_process', 'fs', 'net', 'dns', 'http', 'https', 'tls'];
937
+ const severity = modName && DANGEROUS_MODS.includes(modName) ? 'CRITICAL' : 'HIGH';
938
+ ctx.threats.push({
939
+ type: 'require_process_mainmodule',
940
+ severity,
941
+ message: `require('process').mainModule.require(${modName ? "'" + modName + "'" : '...'}) — indirect process access bypasses direct process.mainModule detection.`,
942
+ file: ctx.relFile
943
+ });
944
+ }
945
+
924
946
  // Detect exec/execSync with dangerous shell commands (direct or via MemberExpression)
925
947
  const execName = callName === 'exec' || callName === 'execSync' ? callName : null;
926
948
  const memberExec = !execName && node.callee.type === 'MemberExpression' &&
@@ -2042,6 +2064,22 @@ function handleCallExpression(node, ctx) {
2042
2064
  });
2043
2065
  }
2044
2066
  }
2067
+
2068
+ // B3: (function(){}).constructor('code')() — direct prototype chain Function access
2069
+ // Also: [].constructor.constructor, ''.constructor.constructor, (0).constructor.constructor
2070
+ if (obj?.type === 'FunctionExpression' || obj?.type === 'ArrowFunctionExpression' ||
2071
+ obj?.type === 'ArrayExpression' || obj?.type === 'Literal') {
2072
+ if (!hasOnlyStringLiteralArgs(node)) {
2073
+ ctx.hasEvalInFile = true;
2074
+ ctx.hasDynamicExec = true;
2075
+ ctx.threats.push({
2076
+ type: 'function_prototype_constructor',
2077
+ severity: 'CRITICAL',
2078
+ message: `Function constructor via prototype chain: (${obj.type === 'FunctionExpression' ? 'function(){}' : obj.type === 'ArrayExpression' ? '[]' : 'literal'}).constructor(code) — bypasses Function/eval detection.`,
2079
+ file: ctx.relFile
2080
+ });
2081
+ }
2082
+ }
2045
2083
  }
2046
2084
 
2047
2085
  // SANDWORM_MODE: Track writeFileSync/writeFile to temp paths
@@ -2164,9 +2202,37 @@ function handleCallExpression(node, ctx) {
2164
2202
  file: ctx.relFile
2165
2203
  });
2166
2204
  }
2205
+ // B1: Reflect.apply(require, null, ['child_process']) — bypasses require() call detection
2206
+ if (target.type === 'Identifier' && target.name === 'require') {
2207
+ const argsArray = node.arguments[2];
2208
+ let modName = null;
2209
+ if (argsArray?.type === 'ArrayExpression' && argsArray.elements.length > 0) {
2210
+ modName = extractStringValueDeep(argsArray.elements[0]);
2211
+ }
2212
+ const severity = modName && ['child_process', 'fs', 'net', 'dns', 'http', 'https'].includes(modName)
2213
+ ? 'CRITICAL' : 'HIGH';
2214
+ ctx.threats.push({
2215
+ type: 'reflect_apply_require',
2216
+ severity,
2217
+ message: `Reflect.apply(require, null, [${modName ? "'" + modName + "'" : '...'}]) — indirect require() bypasses static call detection.`,
2218
+ file: ctx.relFile
2219
+ });
2220
+ }
2167
2221
  }
2168
2222
  }
2169
2223
 
2224
+ // B4: __defineGetter__ / __defineSetter__ — prototype pollution via legacy API
2225
+ if (node.callee.type === 'MemberExpression' &&
2226
+ node.callee.property?.type === 'Identifier' &&
2227
+ (node.callee.property.name === '__defineGetter__' || node.callee.property.name === '__defineSetter__')) {
2228
+ ctx.threats.push({
2229
+ type: 'prototype_pollution',
2230
+ severity: 'HIGH',
2231
+ message: `${node.callee.property.name}() called — legacy prototype pollution API can hijack property access on any object.`,
2232
+ file: ctx.relFile
2233
+ });
2234
+ }
2235
+
2170
2236
  // Batch 1: process.binding('spawn_sync'/'fs') / process._linkedBinding(...)
2171
2237
  if (node.callee.type === 'MemberExpression' &&
2172
2238
  node.callee.object?.type === 'Identifier' && node.callee.object.name === 'process' &&
@@ -2334,6 +2400,37 @@ function handleNewExpression(node, ctx) {
2334
2400
  }
2335
2401
  }
2336
2402
  }
2403
+
2404
+ // B2: new FinalizationRegistry(callback) — deferred execution after GC
2405
+ // Malicious pattern: callback contains require('child_process') or exec/spawn
2406
+ if (node.callee.type === 'Identifier' && node.callee.name === 'FinalizationRegistry' &&
2407
+ node.arguments.length >= 1) {
2408
+ const callback = node.arguments[0];
2409
+ if (callback) {
2410
+ // Check if callback body contains dangerous patterns
2411
+ let hasDangerousBody = false;
2412
+ const cbSource = callback.start !== undefined && callback.end !== undefined
2413
+ ? ctx._sourceCode?.slice(callback.start, callback.end) : null;
2414
+ if (cbSource && /\b(child_process|exec|execSync|spawn|spawnSync)\b/.test(cbSource)) {
2415
+ hasDangerousBody = true;
2416
+ }
2417
+ // Also flag if the callback is a variable known to be dangerous
2418
+ if (callback.type === 'Identifier' && ctx.evalAliases?.has(callback.name)) {
2419
+ hasDangerousBody = true;
2420
+ }
2421
+ if (hasDangerousBody) {
2422
+ ctx.hasDynamicExec = true;
2423
+ ctx.threats.push({
2424
+ type: 'finalization_registry_exec',
2425
+ severity: 'CRITICAL',
2426
+ message: 'new FinalizationRegistry() with dangerous callback — deferred code execution triggered by garbage collection, evades synchronous analysis.',
2427
+ file: ctx.relFile
2428
+ });
2429
+ } else {
2430
+ ctx.hasFinalizationRegistry = true;
2431
+ }
2432
+ }
2433
+ }
2337
2434
  }
2338
2435
 
2339
2436
  function handleLiteral(node, ctx) {
@@ -2463,6 +2560,55 @@ function handleAssignmentExpression(node, ctx) {
2463
2560
  }
2464
2561
  }
2465
2562
 
2563
+ // B6: Symbol property hiding — obj[Symbol(...)] = require('child_process')
2564
+ if (node.left?.type === 'MemberExpression' && node.left.computed &&
2565
+ node.left.property?.type === 'CallExpression' &&
2566
+ node.left.property.callee?.type === 'Identifier' && node.left.property.callee.name === 'Symbol') {
2567
+ // Check if the right side is require('child_process') or similar dangerous module
2568
+ let isDangerous = false;
2569
+ let modName = null;
2570
+ if (node.right?.type === 'CallExpression' && getCallName(node.right) === 'require' &&
2571
+ node.right.arguments?.[0]?.type === 'Literal') {
2572
+ const rawMod = node.right.arguments[0].value;
2573
+ modName = typeof rawMod === 'string' && rawMod.startsWith('node:') ? rawMod.slice(5) : rawMod;
2574
+ if (['child_process', 'fs', 'net', 'dns', 'http', 'https'].includes(modName)) {
2575
+ isDangerous = true;
2576
+ }
2577
+ }
2578
+ // Also detect: obj[Symbol('x')] = eval / Function / exec
2579
+ if (node.right?.type === 'Identifier' && ['eval', 'Function'].includes(node.right.name)) {
2580
+ isDangerous = true;
2581
+ }
2582
+ if (isDangerous) {
2583
+ ctx.threats.push({
2584
+ type: 'symbol_property_hiding',
2585
+ severity: 'HIGH',
2586
+ message: `Dangerous module/function hidden behind Symbol property — obj[Symbol(...)] = ${modName ? "require('" + modName + "')" : node.right?.name || '...'}, evades string-based property enumeration.`,
2587
+ file: ctx.relFile
2588
+ });
2589
+ }
2590
+ }
2591
+
2592
+ // B5: Module.wrap = ... or require('module').wrap = ... — module wrapper override
2593
+ if (node.left?.type === 'MemberExpression' &&
2594
+ node.left.property?.type === 'Identifier' && node.left.property.name === 'wrap') {
2595
+ const obj = node.left.object;
2596
+ // Direct: Module.wrap = ... (where Module was imported via require('module'))
2597
+ const isModuleObj = (obj?.type === 'Identifier' && ctx.moduleAliases?.has(obj.name)) ||
2598
+ (obj?.type === 'Identifier' && obj.name === 'Module');
2599
+ // Inline: require('module').wrap = ...
2600
+ const isInlineRequire = obj?.type === 'CallExpression' && getCallName(obj) === 'require' &&
2601
+ obj.arguments?.[0]?.type === 'Literal' && obj.arguments[0].value === 'module';
2602
+ if (isModuleObj || isInlineRequire) {
2603
+ ctx.threats.push({
2604
+ type: 'module_wrap_override',
2605
+ severity: 'CRITICAL',
2606
+ message: 'Module.wrap overridden — module wrapper function hijacked, allows injecting code into every loaded module.',
2607
+ file: ctx.relFile
2608
+ });
2609
+ }
2610
+ }
2611
+
2466
2612
  // Detect object property indirection: obj.exec = require('child_process').exec
2467
2613
  // or obj.fn = eval — stashing dangerous functions in object properties
2468
2614
  if (node.left?.type === 'MemberExpression' && node.right) {
@@ -2517,6 +2663,17 @@ function handleAssignmentExpression(node, ctx) {
2517
2663
  }
2518
2664
  }
2519
2665
 
2666
+ // B4: Prototype pollution — __proto__ assignment
2667
+ if (node.left?.type === 'MemberExpression' && !node.left.computed &&
2668
+ node.left.property?.type === 'Identifier' && node.left.property.name === '__proto__') {
2669
+ ctx.threats.push({
2670
+ type: 'prototype_pollution',
2671
+ severity: 'HIGH',
2672
+ message: `__proto__ assignment on ${node.left.object?.name || 'object'} — prototype pollution can hijack inherited properties across all objects.`,
2673
+ file: ctx.relFile
2674
+ });
2675
+ }
2676
+
2520
2677
  if (node.left?.type === 'MemberExpression') {
2521
2678
  const left = node.left;
2522
2679
 
@@ -3021,6 +3178,16 @@ function handlePostWalk(ctx) {
3021
3178
  file: ctx.relFile
3022
3179
  });
3023
3180
  }
3181
+
3182
+ // B2 compound: FinalizationRegistry + exec/network in same file = deferred malicious execution
3183
+ if (ctx.hasFinalizationRegistry && ctx.hasDynamicExec) {
3184
+ ctx.threats.push({
3185
+ type: 'finalization_registry_exec',
3186
+ severity: 'CRITICAL',
3187
+ message: 'FinalizationRegistry + dynamic execution in same file — deferred code execution triggered by garbage collection.',
3188
+ file: ctx.relFile
3189
+ });
3190
+ }
3024
3191
  }
3025
3192
 
3026
3193
  function handleWithStatement(node, ctx) {
@@ -3049,6 +3216,22 @@ function handleWithStatement(node, ctx) {
3049
3216
  file: ctx.relFile
3050
3217
  });
3051
3218
  }
3219
+ return; // Already handled as direct with(require(...))
3220
+ }
3221
+
3222
+ // B7: with(obj) { ... require('child_process') ... } — body contains dangerous require/exec
3223
+ // The with statement itself is rare in modern code; combined with dangerous APIs in body = evasion
3224
+ if (node.body) {
3225
+ const bodySource = node.body.start !== undefined && node.body.end !== undefined
3226
+ ? ctx._sourceCode?.slice(node.body.start, node.body.end) : null;
3227
+ if (bodySource && /\b(require\s*\(\s*['"]child_process['"]\s*\)|child_process|exec\s*\(|execSync\s*\(|spawn\s*\()/.test(bodySource)) {
3228
+ ctx.threats.push({
3229
+ type: 'with_body_dangerous',
3230
+ severity: 'HIGH',
3231
+ message: 'with() statement body contains require/exec/spawn — scope injection used to obscure dangerous API calls.',
3232
+ file: ctx.relFile
3233
+ });
3234
+ }
3052
3235
  }
3053
3236
  }
3054
3237
 
@@ -180,7 +180,11 @@ function analyzeFile(content, filePath, basePath) {
180
180
  hasSolanaImport: false,
181
181
  hasSolanaC2Method: false,
182
182
  // Audit v3: uncaughtException/unhandledRejection handler for error hijacking detection
183
- hasUncaughtExceptionHandler: false
183
+ hasUncaughtExceptionHandler: false,
184
+ // Audit v3 B2: FinalizationRegistry deferred exec detection
185
+ hasFinalizationRegistry: false,
186
+ // Audit v3: source code reference for callback body analysis
187
+ _sourceCode: content
184
188
  };
185
189
 
186
190
  // Compute fetchOnlySafeDomains: check if ALL URLs in file point to known registries
@@ -6,15 +6,15 @@ const { MAX_FILE_SIZE, getMaxFileSize } = require('../shared/constants.js');
6
6
  const SHELL_EXCLUDED_DIRS = ['node_modules', '.git', '.muaddib-cache'];
7
7
 
8
8
  const MALICIOUS_PATTERNS = [
9
- { pattern: /curl.*\|.*sh/m, name: 'curl_pipe_shell', severity: 'HIGH' },
10
- { pattern: /wget.*&&.*chmod.*\+x/m, name: 'wget_chmod_exec', severity: 'HIGH' },
9
+ { pattern: /curl[^\n]{0,5000}\|[^\n]{0,5000}sh/m, name: 'curl_pipe_shell', severity: 'HIGH' },
10
+ { pattern: /wget[^\n]{0,5000}&&[^\n]{0,5000}chmod[^\n]{0,5000}\+x/m, name: 'wget_chmod_exec', severity: 'HIGH' },
11
11
  { pattern: /bash\s+-i\s+>&\s+\/dev\/tcp/m, name: 'reverse_shell', severity: 'CRITICAL' },
12
12
  { pattern: /nc\s+-e\s+\/bin\/(ba)?sh/m, name: 'netcat_shell', severity: 'CRITICAL' },
13
13
  { pattern: /rm\s+-rf\s+(~\/|\$HOME|\/home)/m, name: 'home_deletion', severity: 'CRITICAL' },
14
14
  { pattern: /shred.*\$HOME/m, name: 'shred_home', severity: 'CRITICAL' },
15
- { pattern: /curl.*-X\s*POST.*-d/m, name: 'curl_exfiltration', severity: 'HIGH' },
16
- { pattern: /(?:cat|readFile|cp|mv|curl\s+file:\/\/|tar\s+.*|scp\s+).*\.npmrc/m, name: 'npmrc_access', severity: 'HIGH' },
17
- { pattern: /(?:cat|readFile|cp|mv|curl\s+file:\/\/|tar\s+.*|scp\s+).*\.ssh/m, name: 'ssh_access', severity: 'HIGH' },
15
+ { pattern: /curl[^\n]{0,5000}-X\s*POST[^\n]{0,5000}-d/m, name: 'curl_exfiltration', severity: 'HIGH' },
16
+ { pattern: /(?:cat|readFile|cp|mv|curl\s+file:\/\/|tar\s+[^\n]{0,5000}|scp\s+)[^\n]{0,5000}\.npmrc/m, name: 'npmrc_access', severity: 'HIGH' },
17
+ { pattern: /(?:cat|readFile|cp|mv|curl\s+file:\/\/|tar\s+[^\n]{0,5000}|scp\s+)[^\n]{0,5000}\.ssh/m, name: 'ssh_access', severity: 'HIGH' },
18
18
  { pattern: /python\s+-c.*import\s+socket/m, name: 'python_reverse_shell', severity: 'CRITICAL' },
19
19
  { pattern: /perl\s+-e.*socket/m, name: 'perl_reverse_shell', severity: 'CRITICAL' },
20
20
  { pattern: /mkfifo.*\/dev\/tcp/m, name: 'fifo_reverse_shell', severity: 'CRITICAL' },
package/src/scoring.js CHANGED
@@ -102,8 +102,8 @@ const PACKAGE_LEVEL_TYPES = new Set([
102
102
  'shai_hulud_marker', 'suspicious_file',
103
103
  'pypi_malicious_package', 'pypi_typosquat_detected',
104
104
  'dangerous_api_added_critical', 'dangerous_api_added_high', 'dangerous_api_added_medium',
105
- 'publish_burst', 'publish_dormant_spike', 'publish_rapid_succession',
106
- 'maintainer_new_suspicious', 'maintainer_sole_change',
105
+ 'publish_burst', 'dormant_spike', 'rapid_succession',
106
+ 'suspicious_maintainer', 'sole_maintainer_change',
107
107
  'sandbox_network_activity', 'sandbox_file_changes', 'sandbox_process_spawns',
108
108
  'sandbox_canary_exfiltration',
109
109
  // Compound scoring rules — package-level co-occurrences
@@ -99,13 +99,13 @@ function resetMaxFileSize() { _maxFileSize = MAX_FILE_SIZE; }
99
99
  const ACORN_OPTIONS = { ecmaVersion: 2024, sourceType: 'module', allowHashBang: true };
100
100
 
101
101
  const acorn = require('acorn');
102
+ const crypto = require('crypto');
102
103
 
103
104
  /**
104
105
  * AST parse cache — same content+options returns the same AST.
105
106
  * Scanners do not mutate AST nodes (verified: only read comparisons).
106
107
  * Cleared between scans via clearASTCache().
107
- * Key = code.length + '|' + optionsKey + '|' + code.slice(0,128) + code.slice(-64)
108
- * (length-prefixed partial key for fast Map lookup; collisions resolved by full WeakRef check)
108
+ * Key = sha256(code) + '|' + optionsKey (collision-free content-addressable key)
109
109
  */
110
110
  const _astCache = new Map();
111
111
  const _AST_CACHE_MAX = 600; // Max entries (one scan ≈ 500 files max)
@@ -117,9 +117,9 @@ const _AST_CACHE_MAX = 600; // Max entries (one scan ≈ 500 files max)
117
117
  * Returns AST or null if both modes fail.
118
118
  */
119
119
  function safeParse(code, extraOptions = {}) {
120
- // Build cache key: options signature + content fingerprint
120
+ // Build cache key: sha256 content hash + options signature
121
121
  const optKey = Object.keys(extraOptions).length === 0 ? '' : JSON.stringify(extraOptions);
122
- const cacheKey = code.length + '|' + optKey + '|' + code.slice(0, 128) + code.slice(-64);
122
+ const cacheKey = crypto.createHash('sha256').update(code).digest('hex') + '|' + optKey;
123
123
 
124
124
  const cached = _astCache.get(cacheKey);
125
125
  if (cached !== undefined) return cached;
package/src/utils.js CHANGED
@@ -23,6 +23,7 @@ let _scanRoot = '';
23
23
  */
24
24
  const _fileListCache = new Map();
25
25
  let _filesCapped = false;
26
+ let _overflowFiles = [];
26
27
 
27
28
  /**
28
29
  * File content cache — read each file once, reused across all scanners in a single scan.
@@ -121,6 +122,7 @@ function findFiles(dir, options = {}) {
121
122
  return depthA - depthB;
122
123
  });
123
124
  const capped = result.slice(0, maxFiles);
125
+ _overflowFiles = result.slice(maxFiles);
124
126
  _fileListCache.set(cacheKey, [...capped]);
125
127
  _filesCapped = true;
126
128
  return capped;
@@ -227,12 +229,17 @@ function clearFileListCache() {
227
229
  _fileContentCache.clear();
228
230
  clearASTCache();
229
231
  _filesCapped = false;
232
+ _overflowFiles = [];
230
233
  }
231
234
 
232
235
  function wasFilesCapped() {
233
236
  return _filesCapped;
234
237
  }
235
238
 
239
+ function getOverflowFiles() {
240
+ return _overflowFiles;
241
+ }
242
+
236
243
  /**
237
244
  * Escapes HTML characters to prevent XSS
238
245
  * @param {string} str - String to escape
@@ -394,6 +401,7 @@ module.exports = {
394
401
  findJsFiles,
395
402
  clearFileListCache,
396
403
  wasFilesCapped,
404
+ getOverflowFiles,
397
405
  escapeHtml,
398
406
  getCallName,
399
407
  Spinner,