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 +1 -1
- package/src/index.js +2 -1
- package/src/ioc/scraper.js +36 -5
- package/src/ioc/updater.js +1 -1
- package/src/scoring.js +26 -17
package/package.json
CHANGED
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
|
|
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,
|
package/src/ioc/scraper.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
package/src/ioc/updater.js
CHANGED
|
@@ -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
|
-
|
|
80
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
};
|