muaddib-scanner 2.10.96 → 2.10.97
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/feature-extractor.js +5 -1
- package/src/pipeline/processor.js +19 -1
- package/src/scoring.js +103 -1
package/package.json
CHANGED
|
@@ -696,7 +696,11 @@ function extractFeatures(result, meta) {
|
|
|
696
696
|
features.typosquat_scoped_package = typosquatScopedPackage(result, meta) ? 1 : 0;
|
|
697
697
|
features.obfuscation_without_vector = obfuscationWithoutVector(result) ? 1 : 0;
|
|
698
698
|
features.placeholder_anti_dep_confusion = placeholderAntiDepConfusion(result, meta) ? 1 : 0;
|
|
699
|
-
|
|
699
|
+
// F8 disabled for retrain — fires on malware due to incomplete EGRESS_TYPES
|
|
700
|
+
// (missing dangerous_exec, lifecycle_dangerous_exec, node_inline_exec).
|
|
701
|
+
// Re-enable in v2.10.97 after EGRESS_TYPES fix + re-validation.
|
|
702
|
+
// See ml-retrain/ml-auc-v2.10.96.md for details.
|
|
703
|
+
features.install_script_no_network_egress = 0; // installScriptNoNetworkEgress(result, meta) ? 1 : 0;
|
|
700
704
|
|
|
701
705
|
return features;
|
|
702
706
|
}
|
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const { getRule } = require('../rules/index.js');
|
|
4
4
|
const { getPlaybook } = require('../response/playbooks.js');
|
|
5
5
|
const { computeReachableFiles } = require('../scanner/reachability.js');
|
|
6
|
-
const { applyFPReductions, applyCompoundBoosts, calculateRiskScore, getSeverityWeights } = require('../scoring.js');
|
|
6
|
+
const { applyFPReductions, applyCompoundBoosts, calculateRiskScore, getSeverityWeights, applyContextualFPCaps } = require('../scoring.js');
|
|
7
7
|
const { buildIntentPairs } = require('../intent-graph.js');
|
|
8
8
|
const { debugLog } = require('../utils.js');
|
|
9
9
|
|
|
@@ -100,12 +100,21 @@ async function process(threats, targetPath, options, pythonDeps, warnings, scann
|
|
|
100
100
|
// Read package name and dependencies for FP reduction heuristics
|
|
101
101
|
let packageName = null;
|
|
102
102
|
let packageDeps = null;
|
|
103
|
+
let _pkgMeta = null; // v2.10.97: full pkg metadata for contextual FP caps
|
|
103
104
|
try {
|
|
104
105
|
const pkgPath = path.join(targetPath, 'package.json');
|
|
105
106
|
if (fs.existsSync(pkgPath)) {
|
|
106
107
|
const pkgData = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
107
108
|
packageName = pkgData.name || null;
|
|
108
109
|
packageDeps = pkgData.dependencies || null;
|
|
110
|
+
_pkgMeta = {
|
|
111
|
+
name: pkgData.name,
|
|
112
|
+
scripts: pkgData.scripts || {},
|
|
113
|
+
description: pkgData.description || '',
|
|
114
|
+
homepage: pkgData.homepage || (typeof pkgData.repository === 'string' ? pkgData.repository : (pkgData.repository && pkgData.repository.url) || ''),
|
|
115
|
+
dependencies: pkgData.dependencies,
|
|
116
|
+
devDependencies: pkgData.devDependencies,
|
|
117
|
+
};
|
|
109
118
|
}
|
|
110
119
|
} catch { /* graceful fallback */ }
|
|
111
120
|
|
|
@@ -301,6 +310,15 @@ async function process(threats, targetPath, options, pythonDeps, warnings, scann
|
|
|
301
310
|
scannerErrors: scannerErrors.length > 0 ? scannerErrors : undefined
|
|
302
311
|
};
|
|
303
312
|
|
|
313
|
+
// v2.10.97: contextual FP post-filter — deterministic score caps for
|
|
314
|
+
// packages matching well-known FP clusters (100% precision, 302 human labels).
|
|
315
|
+
const fpCaps = applyContextualFPCaps(result, _pkgMeta);
|
|
316
|
+
if (fpCaps.length > 0) {
|
|
317
|
+
debugLog('[FP-CAP] ' + (packageName || targetPath) + ': ' +
|
|
318
|
+
fpCaps.map(c => c.feature + (c.cap > 0 ? '→MAX' + c.cap : '→suppress')).join(', ') +
|
|
319
|
+
' → score=' + result.summary.riskScore);
|
|
320
|
+
}
|
|
321
|
+
|
|
304
322
|
return {
|
|
305
323
|
result,
|
|
306
324
|
deduped,
|
package/src/scoring.js
CHANGED
|
@@ -1011,8 +1011,110 @@ function calculateRiskScore(deduped, intentResult) {
|
|
|
1011
1011
|
};
|
|
1012
1012
|
}
|
|
1013
1013
|
|
|
1014
|
+
// ============================================
|
|
1015
|
+
// v2.10.97: CONTEXTUAL FP POST-FILTER
|
|
1016
|
+
// ============================================
|
|
1017
|
+
// Deterministic score caps for packages matching well-known FP clusters.
|
|
1018
|
+
// Each feature has 100% precision on 302 human-reviewed packages (zero
|
|
1019
|
+
// malware misclassified). Applied AFTER calculateRiskScore() so that
|
|
1020
|
+
// compound boosts and lifecycle floors have already had their say.
|
|
1021
|
+
const {
|
|
1022
|
+
bundleWithoutInstallScripts,
|
|
1023
|
+
installUrlGithubReleases,
|
|
1024
|
+
networkDestinationFirstParty,
|
|
1025
|
+
gitHookSourceLocal,
|
|
1026
|
+
typosquatScopedPackage,
|
|
1027
|
+
obfuscationWithoutVector,
|
|
1028
|
+
placeholderAntiDepConfusion,
|
|
1029
|
+
} = require('./ml/feature-extractor.js');
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Apply contextual FP score caps to a scan result.
|
|
1033
|
+
* Mutates result.summary.riskScore / riskLevel in-place.
|
|
1034
|
+
* Returns array of { feature, cap } describing applied caps (empty if none).
|
|
1035
|
+
*/
|
|
1036
|
+
function applyContextualFPCaps(result, pkgMeta) {
|
|
1037
|
+
if (!result || !result.summary) return [];
|
|
1038
|
+
|
|
1039
|
+
const meta = {
|
|
1040
|
+
name: pkgMeta && pkgMeta.name,
|
|
1041
|
+
registryMeta: {
|
|
1042
|
+
scripts: (pkgMeta && pkgMeta.scripts) || {},
|
|
1043
|
+
description: (pkgMeta && pkgMeta.description) || '',
|
|
1044
|
+
homepage: (pkgMeta && pkgMeta.homepage) || '',
|
|
1045
|
+
dependencies: (pkgMeta && pkgMeta.dependencies),
|
|
1046
|
+
devDependencies: (pkgMeta && pkgMeta.devDependencies),
|
|
1047
|
+
},
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
const applied = [];
|
|
1051
|
+
|
|
1052
|
+
// F7: placeholder anti-dep-confusion → MAX 20
|
|
1053
|
+
if (placeholderAntiDepConfusion(result, meta)) {
|
|
1054
|
+
applied.push({ feature: 'placeholder_anti_dep_confusion', cap: 20 });
|
|
1055
|
+
}
|
|
1056
|
+
// F1: minified bundle without install scripts → MAX 30
|
|
1057
|
+
if (bundleWithoutInstallScripts(result, meta)) {
|
|
1058
|
+
applied.push({ feature: 'bundle_without_install_scripts', cap: 30 });
|
|
1059
|
+
}
|
|
1060
|
+
// F3: credential destination first-party API → MAX 30
|
|
1061
|
+
if (networkDestinationFirstParty(result, meta)) {
|
|
1062
|
+
applied.push({ feature: 'network_destination_first_party', cap: 30 });
|
|
1063
|
+
}
|
|
1064
|
+
// F2: binary installer from GitHub Releases → MAX 35
|
|
1065
|
+
if (installUrlGithubReleases(result)) {
|
|
1066
|
+
applied.push({ feature: 'install_url_github_releases', cap: 35 });
|
|
1067
|
+
}
|
|
1068
|
+
// F4: git hooks from local source → MAX 35
|
|
1069
|
+
if (gitHookSourceLocal(result)) {
|
|
1070
|
+
applied.push({ feature: 'git_hook_source_local', cap: 35 });
|
|
1071
|
+
}
|
|
1072
|
+
// F6: commercial obfuscation without attack vector → MAX 35
|
|
1073
|
+
if (obfuscationWithoutVector(result)) {
|
|
1074
|
+
applied.push({ feature: 'obfuscation_without_vector', cap: 35 });
|
|
1075
|
+
}
|
|
1076
|
+
// F5: typosquat on scoped package → suppress typosquat points
|
|
1077
|
+
if (typosquatScopedPackage(result, meta)) {
|
|
1078
|
+
applied.push({ feature: 'typosquat_scoped_package', cap: -1 });
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (applied.length === 0) return applied;
|
|
1082
|
+
|
|
1083
|
+
// Apply the tightest (lowest) cap
|
|
1084
|
+
const caps = applied.filter(a => a.cap > 0);
|
|
1085
|
+
const lowestCap = caps.length > 0 ? Math.min(...caps.map(a => a.cap)) : Infinity;
|
|
1086
|
+
|
|
1087
|
+
if (lowestCap < result.summary.riskScore) {
|
|
1088
|
+
result.summary.riskScore = lowestCap;
|
|
1089
|
+
result.summary.riskLevel =
|
|
1090
|
+
lowestCap >= _riskThresholds.CRITICAL ? 'CRITICAL'
|
|
1091
|
+
: lowestCap >= _riskThresholds.HIGH ? 'HIGH'
|
|
1092
|
+
: lowestCap >= _riskThresholds.MEDIUM ? 'MEDIUM'
|
|
1093
|
+
: lowestCap > 0 ? 'LOW' : 'SAFE';
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// F5: subtract typosquat points from score
|
|
1097
|
+
if (applied.find(a => a.feature === 'typosquat_scoped_package')) {
|
|
1098
|
+
const typoPoints = result.threats
|
|
1099
|
+
.filter(t => t.type === 'typosquat_detected' || t.type === 'lifecycle_typosquat')
|
|
1100
|
+
.reduce((s, t) => s + (t.points || 0), 0);
|
|
1101
|
+
if (typoPoints > 0) {
|
|
1102
|
+
result.summary.riskScore = Math.max(0, result.summary.riskScore - typoPoints);
|
|
1103
|
+
const rs = result.summary.riskScore;
|
|
1104
|
+
result.summary.riskLevel =
|
|
1105
|
+
rs >= _riskThresholds.CRITICAL ? 'CRITICAL'
|
|
1106
|
+
: rs >= _riskThresholds.HIGH ? 'HIGH'
|
|
1107
|
+
: rs >= _riskThresholds.MEDIUM ? 'MEDIUM'
|
|
1108
|
+
: rs > 0 ? 'LOW' : 'SAFE';
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return applied;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1014
1115
|
module.exports = {
|
|
1015
1116
|
SEVERITY_WEIGHTS, RISK_THRESHOLDS, MAX_RISK_SCORE, CONFIDENCE_FACTORS,
|
|
1016
1117
|
isPackageLevelThreat, computeGroupScore, applyFPReductions, applyCompoundBoosts, calculateRiskScore,
|
|
1017
|
-
applyConfigOverrides, resetConfigOverrides, getSeverityWeights, getRiskThresholds
|
|
1118
|
+
applyConfigOverrides, resetConfigOverrides, getSeverityWeights, getRiskThresholds,
|
|
1119
|
+
applyContextualFPCaps
|
|
1018
1120
|
};
|