muaddib-scanner 2.10.69 → 2.10.70

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.10.69",
3
+ "version": "2.10.70",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -126,16 +126,20 @@ function resetShadowModel() {
126
126
  }
127
127
 
128
128
  /**
129
- * Run shadow model prediction and log comparison with main model.
130
- * Never affects the actual classification decision.
129
+ * Run shadow model prediction and log result.
130
+ * NEVER affects the actual classification decision — log-only.
131
+ *
132
+ * Runs independently of the main model's guard rails so that shadow
133
+ * predictions are logged for ALL packages with score >= 20, not just
134
+ * T1 zone. This provides validation data across the full score range
135
+ * before the shadow model is promoted to production.
131
136
  *
132
137
  * @param {Object} result - scan result
133
138
  * @param {Object} meta - enriched metadata
134
- * @param {string} mainPrediction - the main model's prediction
135
- * @param {number} mainProbability - the main model's probability
136
139
  * @param {string} packageName - for logging
140
+ * @param {number} score - risk score (for log context)
137
141
  */
138
- function runShadowComparison(result, meta, mainPrediction, mainProbability, packageName) {
142
+ function runShadowPrediction(result, meta, packageName, score) {
139
143
  const shadow = loadShadowModel();
140
144
  if (!shadow) return;
141
145
 
@@ -151,20 +155,21 @@ function runShadowComparison(result, meta, mainPrediction, mainProbability, pack
151
155
  }
152
156
 
153
157
  const shadowProb = sigmoid(margin);
158
+ const roundedP = Math.round(shadowProb * 1000) / 1000;
154
159
  const shadowPred = shadowProb >= shadow.threshold ? 'malicious' : 'clean';
155
160
 
156
161
  _shadowStats.total++;
157
- if (shadowPred === mainPrediction) {
158
- _shadowStats.agree++;
159
- } else {
162
+ if (shadowPred === 'malicious') {
160
163
  _shadowStats.disagree++;
161
- console.log(`[ML-SHADOW] Disagreement on ${packageName}: main=${mainPrediction}(${mainProbability}) shadow=${shadowPred}(${Math.round(shadowProb * 1000) / 1000}) [${_shadowStats.disagree}/${_shadowStats.total} disagree]`);
164
+ console.log(`[ML-SHADOW] ${packageName} → ${shadowPred} (p=${roundedP}, score=${score}) [${_shadowStats.disagree}/${_shadowStats.total} flagged]`);
165
+ } else {
166
+ _shadowStats.agree++;
162
167
  }
163
168
 
164
169
  // Periodic summary every 100 classifications
165
170
  if (_shadowStats.total % 100 === 0) {
166
- const agreeRate = ((_shadowStats.agree / _shadowStats.total) * 100).toFixed(1);
167
- console.log(`[ML-SHADOW] Stats: ${_shadowStats.total} total, ${agreeRate}% agree, ${_shadowStats.disagree} disagree`);
171
+ const flagRate = ((_shadowStats.disagree / _shadowStats.total) * 100).toFixed(1);
172
+ console.log(`[ML-SHADOW] Stats: ${_shadowStats.total} total, ${_shadowStats.disagree} flagged (${flagRate}%), ${_shadowStats.agree} clean`);
168
173
  }
169
174
  }
170
175
 
@@ -371,13 +376,6 @@ function classifyPackage(result, meta) {
371
376
 
372
377
  const roundedProb = Math.round(probability * 1000) / 1000;
373
378
 
374
- // Shadow model comparison (log-only, never affects decision)
375
- if (isShadowModelAvailable()) {
376
- const pkgName = (result && result.summary && result.summary.packageName) ||
377
- (meta && meta.name) || 'unknown';
378
- runShadowComparison(result, meta, prediction, roundedProb, pkgName);
379
- }
380
-
381
379
  return {
382
380
  prediction,
383
381
  probability: roundedProb,
@@ -401,9 +399,10 @@ module.exports = {
401
399
  loadBundlerModel,
402
400
  predictBundler,
403
401
  buildBundlerFeatureVector,
404
- // Shadow model (ML1 v2, log-only comparison)
402
+ // Shadow model (ML1 v2, log-only prediction)
405
403
  isShadowModelAvailable,
406
404
  resetShadowModel,
407
405
  loadShadowModel,
406
+ runShadowPrediction,
408
407
  getShadowStats
409
408
  };
@@ -647,6 +647,24 @@ async function scanPackage(name, version, ecosystem, tarballUrl, registryMeta, s
647
647
  }
648
648
  }
649
649
 
650
+ // Shadow model: log-only prediction for ALL score >= 20 npm packages.
651
+ // Runs independently of classifyPackage — no effect on mlResult, webhooks,
652
+ // or any decisions. Collects shadow validation data for the retrained model.
653
+ if (riskScore >= 20 && ecosystem === 'npm') {
654
+ try {
655
+ const { isShadowModelAvailable, runShadowPrediction } = require('../ml/classifier.js');
656
+ if (isShadowModelAvailable()) {
657
+ const shadowMeta = { npmRegistryMeta, fileCountTotal, hasTests, unpackedSize: meta.unpackedSize, registryMeta: meta };
658
+ runShadowPrediction(result, shadowMeta, `${name}@${version}`, riskScore);
659
+ }
660
+ } catch (err) {
661
+ // Non-fatal: shadow failure must never block the pipeline
662
+ if (err.code !== 'MODULE_NOT_FOUND') {
663
+ console.error(`[ML-SHADOW] Error for ${name}@${version}: ${err.message}`);
664
+ }
665
+ }
666
+ }
667
+
650
668
  stats.suspect++;
651
669
 
652
670
  // Fire-and-forget tarball archiving — never blocks the pipeline