muaddib-scanner 2.7.9 → 2.8.0

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.7.9",
3
+ "version": "2.8.0",
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
@@ -592,7 +592,8 @@ async function run(targetPath, options = {}) {
592
592
  // Enrich each threat with rules
593
593
  const enrichedThreats = deduped.map(t => {
594
594
  const rule = getRule(t.type);
595
- const points = SEVERITY_WEIGHTS[t.severity] || 0;
595
+ const confFactor = { high: 1.0, medium: 0.85, low: 0.6 }[rule.confidence] || 1.0;
596
+ const points = Math.round((SEVERITY_WEIGHTS[t.severity] || 0) * confFactor);
596
597
  return {
597
598
  ...t,
598
599
  rule_id: rule.id || t.type,
@@ -154,6 +154,8 @@ function loadStaticIOCs() {
154
154
 
155
155
  const MAX_REDIRECTS = 5;
156
156
  const MAX_RESPONSE_SIZE = 200 * 1024 * 1024; // 200MB
157
+ const MAX_ENTRY_UNCOMPRESSED = 50 * 1024 * 1024; // 50MB per zip entry
158
+ const MAX_TOTAL_UNCOMPRESSED = 500 * 1024 * 1024; // 500MB total budget per zip
157
159
 
158
160
  function fetchJSON(url, options = {}, redirectCount = 0) {
159
161
  return new Promise((resolve, reject) => {
@@ -762,6 +764,7 @@ async function scrapeOSVDataDump() {
762
764
 
763
765
  let malCount = 0;
764
766
  let skippedCount = 0;
767
+ let totalUncompressed = 0;
765
768
 
766
769
  const spinner = new Spinner();
767
770
  try {
@@ -775,6 +778,19 @@ async function scrapeOSVDataDump() {
775
778
  if (!name.startsWith('MAL-') || !name.endsWith('.json')) {
776
779
  skippedCount++;
777
780
  } else {
781
+ // Zip bomb protection: check declared uncompressed size
782
+ const entrySize = entry.header ? entry.header.size : 0;
783
+ if (entrySize > MAX_ENTRY_UNCOMPRESSED) {
784
+ console.warn(`[WARN] Zip bomb protection: skipping oversized entry ${name} (${entrySize} bytes)`);
785
+ skippedCount++;
786
+ continue;
787
+ }
788
+ totalUncompressed += entrySize;
789
+ if (totalUncompressed > MAX_TOTAL_UNCOMPRESSED) {
790
+ console.warn(`[WARN] Zip bomb protection: total uncompressed budget exceeded at entry ${name}, stopping`);
791
+ break;
792
+ }
793
+
778
794
  try {
779
795
  const content = entry.getData().toString('utf8');
780
796
  const vuln = JSON.parse(content);
@@ -784,8 +800,8 @@ async function scrapeOSVDataDump() {
784
800
  // Track known IDs so OSSF can skip them
785
801
  knownIds.add(vuln.id || path.basename(name, '.json'));
786
802
  malCount++;
787
- } catch {
788
- // Skip unparseable entries
803
+ } catch (parseErr) {
804
+ console.warn(`[WARN] Skipping unparseable entry: ${name}`);
789
805
  }
790
806
  }
791
807
 
@@ -824,6 +840,7 @@ async function scrapeOSVPyPIDataDump() {
824
840
 
825
841
  let malCount = 0;
826
842
  let skippedCount = 0;
843
+ let totalUncompressed = 0;
827
844
 
828
845
  const spinner = new Spinner();
829
846
  try {
@@ -837,14 +854,27 @@ async function scrapeOSVPyPIDataDump() {
837
854
  if (!name.startsWith('MAL-') || !name.endsWith('.json')) {
838
855
  skippedCount++;
839
856
  } else {
857
+ // Zip bomb protection: check declared uncompressed size
858
+ const entrySize = entry.header ? entry.header.size : 0;
859
+ if (entrySize > MAX_ENTRY_UNCOMPRESSED) {
860
+ console.warn(`[WARN] Zip bomb protection: skipping oversized entry ${name} (${entrySize} bytes)`);
861
+ skippedCount++;
862
+ continue;
863
+ }
864
+ totalUncompressed += entrySize;
865
+ if (totalUncompressed > MAX_TOTAL_UNCOMPRESSED) {
866
+ console.warn(`[WARN] Zip bomb protection: total uncompressed budget exceeded at entry ${name}, stopping`);
867
+ break;
868
+ }
869
+
840
870
  try {
841
871
  const content = entry.getData().toString('utf8');
842
872
  const vuln = JSON.parse(content);
843
873
  const parsed = parseOSVEntry(vuln, 'osv-malicious-pypi', 'PyPI');
844
874
  for (const p of parsed) packages.push(p);
845
875
  malCount++;
846
- } catch {
847
- // Skip unparseable entries
876
+ } catch (parseErr) {
877
+ console.warn(`[WARN] Skipping unparseable entry: ${name}`);
848
878
  }
849
879
  }
850
880
 
@@ -1356,7 +1386,8 @@ module.exports = {
1356
1386
  parseCSVLine, parseCSV, extractVersions, parseOSVEntry,
1357
1387
  createFreshness, isAllowedRedirect, loadStaticIOCs,
1358
1388
  validateIOCEntry,
1359
- CONFIDENCE_ORDER, ALLOWED_REDIRECT_DOMAINS
1389
+ CONFIDENCE_ORDER, ALLOWED_REDIRECT_DOMAINS,
1390
+ MAX_ENTRY_UNCOMPRESSED, MAX_TOTAL_UNCOMPRESSED
1360
1391
  };
1361
1392
 
1362
1393
  // Direct execution if called as CLI
@@ -345,7 +345,7 @@ function createOptimizedIOCs(iocs) {
345
345
  const NEVER_WILDCARD = new Set([
346
346
  'event-stream', 'ua-parser-js', 'coa', 'rc',
347
347
  'colors', 'faker', 'node-ipc',
348
- 'posthog-node', 'ngx-bootstrap', '@asyncapi/specs'
348
+ 'posthog-node', 'posthog-js', 'ngx-bootstrap', '@asyncapi/specs'
349
349
  ]);
350
350
 
351
351
  function generateCompactIOCs(fullIOCs) {
package/src/scoring.js CHANGED
@@ -1,3 +1,5 @@
1
+ const { getRule } = require('./rules/index.js');
2
+
1
3
  // ============================================
2
4
  // SCORING CONSTANTS
3
5
  // ============================================
@@ -38,6 +40,13 @@ const MAX_RISK_SCORE = 100;
38
40
  // to limit noise while preserving some signal. CRITICAL and HIGH prototype_hook findings still score normally.
39
41
  const PROTO_HOOK_MEDIUM_CAP = 15;
40
42
 
43
+ // Confidence-weighted scoring factors (v2.7.10)
44
+ // High-confidence detections (eval, IOC, shell injection) score at full weight.
45
+ // Medium-confidence heuristics (lifecycle_script, obfuscation, high_entropy) are discounted.
46
+ // Low-confidence informational findings (possible_obfuscation, base64_in_script) are heavily discounted.
47
+ // Unknown/paranoid rules default to 1.0 (no penalty).
48
+ const CONFIDENCE_FACTORS = { high: 1.0, medium: 0.85, low: 0.6 };
49
+
41
50
  // ============================================
42
51
  // PER-FILE MAX SCORING (v2.2.11)
43
52
  // ============================================
@@ -76,24 +85,24 @@ function isPackageLevelThreat(threat) {
76
85
  * @returns {number} score 0-100
77
86
  */
78
87
  function computeGroupScore(threats) {
79
- const criticalCount = threats.filter(t => t.severity === 'CRITICAL').length;
80
- const highCount = threats.filter(t => t.severity === 'HIGH').length;
81
- const mediumCount = threats.filter(t => t.severity === 'MEDIUM').length;
82
- const lowCount = threats.filter(t => t.severity === 'LOW').length;
88
+ let score = 0;
89
+ let protoHookMediumPoints = 0;
83
90
 
84
- const mediumProtoHookCount = threats.filter(
85
- t => t.type === 'prototype_hook' && t.severity === 'MEDIUM'
86
- ).length;
87
- const protoHookPoints = Math.min(mediumProtoHookCount * SEVERITY_WEIGHTS.MEDIUM, PROTO_HOOK_MEDIUM_CAP);
88
- const otherMediumCount = mediumCount - mediumProtoHookCount;
91
+ for (const t of threats) {
92
+ const weight = SEVERITY_WEIGHTS[t.severity] || 0;
93
+ const rule = getRule(t.type);
94
+ const factor = CONFIDENCE_FACTORS[rule.confidence] || 1.0;
89
95
 
90
- let score = 0;
91
- score += criticalCount * SEVERITY_WEIGHTS.CRITICAL;
92
- score += highCount * SEVERITY_WEIGHTS.HIGH;
93
- score += otherMediumCount * SEVERITY_WEIGHTS.MEDIUM;
94
- score += protoHookPoints;
95
- score += lowCount * SEVERITY_WEIGHTS.LOW;
96
- return Math.min(MAX_RISK_SCORE, score);
96
+ if (t.type === 'prototype_hook' && t.severity === 'MEDIUM') {
97
+ protoHookMediumPoints += weight * factor;
98
+ continue;
99
+ }
100
+
101
+ score += weight * factor;
102
+ }
103
+
104
+ score += Math.min(protoHookMediumPoints, PROTO_HOOK_MEDIUM_CAP);
105
+ return Math.min(MAX_RISK_SCORE, Math.round(score));
97
106
  }
98
107
 
99
108
  // ============================================
@@ -434,6 +443,6 @@ function calculateRiskScore(deduped, intentResult) {
434
443
  }
435
444
 
436
445
  module.exports = {
437
- SEVERITY_WEIGHTS, RISK_THRESHOLDS, MAX_RISK_SCORE,
446
+ SEVERITY_WEIGHTS, RISK_THRESHOLDS, MAX_RISK_SCORE, CONFIDENCE_FACTORS,
438
447
  isPackageLevelThreat, computeGroupScore, applyFPReductions, calculateRiskScore
439
448
  };