muaddib-scanner 2.2.26 → 2.2.27

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.2.26",
3
+ "version": "2.2.27",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/monitor.js CHANGED
@@ -261,10 +261,13 @@ function buildTemporalWebhookEmbed(temporalResult) {
261
261
  };
262
262
  }
263
263
 
264
- async function tryTemporalAlert(temporalResult) {
265
- // Temporal anomalies are logged only — no webhook unless --verbose
266
- console.log(`[MONITOR] ANOMALY (logged only): temporal lifecycle change for ${temporalResult.packageName}`);
267
- if (!isVerboseMode()) return;
264
+ async function tryTemporalAlert(temporalResult, options) {
265
+ const force = options && options.force;
266
+ // Temporal anomalies are logged only no webhook unless --verbose or forced
267
+ if (!force) {
268
+ console.log(`[MONITOR] ANOMALY (logged only): temporal lifecycle change for ${temporalResult.packageName}`);
269
+ }
270
+ if (!force && !isVerboseMode()) return;
268
271
 
269
272
  const url = getWebhookUrl();
270
273
  if (!url) return;
@@ -318,10 +321,13 @@ function buildTemporalAstWebhookEmbed(astResult) {
318
321
  };
319
322
  }
320
323
 
321
- async function tryTemporalAstAlert(astResult) {
322
- // AST anomalies are logged only — no webhook unless --verbose
323
- console.log(`[MONITOR] ANOMALY (logged only): AST change for ${astResult.packageName}`);
324
- if (!isVerboseMode()) return;
324
+ async function tryTemporalAstAlert(astResult, options) {
325
+ const force = options && options.force;
326
+ // AST anomalies are logged only no webhook unless --verbose or forced
327
+ if (!force) {
328
+ console.log(`[MONITOR] ANOMALY (logged only): AST change for ${astResult.packageName}`);
329
+ }
330
+ if (!force && !isVerboseMode()) return;
325
331
 
326
332
  const url = getWebhookUrl();
327
333
  if (!url) return;
@@ -416,10 +422,13 @@ function buildPublishAnomalyWebhookEmbed(publishResult) {
416
422
  };
417
423
  }
418
424
 
419
- async function tryTemporalPublishAlert(publishResult) {
420
- // Publish anomalies are logged only — no webhook unless --verbose
421
- console.log(`[MONITOR] ANOMALY (logged only): publish frequency for ${publishResult.packageName}`);
422
- if (!isVerboseMode()) return;
425
+ async function tryTemporalPublishAlert(publishResult, options) {
426
+ const force = options && options.force;
427
+ // Publish anomalies are logged only no webhook unless --verbose or forced
428
+ if (!force) {
429
+ console.log(`[MONITOR] ANOMALY (logged only): publish frequency for ${publishResult.packageName}`);
430
+ }
431
+ if (!force && !isVerboseMode()) return;
423
432
 
424
433
  const url = getWebhookUrl();
425
434
  if (!url) return;
@@ -517,10 +526,13 @@ function buildMaintainerChangeWebhookEmbed(maintainerResult) {
517
526
  };
518
527
  }
519
528
 
520
- async function tryTemporalMaintainerAlert(maintainerResult) {
521
- // Maintainer changes are logged only — no webhook unless --verbose
522
- console.log(`[MONITOR] ANOMALY (logged only): maintainer change for ${maintainerResult.packageName}`);
523
- if (!isVerboseMode()) return;
529
+ async function tryTemporalMaintainerAlert(maintainerResult, options) {
530
+ const force = options && options.force;
531
+ // Maintainer changes are logged only no webhook unless --verbose or forced
532
+ if (!force) {
533
+ console.log(`[MONITOR] ANOMALY (logged only): maintainer change for ${maintainerResult.packageName}`);
534
+ }
535
+ if (!force && !isVerboseMode()) return;
524
536
 
525
537
  const url = getWebhookUrl();
526
538
  if (!url) return;
@@ -1611,6 +1623,41 @@ async function poll(state) {
1611
1623
  console.log(`[MONITOR] Found ${npmDisplay} npm + ${pypiDisplay} PyPI new packages`);
1612
1624
  }
1613
1625
 
1626
+ /**
1627
+ * Returns the highest severity level from all suspicious temporal results.
1628
+ * Used to decide whether a temporal alert can be downgraded to FALSE POSITIVE.
1629
+ * Returns 'CRITICAL', 'HIGH', 'MEDIUM', 'LOW', or null if no findings.
1630
+ */
1631
+ function getTemporalMaxSeverity(temporalResult, astResult, publishResult, maintainerResult) {
1632
+ const SEVERITY_ORDER = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
1633
+ let maxLevel = 0;
1634
+ let maxSeverity = null;
1635
+
1636
+ const allFindings = [];
1637
+ if (temporalResult && temporalResult.suspicious && temporalResult.findings) {
1638
+ allFindings.push(...temporalResult.findings);
1639
+ }
1640
+ if (astResult && astResult.suspicious && astResult.findings) {
1641
+ allFindings.push(...astResult.findings);
1642
+ }
1643
+ if (publishResult && publishResult.suspicious && publishResult.anomalies) {
1644
+ allFindings.push(...publishResult.anomalies);
1645
+ }
1646
+ if (maintainerResult && maintainerResult.suspicious && maintainerResult.findings) {
1647
+ allFindings.push(...maintainerResult.findings);
1648
+ }
1649
+
1650
+ for (const f of allFindings) {
1651
+ const level = SEVERITY_ORDER[f.severity] || 0;
1652
+ if (level > maxLevel) {
1653
+ maxLevel = level;
1654
+ maxSeverity = f.severity;
1655
+ }
1656
+ }
1657
+
1658
+ return maxSeverity;
1659
+ }
1660
+
1614
1661
  /**
1615
1662
  * Returns true if publish_anomaly is the ONLY suspicious temporal result.
1616
1663
  * publish_anomaly alone is too noisy for webhooks — only alert when combined
@@ -1710,9 +1757,24 @@ async function resolveTarballAndScan(item) {
1710
1757
  // Sandbox ran and package is CLEAN → suppress temporal webhooks
1711
1758
  if (sandboxResult && sandboxResult.score === 0) {
1712
1759
  console.log(`[MONITOR] FALSE POSITIVE (sandbox clean, no alert): ${item.name}@${item.version}`);
1713
- // Static scan is CLEAN (0 findings) and no sandbox ran → suppress temporal webhooks
1760
+ // Static scan is CLEAN (0 findings) and no sandbox ran
1714
1761
  } else if (staticClean && !sandboxResult) {
1715
- console.log(`[MONITOR] FALSE POSITIVE (static clean, no alert): ${item.name}@${item.version}`);
1762
+ // Temporal CRITICAL/HIGH cannot be downgraded — "static clean" may mean obfuscated payload
1763
+ const temporalMaxSev = getTemporalMaxSeverity(temporalResult, astResult, publishResult, maintainerResult);
1764
+ if (temporalMaxSev === 'CRITICAL' || temporalMaxSev === 'HIGH') {
1765
+ console.log(`[MONITOR] Temporal ${temporalMaxSev} preserved despite static clean scan: ${item.name}@${item.version}`);
1766
+ console.log(`[MONITOR] SUSPECT (temporal anomaly, possible obfuscated payload): ${item.name}@${item.version}`);
1767
+ stats.suspect++;
1768
+ stats.clean--;
1769
+ updateScanStats('suspect');
1770
+ // Force-send temporal webhooks (bypass verbose mode check)
1771
+ if (temporalResult && temporalResult.suspicious) await tryTemporalAlert(temporalResult, { force: true });
1772
+ if (astResult && astResult.suspicious) await tryTemporalAstAlert(astResult, { force: true });
1773
+ if (publishResult && publishResult.suspicious) await tryTemporalPublishAlert(publishResult, { force: true });
1774
+ if (maintainerResult && maintainerResult.suspicious) await tryTemporalMaintainerAlert(maintainerResult, { force: true });
1775
+ } else {
1776
+ console.log(`[MONITOR] FALSE POSITIVE (static clean, no alert): ${item.name}@${item.version}`);
1777
+ }
1716
1778
  // publish_anomaly alone → no webhook (too noisy, not actionable alone)
1717
1779
  } else if (isPublishAnomalyOnly(temporalResult, astResult, publishResult, maintainerResult)) {
1718
1780
  console.log(`[MONITOR] PUBLISH ANOMALY (alone, no alert): ${item.name}@${item.version}`);
@@ -1787,6 +1849,7 @@ module.exports = {
1787
1849
  runTemporalMaintainerCheck,
1788
1850
  isCanaryEnabled,
1789
1851
  buildCanaryExfiltrationWebhookEmbed,
1852
+ getTemporalMaxSeverity,
1790
1853
  isPublishAnomalyOnly,
1791
1854
  isVerboseMode,
1792
1855
  setVerboseMode,