muaddib-scanner 2.6.1 → 2.6.2

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.6.1",
3
+ "version": "2.6.2",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "@eslint/js": "10.0.1",
58
- "eslint": "10.0.2",
58
+ "eslint": "10.0.3",
59
59
  "eslint-plugin-security": "^4.0.0",
60
60
  "globals": "17.4.0"
61
61
  }
package/src/scoring.js CHANGED
@@ -122,8 +122,14 @@ const FP_COUNT_THRESHOLDS = {
122
122
  // B1 FP reduction: bundled code aliases eval/Function (sinon, storybook, vitest)
123
123
  dangerous_call_eval: { maxCount: 3, from: 'MEDIUM', to: 'LOW' },
124
124
  // P6: HTTP client libraries (undici, aws-sdk, nodemailer, jsdom) parse Authorization/Bearer headers
125
- // with 5+ credential regexes. Real harvesters use 1-2 targeted regexes.
126
- credential_regex_harvest: { maxCount: 4, from: 'HIGH', to: 'LOW' }
125
+ // with 3+ credential regexes. Real harvesters use 1-2 targeted regexes.
126
+ credential_regex_harvest: { maxCount: 2, from: 'HIGH', to: 'LOW' },
127
+ // P7: Config frameworks (pm2, nx, dotenv, aws-sdk) read 10+ env vars — not credential theft.
128
+ // Real stealers access 1-5 targeted env vars. Count >10 = config loader pattern.
129
+ env_access: { maxCount: 10, from: 'HIGH', to: 'LOW' },
130
+ // P7: Bundled files with 5+ high-entropy strings are data files, not malware payloads.
131
+ // Real payloads use 1-2 targeted encoded strings. Count >5 = bundled assets/data.
132
+ high_entropy_string: { maxCount: 5, to: 'LOW' }
127
133
  };
128
134
 
129
135
  // Types exempt from dist/ downgrade — IOC matches, lifecycle scripts, and
@@ -144,8 +150,9 @@ const DIST_EXEMPT_TYPES = new Set([
144
150
  // fetch_decrypt_exec (fetch+decrypt+eval triple) remains exempt — never coincidental.
145
151
  ]);
146
152
 
147
- // Regex matching dist/build/minified/bundled file paths
148
- const DIST_FILE_RE = /(?:^|[/\\])(?:dist|build)[/\\]|\.min\.js$|\.bundle\.js$/i;
153
+ // Regex matching dist/build/out/output/minified/bundled file paths
154
+ // P7: added out/ and output/ — common build output directories (esbuild, custom build scripts)
155
+ const DIST_FILE_RE = /(?:^|[/\\])(?:dist|build|out|output)[/\\]|\.min\.js$|\.bundle\.js$/i;
149
156
 
150
157
  // Bundler artifact types: get two-notch downgrade in dist/ files (CRITICAL→MEDIUM, HIGH→LOW).
151
158
  // These are individual pattern signals that bundlers routinely produce (eval for globalThis,
@@ -155,7 +162,9 @@ const DIST_BUNDLER_ARTIFACT_TYPES = new Set([
155
162
  'dynamic_require', 'dynamic_import',
156
163
  'obfuscation_detected', 'high_entropy_string', 'possible_obfuscation',
157
164
  'js_obfuscation_pattern', 'vm_code_execution',
158
- 'module_compile', 'module_compile_dynamic'
165
+ 'module_compile', 'module_compile_dynamic',
166
+ // P7: env_access in dist/ is bundled SDK config reading, not credential theft
167
+ 'env_access'
159
168
  ]);
160
169
 
161
170
  // Types exempt from reachability downgrade — IOC matches, lifecycle, and package-level types.
@@ -223,15 +232,16 @@ function applyFPReductions(threats, reachableFiles, packageName) {
223
232
  const rule = FP_COUNT_THRESHOLDS[t.type];
224
233
  if (rule && typeCounts[t.type] > rule.maxCount && (!rule.from || t.severity === rule.from)) {
225
234
  const typeRatio = typeCounts[t.type] / totalThreats;
226
- // suspicious_dataflow: partial bypass of percentage guard up to 80%.
227
- // Complex apps (SMTP, monitoring) have 50-80% dataflow findings — still downgrade.
228
- // But if dataflow is >80% of ALL findings, it may be real targeted exfiltration.
229
- // (Audit fix: full bypass was exploitable4+ dataflow patterns = all LOW.)
235
+ // suspicious_dataflow: full bypass of percentage guard. Packages with >3 suspicious_dataflow
236
+ // findings are always legitimate SDKs (SMTP, monitoring, analytics). Real malware has 1-2
237
+ // targeted source→sink pairs. The count >3 threshold is sufficient protection.
238
+ // P7: removed 80% ratio capit caused ~30k FP hits in production on SDK packages
239
+ // where dataflow was the dominant finding type (e.g. @darajs/core, addio-admin-sdk).
230
240
  // vm_code_execution: full bypass — packages with only vm.Script calls (cassandra-driver,
231
241
  // webpack, jest) are legitimate. Real malware using vm always has other signals
232
242
  // (network, fs, obfuscation). The >3 count threshold is sufficient protection.
233
243
  if (typeRatio < 0.4 ||
234
- (t.type === 'suspicious_dataflow' && typeRatio < 0.8) ||
244
+ t.type === 'suspicious_dataflow' ||
235
245
  t.type === 'vm_code_execution') {
236
246
  t.severity = rule.to;
237
247
  }
@@ -84,6 +84,7 @@ async function runTemporalAnalyses(targetPath, options, pkgNames) {
84
84
  }
85
85
  {
86
86
  const PUBLISH_CONCURRENCY = 5;
87
+ const publishThreats = [];
87
88
  for (let i = 0; i < pkgNames.length; i += PUBLISH_CONCURRENCY) {
88
89
  const batch = pkgNames.slice(i, i + PUBLISH_CONCURRENCY);
89
90
  const results = await Promise.allSettled(
@@ -93,15 +94,38 @@ async function runTemporalAnalyses(targetPath, options, pkgNames) {
93
94
  if (r.status !== 'fulfilled' || !r.value.suspicious) continue;
94
95
  const det = r.value;
95
96
  for (const a of det.anomalies) {
96
- threats.push({
97
+ publishThreats.push({
97
98
  type: a.type,
98
99
  severity: a.severity,
99
100
  message: a.description,
100
- file: `node_modules/${det.packageName}/package.json`
101
+ file: `node_modules/${det.packageName}/package.json`,
102
+ _scope: det.packageName.startsWith('@') ? det.packageName.split('/')[0] : null
101
103
  });
102
104
  }
103
105
  }
104
106
  }
107
+
108
+ // P7: Scope-aware deduplication for monorepo releases.
109
+ // When 3+ packages from the same @scope trigger publish_burst or rapid_succession,
110
+ // it's a coordinated monorepo release (lerna, nx, turbo), not an attack.
111
+ // Downgrade all findings for that scope to LOW severity.
112
+ const MONOREPO_SCOPE_THRESHOLD = 3;
113
+ const scopeTypeCounts = new Map(); // key: `${scope}:${type}` → count
114
+ for (const t of publishThreats) {
115
+ if (!t._scope) continue;
116
+ const key = `${t._scope}:${t.type}`;
117
+ scopeTypeCounts.set(key, (scopeTypeCounts.get(key) || 0) + 1);
118
+ }
119
+ for (const t of publishThreats) {
120
+ if (t._scope) {
121
+ const key = `${t._scope}:${t.type}`;
122
+ if ((scopeTypeCounts.get(key) || 0) >= MONOREPO_SCOPE_THRESHOLD) {
123
+ t.severity = 'LOW';
124
+ }
125
+ }
126
+ delete t._scope; // clean up internal field
127
+ threats.push(t);
128
+ }
105
129
  }
106
130
  }
107
131