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 +1 -1
- package/src/ml/classifier.js +18 -19
- package/src/monitor/queue.js +18 -0
package/package.json
CHANGED
package/src/ml/classifier.js
CHANGED
|
@@ -126,16 +126,20 @@ function resetShadowModel() {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
* Run shadow model prediction and log
|
|
130
|
-
*
|
|
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
|
|
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 ===
|
|
158
|
-
_shadowStats.agree++;
|
|
159
|
-
} else {
|
|
162
|
+
if (shadowPred === 'malicious') {
|
|
160
163
|
_shadowStats.disagree++;
|
|
161
|
-
console.log(`[ML-SHADOW]
|
|
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
|
|
167
|
-
console.log(`[ML-SHADOW] Stats: ${_shadowStats.total} total, ${
|
|
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
|
|
402
|
+
// Shadow model (ML1 v2, log-only prediction)
|
|
405
403
|
isShadowModelAvailable,
|
|
406
404
|
resetShadowModel,
|
|
407
405
|
loadShadowModel,
|
|
406
|
+
runShadowPrediction,
|
|
408
407
|
getShadowStats
|
|
409
408
|
};
|
package/src/monitor/queue.js
CHANGED
|
@@ -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
|