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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.2.7",
3
+ "version": "2.2.8",
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
@@ -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);
@@ -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));