muaddib-scanner 2.2.7 → 2.2.8
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 +48 -0
- package/src/scanner/typosquat.js +13 -1
- package/tmp-summary.js +0 -24
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -64,6 +64,50 @@ const MAX_RISK_SCORE = 100;
|
|
|
64
64
|
|
|
65
65
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
66
66
|
|
|
67
|
+
// ============================================
|
|
68
|
+
// FP REDUCTION POST-PROCESSING
|
|
69
|
+
// ============================================
|
|
70
|
+
// Legitimate frameworks produce high volumes of certain threat types that
|
|
71
|
+
// malware never does. This function downgrades severity when the count
|
|
72
|
+
// exceeds thresholds only seen in legitimate codebases.
|
|
73
|
+
const FP_COUNT_THRESHOLDS = {
|
|
74
|
+
dynamic_require: { maxCount: 10, from: 'HIGH', to: 'LOW' },
|
|
75
|
+
dangerous_call_function: { maxCount: 5, from: 'MEDIUM', to: 'LOW' },
|
|
76
|
+
require_cache_poison: { maxCount: 3, from: 'CRITICAL', to: 'LOW' }
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Custom class prototypes that HTTP frameworks legitimately extend.
|
|
80
|
+
// Distinguished from dangerous core Node.js prototype hooks.
|
|
81
|
+
const FRAMEWORK_PROTOTYPES = ['Request', 'Response', 'App', 'Router'];
|
|
82
|
+
const FRAMEWORK_PROTO_RE = new RegExp(
|
|
83
|
+
'^(' + FRAMEWORK_PROTOTYPES.join('|') + ')\\.prototype\\.'
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
function applyFPReductions(threats) {
|
|
87
|
+
// Count occurrences of each threat type (package-level, across all files)
|
|
88
|
+
const typeCounts = {};
|
|
89
|
+
for (const t of threats) {
|
|
90
|
+
typeCounts[t.type] = (typeCounts[t.type] || 0) + 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const t of threats) {
|
|
94
|
+
// Count-based downgrade: if a threat type appears too many times,
|
|
95
|
+
// it's a framework/plugin system, not malware
|
|
96
|
+
const rule = FP_COUNT_THRESHOLDS[t.type];
|
|
97
|
+
if (rule && typeCounts[t.type] > rule.maxCount && t.severity === rule.from) {
|
|
98
|
+
t.severity = rule.to;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Prototype hook: framework class prototypes → MEDIUM
|
|
102
|
+
// Core Node.js prototypes (http.IncomingMessage, net.Socket) stay CRITICAL
|
|
103
|
+
// Browser/native APIs (globalThis.fetch, XMLHttpRequest) stay HIGH
|
|
104
|
+
if (t.type === 'prototype_hook' && t.severity === 'HIGH' &&
|
|
105
|
+
FRAMEWORK_PROTO_RE.test(t.message)) {
|
|
106
|
+
t.severity = 'MEDIUM';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
67
111
|
// Paranoid mode scanner
|
|
68
112
|
function scanParanoid(targetPath) {
|
|
69
113
|
const threats = [];
|
|
@@ -563,6 +607,10 @@ async function run(targetPath, options = {}) {
|
|
|
563
607
|
}
|
|
564
608
|
}
|
|
565
609
|
|
|
610
|
+
// FP reduction: legitimate frameworks produce high volumes of certain threat types.
|
|
611
|
+
// A malware package typically has 1-3 occurrences, not dozens.
|
|
612
|
+
applyFPReductions(deduped);
|
|
613
|
+
|
|
566
614
|
// Enrich each threat with rules
|
|
567
615
|
const enrichedThreats = deduped.map(t => {
|
|
568
616
|
const rule = getRule(t.type);
|
package/src/scanner/typosquat.js
CHANGED
|
@@ -72,7 +72,19 @@ const WHITELIST = new Set([
|
|
|
72
72
|
'eslint-config-prettier', 'eslint-plugin-prettier',
|
|
73
73
|
'eslint-scope', 'eslint-visitor-keys',
|
|
74
74
|
'esbuild-register',
|
|
75
|
-
'neo-async'
|
|
75
|
+
'neo-async',
|
|
76
|
+
|
|
77
|
+
// Packages with names close to other popular packages (not typosquats)
|
|
78
|
+
'chai', // resembles chalk (missing_char)
|
|
79
|
+
'pino', // resembles sinon (missing_char)
|
|
80
|
+
'ioredis', // resembles redis (extra prefix)
|
|
81
|
+
'bcryptjs', // resembles bcrypt (suffix)
|
|
82
|
+
'recast', // resembles react (extra_char)
|
|
83
|
+
'asyncdi', // resembles async (suffix)
|
|
84
|
+
'redux', // resembles redis (wrong_char)
|
|
85
|
+
'args', // resembles yargs (missing_char)
|
|
86
|
+
'oxlint', // resembles eslint (wrong_char)
|
|
87
|
+
'vasync' // resembles async (extra prefix)
|
|
76
88
|
]);
|
|
77
89
|
|
|
78
90
|
|
package/tmp-summary.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
const m = require('./metrics/v2.2.6.json');
|
|
2
|
-
console.log('=== BENIGN FPR RESULTS (50 packages) ===');
|
|
3
|
-
console.log('TPR:', m.groundTruth.tpr*100+'%');
|
|
4
|
-
console.log('FPR:', m.benign.fpr*100+'%', '('+m.benign.flagged+'/'+m.benign.scanned+')');
|
|
5
|
-
console.log('ADR:', m.adversarial.adr*100+'%');
|
|
6
|
-
console.log();
|
|
7
|
-
console.log('=== FALSE POSITIVES (score > 20) ===');
|
|
8
|
-
const fps = m.benign.details.filter(d => d.flagged).sort((a,b) => b.score - a.score);
|
|
9
|
-
fps.forEach(fp => {
|
|
10
|
-
console.log(fp.name+': score '+fp.score);
|
|
11
|
-
const types = {};
|
|
12
|
-
(fp.threats||[]).forEach(t => { types[t.type] = (types[t.type]||0)+1; });
|
|
13
|
-
Object.entries(types).sort((a,b)=>b[1]-a[1]).forEach(([k,v]) => console.log(' '+k+': '+v));
|
|
14
|
-
});
|
|
15
|
-
console.log();
|
|
16
|
-
console.log('=== NON-FLAGGED BUT ELEVATED (score > 0) ===');
|
|
17
|
-
m.benign.details.filter(d => !d.flagged && d.score > 0).sort((a,b) => b.score - a.score).forEach(d => console.log(d.name+': score '+d.score));
|
|
18
|
-
console.log();
|
|
19
|
-
console.log('=== THREAT TYPE FREQUENCY ===');
|
|
20
|
-
const allTypes = {};
|
|
21
|
-
fps.forEach(fp => {
|
|
22
|
-
(fp.threats||[]).forEach(t => { allTypes[t.type] = (allTypes[t.type]||0)+1; });
|
|
23
|
-
});
|
|
24
|
-
Object.entries(allTypes).sort((a,b)=>b[1]-a[1]).forEach(([k,v]) => console.log(' '+k+': '+v));
|