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 +1 -1
- package/src/monitor.js +81 -18
- package/src/response/playbooks.js +362 -346
- package/src/rules/index.js +1076 -1038
- package/src/scanner/ast-detectors.js +67 -0
- package/src/scanner/ast.js +10 -1
package/package.json
CHANGED
package/src/monitor.js
CHANGED
|
@@ -261,10 +261,13 @@ function buildTemporalWebhookEmbed(temporalResult) {
|
|
|
261
261
|
};
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
async function tryTemporalAlert(temporalResult) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (!
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
if (!
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
if (!
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
if (!
|
|
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
|
|
1760
|
+
// Static scan is CLEAN (0 findings) and no sandbox ran
|
|
1714
1761
|
} else if (staticClean && !sandboxResult) {
|
|
1715
|
-
|
|
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,
|