muaddib-scanner 2.9.1 → 2.9.2

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.9.1",
3
+ "version": "2.9.2",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -28,7 +28,7 @@ const { computeReachableFiles } = require('./scanner/reachability.js');
28
28
  const { runTemporalAnalyses } = require('./temporal-runner.js');
29
29
  const { formatOutput } = require('./output-formatter.js');
30
30
  const { setExtraExcludes, getExtraExcludes, Spinner, listInstalledPackages, clearFileListCache, debugLog } = require('./utils.js');
31
- const { SEVERITY_WEIGHTS, RISK_THRESHOLDS, MAX_RISK_SCORE, isPackageLevelThreat, computeGroupScore, applyFPReductions, calculateRiskScore } = require('./scoring.js');
31
+ const { SEVERITY_WEIGHTS, RISK_THRESHOLDS, MAX_RISK_SCORE, isPackageLevelThreat, computeGroupScore, applyFPReductions, applyCompoundBoosts, calculateRiskScore } = require('./scoring.js');
32
32
  const { buildIntentPairs } = require('./intent-graph.js');
33
33
 
34
34
  const { MAX_FILE_SIZE, safeParse } = require('./shared/constants.js');
@@ -598,6 +598,11 @@ async function run(targetPath, options = {}) {
598
598
  // A malware package typically has 1-3 occurrences, not dozens.
599
599
  applyFPReductions(deduped, reachableFiles, packageName, packageDeps);
600
600
 
601
+ // Compound scoring: inject synthetic CRITICAL threats when co-occurring types
602
+ // indicate unambiguous malice. Applied AFTER FP reductions to recover signals
603
+ // that were individually downgraded (count-based, dist, reachability).
604
+ applyCompoundBoosts(deduped);
605
+
601
606
  // Intent coherence analysis: detect source→sink pairs within files
602
607
  // Pass targetPath for destination-aware SDK pattern detection
603
608
  const intentResult = buildIntentPairs(deduped, targetPath);
@@ -537,6 +537,35 @@ const PLAYBOOKS = {
537
537
  'Dans un package non-crypto, cela indique un potentiel canal C2 via blockchain. ' +
538
538
  'Verifier le contexte: si le package n\'a rien a voir avec la blockchain, supprimer immediatement.',
539
539
 
540
+ crypto_staged_payload:
541
+ 'CRITIQUE: Chaine steganographique complete detectee — fichier binaire (.png/.jpg/.wasm) avec eval() + dechiffrement crypto. ' +
542
+ 'Le payload malveillant est cache dans un fichier binaire et dechiffre a runtime. Supprimer le package immediatement. ' +
543
+ 'Analyser le fichier binaire dans un sandbox pour extraire le payload.',
544
+
545
+ lifecycle_typosquat:
546
+ 'CRITIQUE: Package avec nom similaire a un package populaire ET scripts lifecycle. ' +
547
+ 'Vecteur classique de dependency confusion: le code s\'execute a l\'installation. ' +
548
+ 'NE PAS installer. Verifier le nom exact du package. Signaler sur npm.',
549
+
550
+ credential_env_exfil:
551
+ 'CRITIQUE: Ecriture dans des chemins sensibles (cache npm/yarn, credentials) + acces aux variables d\'environnement. ' +
552
+ 'Double vecteur d\'exfiltration de credentials. Supprimer le package. Regenerer tous les secrets. ' +
553
+ 'Nettoyer le cache: npm cache clean --force.',
554
+
555
+ lifecycle_inline_exec:
556
+ 'CRITIQUE: Script lifecycle avec node -e (execution inline). Le code s\'execute automatiquement a npm install. ' +
557
+ 'NE PAS installer. Si deja installe: considerer la machine compromise. ' +
558
+ 'Auditer les modifications systeme recentes.',
559
+
560
+ lifecycle_remote_require:
561
+ 'CRITIQUE: Script lifecycle avec require(http/https) pour charger du code distant. ' +
562
+ 'Le payload est telecharge et execute automatiquement a l\'installation. ' +
563
+ 'NE PAS installer. Bloquer les connexions sortantes. Supprimer le package.',
564
+
565
+ obfuscated_credential_tampering:
566
+ 'CRITIQUE: Code obfusque + ecriture dans des chemins sensibles. Dissimulation de vol de credentials. ' +
567
+ 'Supprimer le package immediatement. Nettoyer le cache npm/yarn. Regenerer tous les secrets.',
568
+
540
569
  bin_field_hijack:
541
570
  'CRITIQUE: Le champ "bin" de package.json shadow une commande systeme (node, npm, git, bash, etc.). ' +
542
571
  'A l\'installation, npm cree un symlink dans node_modules/.bin/ qui intercepte la commande reelle. ' +
@@ -1594,6 +1594,81 @@ const RULES = {
1594
1594
  ],
1595
1595
  mitre: 'T1102'
1596
1596
  },
1597
+
1598
+ // Compound scoring rules (v2.9.2)
1599
+ // Injected by applyCompoundBoosts() when co-occurring threat types indicate unambiguous malice.
1600
+ crypto_staged_payload: {
1601
+ id: 'MUADDIB-COMPOUND-001',
1602
+ name: 'Steganographic Payload + Crypto Decryption',
1603
+ severity: 'CRITICAL',
1604
+ confidence: 'high',
1605
+ description: 'Reference a un fichier binaire (.png/.jpg/.wasm) avec eval() combinee avec dechiffrement crypto (createDecipher). Chaine steganographique complete: payload cache dans un fichier binaire, dechiffre a runtime.',
1606
+ references: [
1607
+ 'https://attack.mitre.org/techniques/T1140/',
1608
+ 'https://attack.mitre.org/techniques/T1027/003/'
1609
+ ],
1610
+ mitre: 'T1140'
1611
+ },
1612
+ lifecycle_typosquat: {
1613
+ id: 'MUADDIB-COMPOUND-002',
1614
+ name: 'Lifecycle Hook on Typosquat Package',
1615
+ severity: 'CRITICAL',
1616
+ confidence: 'high',
1617
+ description: 'Script lifecycle (preinstall/postinstall) sur un package avec nom similaire a un package populaire. Vecteur classique de dependency confusion: le code s\'execute automatiquement a l\'installation.',
1618
+ references: [
1619
+ 'https://attack.mitre.org/techniques/T1195/002/',
1620
+ 'https://snyk.io/blog/typosquatting-attacks/'
1621
+ ],
1622
+ mitre: 'T1195.002'
1623
+ },
1624
+ credential_env_exfil: {
1625
+ id: 'MUADDIB-COMPOUND-003',
1626
+ name: 'Credential Tampering + Env Access',
1627
+ severity: 'CRITICAL',
1628
+ confidence: 'high',
1629
+ description: 'Ecriture dans un chemin sensible (cache npm/yarn, credentials) combinee avec acces aux variables d\'environnement. Chaine d\'exfiltration de credentials par double vecteur.',
1630
+ references: [
1631
+ 'https://attack.mitre.org/techniques/T1552/001/',
1632
+ 'https://attack.mitre.org/techniques/T1565/001/'
1633
+ ],
1634
+ mitre: 'T1552.001'
1635
+ },
1636
+ lifecycle_inline_exec: {
1637
+ id: 'MUADDIB-COMPOUND-004',
1638
+ name: 'Lifecycle Hook + Inline Node Execution',
1639
+ severity: 'CRITICAL',
1640
+ confidence: 'high',
1641
+ description: 'Script lifecycle avec execution inline Node.js (node -e). Le code s\'execute automatiquement a npm install avec un payload inline.',
1642
+ references: [
1643
+ 'https://attack.mitre.org/techniques/T1059/007/',
1644
+ 'https://attack.mitre.org/techniques/T1195/002/'
1645
+ ],
1646
+ mitre: 'T1059.007'
1647
+ },
1648
+ lifecycle_remote_require: {
1649
+ id: 'MUADDIB-COMPOUND-005',
1650
+ name: 'Lifecycle Hook + Remote Code Loading',
1651
+ severity: 'CRITICAL',
1652
+ confidence: 'high',
1653
+ description: 'Script lifecycle avec require(http/https) pour charger du code distant. Le payload est telecharge et execute automatiquement a l\'installation.',
1654
+ references: [
1655
+ 'https://attack.mitre.org/techniques/T1105/',
1656
+ 'https://attack.mitre.org/techniques/T1195/002/'
1657
+ ],
1658
+ mitre: 'T1105'
1659
+ },
1660
+ obfuscated_credential_tampering: {
1661
+ id: 'MUADDIB-COMPOUND-006',
1662
+ name: 'Obfuscated Code + Credential Tampering',
1663
+ severity: 'CRITICAL',
1664
+ confidence: 'high',
1665
+ description: 'Code obfusque combine avec ecriture dans des chemins sensibles (cache npm/yarn, credentials). Dissimulation de vol de credentials.',
1666
+ references: [
1667
+ 'https://attack.mitre.org/techniques/T1027/',
1668
+ 'https://attack.mitre.org/techniques/T1565/001/'
1669
+ ],
1670
+ mitre: 'T1027'
1671
+ },
1597
1672
  };
1598
1673
 
1599
1674
  function getRule(type) {
package/src/scoring.js CHANGED
@@ -62,7 +62,9 @@ const PACKAGE_LEVEL_TYPES = new Set([
62
62
  'publish_burst', 'publish_dormant_spike', 'publish_rapid_succession',
63
63
  'maintainer_new_suspicious', 'maintainer_sole_change',
64
64
  'sandbox_network_activity', 'sandbox_file_changes', 'sandbox_process_spawns',
65
- 'sandbox_canary_exfiltration'
65
+ 'sandbox_canary_exfiltration',
66
+ // Compound scoring rules — package-level co-occurrences
67
+ 'lifecycle_typosquat', 'lifecycle_inline_exec', 'lifecycle_remote_require'
66
68
  ]);
67
69
 
68
70
  /**
@@ -156,7 +158,12 @@ const DIST_EXEMPT_TYPES = new Set([
156
158
  'reverse_shell', // net.Socket + connect + pipe (always malicious)
157
159
  'detached_credential_exfil', // detached process + credential exfil (DPRK/Lazarus)
158
160
  'node_modules_write', // writeFile to node_modules/ (worm propagation)
159
- 'npm_publish_worm' // exec("npm publish") (worm propagation)
161
+ 'npm_publish_worm', // exec("npm publish") (worm propagation)
162
+ // Dangerous shell commands in dist/ are real threats, never bundler output
163
+ 'dangerous_exec',
164
+ // Compound scoring rules — co-occurrence signals, never FP
165
+ 'crypto_staged_payload', 'lifecycle_typosquat', 'credential_env_exfil',
166
+ 'lifecycle_inline_exec', 'lifecycle_remote_require', 'obfuscated_credential_tampering'
160
167
  // P6: remote_code_load and proxy_data_intercept removed — in bundled dist/ files,
161
168
  // fetch + eval co-occurrence is coincidental (bundler combines HTTP client + template compilation).
162
169
  // fetch_decrypt_exec (fetch+decrypt+eval triple) remains exempt — never coincidental.
@@ -203,6 +210,93 @@ const REACHABILITY_EXEMPT_TYPES = new Set([
203
210
  'detached_credential_exfil' // DPRK/Lazarus: invoked via lifecycle, not require/import
204
211
  ]);
205
212
 
213
+ // ============================================
214
+ // COMPOUND SCORING RULES (v2.9.2)
215
+ // ============================================
216
+ // Co-occurrences of threat types that NEVER appear in benign packages.
217
+ // Applied AFTER FP reductions to recover signals that were individually downgraded.
218
+ // Each compound injects a new CRITICAL threat when all required types are present.
219
+ const SCORING_COMPOUNDS = [
220
+ {
221
+ type: 'crypto_staged_payload',
222
+ requires: ['staged_binary_payload', 'crypto_decipher'],
223
+ severity: 'CRITICAL',
224
+ message: 'Binary file reference + crypto decryption — steganographic payload chain (scoring compound).',
225
+ fileFrom: 'staged_binary_payload'
226
+ },
227
+ {
228
+ type: 'lifecycle_typosquat',
229
+ requires: ['lifecycle_script', 'typosquat_detected'],
230
+ severity: 'CRITICAL',
231
+ message: 'Lifecycle hook on typosquat package — dependency confusion attack vector (scoring compound).',
232
+ fileFrom: 'typosquat_detected'
233
+ },
234
+ {
235
+ type: 'credential_env_exfil',
236
+ requires: ['credential_tampering', 'env_access'],
237
+ severity: 'CRITICAL',
238
+ message: 'Credential path tampering + environment variable access — credential exfiltration chain (scoring compound).',
239
+ fileFrom: 'credential_tampering'
240
+ },
241
+ {
242
+ type: 'lifecycle_inline_exec',
243
+ requires: ['lifecycle_script', 'node_inline_exec'],
244
+ severity: 'CRITICAL',
245
+ message: 'Lifecycle hook with inline Node execution (node -e) — install-time code execution (scoring compound).',
246
+ fileFrom: 'node_inline_exec'
247
+ },
248
+ {
249
+ type: 'lifecycle_remote_require',
250
+ requires: ['lifecycle_script', 'network_require'],
251
+ severity: 'CRITICAL',
252
+ message: 'Lifecycle hook loading remote code (require http/https) — supply chain payload delivery (scoring compound).',
253
+ fileFrom: 'network_require'
254
+ },
255
+ {
256
+ type: 'obfuscated_credential_tampering',
257
+ requires: ['credential_tampering', 'obfuscation_detected'],
258
+ severity: 'CRITICAL',
259
+ message: 'Obfuscated code + credential path tampering — concealed credential theft (scoring compound).',
260
+ fileFrom: 'credential_tampering'
261
+ }
262
+ ];
263
+
264
+ /**
265
+ * Apply compound boost rules: inject synthetic CRITICAL threats when
266
+ * co-occurring threat types indicate unambiguous malice.
267
+ * Called AFTER applyFPReductions to recover individually-downgraded signals.
268
+ * @param {Array} threats - deduplicated threat array (mutated in place)
269
+ */
270
+ function applyCompoundBoosts(threats) {
271
+ const typeSet = new Set(threats.map(t => t.type));
272
+
273
+ // Build map of type → first file encountered (for file assignment)
274
+ const typeFileMap = Object.create(null);
275
+ for (const t of threats) {
276
+ if (!typeFileMap[t.type]) {
277
+ typeFileMap[t.type] = t.file || '(unknown)';
278
+ }
279
+ }
280
+
281
+ for (const compound of SCORING_COMPOUNDS) {
282
+ // Skip if compound already present (e.g. from a scanner)
283
+ if (typeSet.has(compound.type)) continue;
284
+
285
+ // Check all required types are present
286
+ if (compound.requires.every(req => typeSet.has(req))) {
287
+ threats.push({
288
+ type: compound.type,
289
+ severity: compound.severity,
290
+ message: compound.message,
291
+ file: typeFileMap[compound.fileFrom] || '(unknown)',
292
+ count: 1,
293
+ compound: true
294
+ });
295
+ typeSet.add(compound.type);
296
+ }
297
+ }
298
+ }
299
+
206
300
  // Custom class prototypes that HTTP frameworks legitimately extend.
207
301
  // Distinguished from dangerous core Node.js prototype hooks.
208
302
  const FRAMEWORK_PROTOTYPES = ['Request', 'Response', 'App', 'Router'];
@@ -463,5 +557,5 @@ function calculateRiskScore(deduped, intentResult) {
463
557
 
464
558
  module.exports = {
465
559
  SEVERITY_WEIGHTS, RISK_THRESHOLDS, MAX_RISK_SCORE, CONFIDENCE_FACTORS,
466
- isPackageLevelThreat, computeGroupScore, applyFPReductions, calculateRiskScore
560
+ isPackageLevelThreat, computeGroupScore, applyFPReductions, applyCompoundBoosts, calculateRiskScore
467
561
  };