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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.10.96",
3
+ "version": "2.10.97",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
- features.install_script_no_network_egress = installScriptNoNetworkEgress(result, meta) ? 1 : 0;
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
  };