muaddib-scanner 2.11.169 → 2.11.170

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.11.169",
3
+ "version": "2.11.170",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "node_modules",
3
- "timestamp": "2026-07-13T15:38:17.450Z",
3
+ "timestamp": "2026-07-15T11:41:29.684Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -164,6 +164,24 @@ function hasIOCMatch(result) {
164
164
  return result.threats.some(t => IOC_MATCH_TYPES.has(t.type));
165
165
  }
166
166
 
167
+ // Stealth-exec + obfuscation compound: a detached/silent background process
168
+ // (silent_stealth_process AST-092 / detached_process AST-012) shipped ALONGSIDE
169
+ // obfuscator.io-mangled code (js_obfuscation_pattern ENTROPY-003, the `_0x*` hex
170
+ // signature). The AND is quasi-never legitimate: legit process managers (pm2,
171
+ // Platformatic Watt, the mongodb driver) detach but do NOT obfuscate, and legit
172
+ // obfuscated bundles do NOT silently detach a process. It is the ATO / CI-supply-
173
+ // chain fingerprint (@asyncapi/specs Miasma 2026-07-14; @umudik/lotaru) whose raw
174
+ // score reputation attenuation crushes to LOW on an established package.
175
+ // Measured on the 128,429-scan production ledger (scripts/mine-fpr): exactly two
176
+ // packages match — BOTH confirmed malicious — so wiring this as a reputation-
177
+ // independent webhook bypass adds ZERO benign false positives (ΔFPR = 0).
178
+ function hasStealthObfuscationCompound(result) {
179
+ if (!result || !result.threats) return false;
180
+ const types = new Set(result.threats.map(t => t.type));
181
+ const stealthExec = types.has('silent_stealth_process') || types.has('detached_process');
182
+ return stealthExec && types.has('js_obfuscation_pattern');
183
+ }
184
+
167
185
  function hasTyposquat(result) {
168
186
  if (!result || !result.threats) return false;
169
187
  return result.threats.some(t => t.type === 'typosquat_detected' || t.type === 'pypi_typosquat_detected');
@@ -496,6 +514,7 @@ module.exports = {
496
514
  hasHighOrCritical,
497
515
  hasHighConfidenceThreat,
498
516
  hasIOCMatch,
517
+ hasStealthObfuscationCompound,
499
518
  hasTyposquat,
500
519
  hasLifecycleWithIntent,
501
520
  isSuspectClassification,
@@ -67,6 +67,7 @@ const {
67
67
  const {
68
68
  isSuspectClassification,
69
69
  hasHighConfidenceThreat,
70
+ hasStealthObfuscationCompound,
70
71
  isSandboxEnabled,
71
72
  isCanaryEnabled,
72
73
  recordError,
@@ -1491,8 +1492,15 @@ async function scanPackage(name, version, ecosystem, tarballUrl, registryMeta, s
1491
1492
  // Adjusts score for webhook decision without mutating persisted alert data.
1492
1493
  // High-confidence malice types BYPASS reputation — supply-chain compromise protection.
1493
1494
  // Reuses npmRegistryMeta fetched earlier (ML Phase 2a) — no duplicate HTTP call.
1495
+ // Malice bypass: don't let reputation attenuation re-crush a confirmed-malice
1496
+ // detection (the scoring.js Track R floor already lifted it). The stealth+obf
1497
+ // compound is included here for the SAME reason as _hasConfirmedMalice — its
1498
+ // component types aren't individually high-confidence, so hasHighConfidenceThreat
1499
+ // alone misses the asyncapi/specs Miasma ATO and would re-attenuate 20 → 2 for the
1500
+ // webhook/ranking decision.
1501
+ const _maliceBypass = hasHighConfidenceThreat(result) || hasStealthObfuscationCompound(result);
1494
1502
  let adjustedResult = result;
1495
- if (ecosystem === 'npm' && !hasHighConfidenceThreat(result)) {
1503
+ if (ecosystem === 'npm' && !_maliceBypass) {
1496
1504
  try {
1497
1505
  const reputationFactor = computeReputationFactor(npmRegistryMeta);
1498
1506
  if (reputationFactor !== 1.0) {
@@ -1507,8 +1515,8 @@ async function scanPackage(name, version, ecosystem, tarballUrl, registryMeta, s
1507
1515
  } catch (err) {
1508
1516
  console.error(`[MONITOR] Reputation error for ${name}: ${err.message}`);
1509
1517
  }
1510
- } else if (ecosystem === 'npm' && hasHighConfidenceThreat(result)) {
1511
- console.log(`[MONITOR] REPUTATION BYPASS: ${name} has high-confidence threat — using raw score`);
1518
+ } else if (ecosystem === 'npm' && _maliceBypass) {
1519
+ console.log(`[MONITOR] REPUTATION BYPASS: ${name} has confirmed-malice signal — using raw score`);
1512
1520
  }
1513
1521
 
1514
1522
  // Record daily alert with post-reputation score for top suspects ranking.
@@ -55,6 +55,7 @@ const {
55
55
  const {
56
56
  HIGH_CONFIDENCE_MALICE_TYPES,
57
57
  hasIOCMatch,
58
+ hasStealthObfuscationCompound,
58
59
  hasHighOrCritical,
59
60
  formatErrorBreakdown
60
61
  } = require('./classify.js');
@@ -128,6 +129,17 @@ function shouldSendWebhook(result, sandboxResult, mlResult) {
128
129
  if (mlResult && mlResult.prediction !== 'clean' && mlResult.probability >= 0.90
129
130
  && hasHighOrCritical(result)) return true;
130
131
 
132
+ // 1c. Stealth-exec + obfuscation compound — reputation-independent bypass.
133
+ // A detached/silent background process shipped with obfuscator.io-mangled code
134
+ // is the account-takeover / CI-supply-chain fingerprint. On an established
135
+ // package the reputationFactor (0.1x) crushes the raw score below `threshold`
136
+ // and silences the webhook: @asyncapi/specs@6.11.2 (Miasma, 2026-07-14) scored
137
+ // globalRiskScore 49 → riskScore 4/LOW → suppressed, despite a CRITICAL
138
+ // silent_stealth_process finding. This compound is quasi-never benign (measured
139
+ // ΔFPR=0 on the 128K-scan production ledger), so it fires regardless of
140
+ // reputation attenuation — same rationale as the IOC bypass above.
141
+ if (hasStealthObfuscationCompound(result)) return true;
142
+
131
143
  // 2. Real sandbox detection (> 30) — always send
132
144
  if (sandboxScore > 30) return true;
133
145
 
package/src/scoring.js CHANGED
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  const { getRule } = require('./rules/index.js');
21
- const { HIGH_CONFIDENCE_MALICE_TYPES } = require('./monitor/classify.js');
21
+ const { HIGH_CONFIDENCE_MALICE_TYPES, hasStealthObfuscationCompound } = require('./monitor/classify.js');
22
22
  // v2.10.73 P1: bundle detection helpers — extended bundle path regex + veto check
23
23
  const { BUNDLE_PATH_RE, hasBundleVetoSignal } = require('./shared/bundle-detect.js');
24
24
 
@@ -1982,7 +1982,15 @@ function _hasConfirmedMalice(threats) {
1982
1982
  const hasCompound = threats.some(t => t.compound === true);
1983
1983
  const hasStagedC2 = threats.some(t => t.type === 'staged_payload') &&
1984
1984
  threats.some(t => t.type === 'suspicious_domain' && t.severity === 'HIGH');
1985
- return hasHC || hasCompound || hasStagedC2;
1985
+ // Stealth-exec + obfuscation compound (asyncapi/specs Miasma ATO 2026-07-14): a
1986
+ // detached/silent background process (silent_stealth_process / detached_process) shipped
1987
+ // with obfuscator.io-mangled code (js_obfuscation_pattern). Quasi-never benign, BUT the
1988
+ // component types are individually NOT in HIGH_CONFIDENCE_MALICE_TYPES (they fire on
1989
+ // pm2 / mongodb / anti-piracy bundles) so only the COMBINATION is confirmed-malice.
1990
+ // Measured ΔFPR=0 on the 130K-scan production ledger (2 matches, both malicious). Shared
1991
+ // predicate with the monitor webhook + reputation bypass (classify.hasStealthObfuscationCompound).
1992
+ const hasStealthObf = hasStealthObfuscationCompound({ threats });
1993
+ return hasHC || hasCompound || hasStagedC2 || hasStealthObf;
1986
1994
  }
1987
1995
 
1988
1996
  // P3 (TeamPCP / Mini Shai-Hulud hardening): broader malice predicate used to
package/stats.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_comment": "GENERATED by scripts/collect-stats.js — do not edit by hand. Run `npm run docs:stats`.",
3
- "version": "2.11.169",
3
+ "version": "2.11.170",
4
4
  "scanners": 22,
5
5
  "rulesTotal": 277,
6
6
  "rulesCore": 272,