muaddib-scanner 2.5.7 → 2.5.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/_test_aiweapon.js +12 -0
- package/_test_fp.js +11 -0
- package/_test_fp2.js +37 -0
- package/_test_fp3.js +19 -0
- package/_test_nodemailer.js +17 -0
- package/_test_p4_detail.js +45 -0
- package/_test_p4_detail2.js +32 -0
- package/_test_p4_quick.js +33 -0
- package/_test_regex.js +5 -0
- package/_vitest_result.json +395 -0
- package/iocs/builtin.yaml +80 -11
- package/iocs/packages.yaml +216 -12
- package/logs/alerts/2026-03-06T13-25-09-667-evil-pkg.json +20 -0
- package/logs/alerts/2026-03-06T13-25-09-668-evil-pkg.json +20 -0
- package/logs/alerts/2026-03-06T13-25-09-668-suspect-pkg.json +24 -0
- package/logs/alerts/2026-03-06T13-25-10-228-evil-pkg.json +24 -0
- package/logs/daily-reports/2026-03-06.json +61 -0
- package/package.json +1 -1
- package/src/ioc/scraper.js +1 -1
- package/src/scanner/ast-detectors.js +24 -8
- package/src/scanner/ast.js +21 -1
- package/src/scanner/typosquat.js +5 -1
- package/src/scoring.js +40 -10
package/src/scoring.js
CHANGED
|
@@ -106,11 +106,17 @@ const FP_COUNT_THRESHOLDS = {
|
|
|
106
106
|
dynamic_require: { maxCount: 10, from: 'HIGH', to: 'LOW' },
|
|
107
107
|
dangerous_call_function: { maxCount: 5, from: 'MEDIUM', to: 'LOW' },
|
|
108
108
|
require_cache_poison: { maxCount: 3, from: 'CRITICAL', to: 'LOW' },
|
|
109
|
-
suspicious_dataflow: { maxCount:
|
|
109
|
+
suspicious_dataflow: { maxCount: 3, to: 'LOW' },
|
|
110
110
|
obfuscation_detected: { maxCount: 3, to: 'LOW' },
|
|
111
111
|
module_compile_dynamic: { maxCount: 3, from: 'CRITICAL', to: 'LOW' },
|
|
112
112
|
module_compile: { maxCount: 3, from: 'CRITICAL', to: 'LOW' },
|
|
113
|
-
zlib_inflate_eval: { maxCount: 2, from: 'CRITICAL', to: 'LOW' }
|
|
113
|
+
zlib_inflate_eval: { maxCount: 2, from: 'CRITICAL', to: 'LOW' },
|
|
114
|
+
// P4: plugin loaders legitimately use many dynamic imports (webpack, eslint, knex, gatsby)
|
|
115
|
+
dynamic_import: { maxCount: 5, from: 'HIGH', to: 'LOW' },
|
|
116
|
+
// P4: hash algorithms contain bit manipulation that triggers obfuscation heuristics
|
|
117
|
+
js_obfuscation_pattern: { maxCount: 1, from: 'HIGH', to: 'LOW' },
|
|
118
|
+
// P4: bundled credential_tampering from minified alias resolution (jspdf, lerna)
|
|
119
|
+
credential_tampering: { maxCount: 5, to: 'LOW' }
|
|
114
120
|
};
|
|
115
121
|
|
|
116
122
|
// Types exempt from dist/ downgrade — IOC matches and lifecycle scripts are always real
|
|
@@ -182,6 +188,18 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
182
188
|
|
|
183
189
|
const totalThreats = threats.length;
|
|
184
190
|
|
|
191
|
+
// P4: Plugin loader pattern — packages with 2+ dynamic_require + dynamic_import combined
|
|
192
|
+
// are legitimate plugin systems (webpack, eslint, karma, knex, jasmine, gatsby).
|
|
193
|
+
// Malware uses one pattern, not both. Bypass the per-type percentage guard.
|
|
194
|
+
const pluginLoaderCount = (typeCounts.dynamic_require || 0) + (typeCounts.dynamic_import || 0);
|
|
195
|
+
if (pluginLoaderCount > 1) {
|
|
196
|
+
for (const t of threats) {
|
|
197
|
+
if ((t.type === 'dynamic_require' || t.type === 'dynamic_import') && t.severity === 'HIGH') {
|
|
198
|
+
t.severity = 'LOW';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
185
203
|
for (const t of threats) {
|
|
186
204
|
// Count-based downgrade: if a threat type appears too many times,
|
|
187
205
|
// it's a framework/plugin system, not malware.
|
|
@@ -190,7 +208,10 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
190
208
|
const rule = FP_COUNT_THRESHOLDS[t.type];
|
|
191
209
|
if (rule && typeCounts[t.type] > rule.maxCount && (!rule.from || t.severity === rule.from)) {
|
|
192
210
|
const typeRatio = typeCounts[t.type] / totalThreats;
|
|
193
|
-
|
|
211
|
+
// P4: suspicious_dataflow bypasses the percentage guard — multiple data flow paths
|
|
212
|
+
// indicate a complex application (SMTP client, monitoring agent), not malware.
|
|
213
|
+
// Malware has 1-2 targeted exfiltration flows, not 4+.
|
|
214
|
+
if (typeRatio < 0.5 || t.type === 'suspicious_dataflow') {
|
|
194
215
|
t.severity = rule.to;
|
|
195
216
|
}
|
|
196
217
|
}
|
|
@@ -220,11 +241,13 @@ function applyFPReductions(threats, reachableFiles, packageName) {
|
|
|
220
241
|
}
|
|
221
242
|
}
|
|
222
243
|
|
|
223
|
-
// Dist/build/minified files: bundler artifacts get severity downgraded
|
|
244
|
+
// Dist/build/minified files: bundler artifacts get severity downgraded two notches.
|
|
224
245
|
// Real malware injects payloads in source files, not in dist/ output.
|
|
246
|
+
// Two-notch downgrade (P4): cross-file bonus amplifies dist/ noise in large packages.
|
|
247
|
+
// IOC matches and lifecycle scripts are exempt (DIST_EXEMPT_TYPES).
|
|
225
248
|
if (t.file && !DIST_EXEMPT_TYPES.has(t.type) && DIST_FILE_RE.test(t.file)) {
|
|
226
|
-
if (t.severity === 'CRITICAL') t.severity = '
|
|
227
|
-
else if (t.severity === 'HIGH') t.severity = '
|
|
249
|
+
if (t.severity === 'CRITICAL') t.severity = 'MEDIUM';
|
|
250
|
+
else if (t.severity === 'HIGH') t.severity = 'LOW';
|
|
228
251
|
else if (t.severity === 'MEDIUM') t.severity = 'LOW';
|
|
229
252
|
}
|
|
230
253
|
|
|
@@ -271,9 +294,11 @@ function calculateRiskScore(deduped) {
|
|
|
271
294
|
let maxFileScore = 0;
|
|
272
295
|
let mostSuspiciousFile = null;
|
|
273
296
|
const fileScores = {};
|
|
297
|
+
const fileHasMediumPlus = {}; // P4: track files with MEDIUM+ threats for cross-file bonus
|
|
274
298
|
for (const [file, fileThreats] of fileGroups) {
|
|
275
299
|
const score = computeGroupScore(fileThreats);
|
|
276
300
|
fileScores[file] = score;
|
|
301
|
+
fileHasMediumPlus[file] = fileThreats.some(t => t.severity !== 'LOW');
|
|
277
302
|
if (score > maxFileScore) {
|
|
278
303
|
maxFileScore = score;
|
|
279
304
|
mostSuspiciousFile = file;
|
|
@@ -286,11 +311,16 @@ function calculateRiskScore(deduped) {
|
|
|
286
311
|
// 5. Cross-file bonus: aggregate signal from non-max files
|
|
287
312
|
// A package with 3 files each scoring 20 is more suspicious than 1 file scoring 20.
|
|
288
313
|
// Add 25% of each non-max file's score as a bonus, capped at 25.
|
|
289
|
-
|
|
314
|
+
// P4: Only count files that have at least one MEDIUM+ threat.
|
|
315
|
+
// Files with only LOW findings are noise in large packages and shouldn't amplify the score.
|
|
316
|
+
const bonusEligibleScores = Object.entries(fileScores)
|
|
317
|
+
.filter(([file]) => fileHasMediumPlus[file])
|
|
318
|
+
.map(([, score]) => score)
|
|
319
|
+
.sort((a, b) => b - a);
|
|
290
320
|
let crossFileBonus = 0;
|
|
291
|
-
if (
|
|
292
|
-
for (let i = 1; i <
|
|
293
|
-
crossFileBonus += Math.ceil(
|
|
321
|
+
if (bonusEligibleScores.length > 1) {
|
|
322
|
+
for (let i = 1; i < bonusEligibleScores.length; i++) {
|
|
323
|
+
crossFileBonus += Math.ceil(bonusEligibleScores[i] * 0.25);
|
|
294
324
|
}
|
|
295
325
|
crossFileBonus = Math.min(crossFileBonus, 25);
|
|
296
326
|
}
|