muaddib-scanner 2.11.85 → 2.11.86

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/bin/muaddib.js CHANGED
@@ -646,6 +646,19 @@ if (command === 'version' || command === '--version' || command === '-v') {
646
646
  console.log(` FP rate: ${globalFpRate}`);
647
647
  console.log('');
648
648
  process.exit(0);
649
+ } else if (command === 'fpr-live') {
650
+ const { runFprLive } = require('../src/commands/fpr-live.js');
651
+ const fprOpts = { json: jsonOutput };
652
+ for (let i = 0; i < options.length; i++) {
653
+ if (options[i] === '--since' && options[i + 1]) { fprOpts.since = options[i + 1]; i++; }
654
+ else if (options[i] === '--trend-days' && options[i + 1]) { fprOpts.trendDays = parseInt(options[i + 1], 10); i++; }
655
+ }
656
+ runFprLive(fprOpts).then(() => {
657
+ process.exit(0);
658
+ }).catch(err => {
659
+ console.error('[ERROR]', err.message);
660
+ process.exit(1);
661
+ });
649
662
  } else if (command === 'evaluate') {
650
663
  if (wantHelp) showHelp('evaluate');
651
664
  const { evaluate } = require('../src/commands/evaluate.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.11.85",
3
+ "version": "2.11.86",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "node_modules",
3
- "timestamp": "2026-06-10T23:09:20.823Z",
3
+ "timestamp": "2026-06-10T23:35:51.270Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -0,0 +1,218 @@
1
+ 'use strict';
2
+
3
+ // muaddib fpr-live — honest operational alert-rate / FPR report, computed entirely
4
+ // from data already on disk (no corpus, no re-download):
5
+ // - data/scan-stats.json : lifetime cumulative counters + per-day history
6
+ // - data/scan-ledger.jsonl : recent rolling window with per-package detail
7
+ // (outcome, score, firing-rule `types[]`, ecosystem)
8
+ //
9
+ // HONEST METRIC NOTE: `alertRate = alerted / scanned` is NOT the curated FPR (1.10%
10
+ // on famous packages with reputation suppression). On the live firehose, which is
11
+ // dominated by new / low-reputation packages, alertRate is the operational reality
12
+ // and — because confirmed malware is a vanishingly small fraction of alerts — it is
13
+ // a tight UPPER BOUND on the true FPR. Turning it into a precise FPR needs the
14
+ // alerts triaged into TP/FP (independent adjudication). We report it as what it is,
15
+ // never dressed up.
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const readline = require('readline');
20
+
21
+ const { computeLedgerRollup, SCAN_LEDGER_FILE } = require('../monitor/state.js');
22
+
23
+ const SCAN_STATS_FILE = path.join(__dirname, '..', '..', 'data', 'scan-stats.json');
24
+ const OUT_FILE = path.join(__dirname, '..', '..', 'data', 'fpr-live.json');
25
+
26
+ function _pct(n, d) {
27
+ if (!d || d <= 0) return null;
28
+ return n / d;
29
+ }
30
+ function _fmtPct(r) {
31
+ return r === null || r === undefined ? 'N/A' : (r * 100).toFixed(2) + '%';
32
+ }
33
+
34
+ function _loadScanStats() {
35
+ try {
36
+ return JSON.parse(fs.readFileSync(SCAN_STATS_FILE, 'utf8'));
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ // Streaming pass over the ledger for the two dimensions computeLedgerRollup does not
43
+ // carry: score buckets and the firing-rule histogram. Bounded memory (readline, plus
44
+ // a rule map whose key-count is bounded by the rule catalogue ~260).
45
+ async function _ledgerDetail(ledgerFile) {
46
+ const buckets = { '20-29': 0, '30-49': 0, '50-74': 0, '75-100': 0 };
47
+ const ruleCounts = Object.create(null);
48
+ let alertsScored = 0;
49
+ let alertsUnscored = 0;
50
+
51
+ if (!fs.existsSync(ledgerFile)) {
52
+ return { buckets, topRules: [], alertsScored, alertsUnscored, available: false };
53
+ }
54
+
55
+ const rl = readline.createInterface({
56
+ input: fs.createReadStream(ledgerFile, { encoding: 'utf8' }),
57
+ crlfDelay: Infinity
58
+ });
59
+
60
+ for await (const line of rl) {
61
+ if (!line) continue;
62
+ let e;
63
+ try { e = JSON.parse(line); } catch { continue; }
64
+ if (!e || e.outcome === 'dropped') continue;
65
+ // An "alert" is a suspect/confirmed outcome — the same numerator as alertRate.
66
+ if (e.outcome !== 'suspect' && e.outcome !== 'confirmed') continue;
67
+
68
+ const score = typeof e.score === 'number' ? e.score : null;
69
+ if (score === null) {
70
+ alertsUnscored++;
71
+ } else {
72
+ alertsScored++;
73
+ if (score >= 75) buckets['75-100']++;
74
+ else if (score >= 50) buckets['50-74']++;
75
+ else if (score >= 30) buckets['30-49']++;
76
+ else if (score >= 20) buckets['20-29']++;
77
+ // scores < 20 with a suspect outcome are anomalies; ignore for the FP buckets.
78
+ }
79
+ if (Array.isArray(e.types)) {
80
+ for (const t of e.types) ruleCounts[t] = (ruleCounts[t] || 0) + 1;
81
+ }
82
+ }
83
+
84
+ const topRules = Object.entries(ruleCounts)
85
+ .sort((a, b) => b[1] - a[1])
86
+ .slice(0, 15)
87
+ .map(([type, count]) => ({ type, count }));
88
+
89
+ return { buckets, topRules, alertsScored, alertsUnscored, available: true };
90
+ }
91
+
92
+ // Pure builder (no I/O side effects beyond reading) — returns the full report object.
93
+ async function buildFprLiveReport(opts = {}) {
94
+ const ledgerFile = opts.ledgerFile || SCAN_LEDGER_FILE;
95
+ const stats = _loadScanStats();
96
+
97
+ // 1. Lifetime + daily trend, from scan-stats.json cumulative counters.
98
+ let lifetime = null;
99
+ let trend = [];
100
+ if (stats && stats.stats) {
101
+ const s = stats.stats;
102
+ lifetime = {
103
+ total_scanned: s.total_scanned || 0,
104
+ suspect: s.suspect || 0,
105
+ confirmed_malicious: s.confirmed_malicious || 0,
106
+ // alertRate ≈ FPR upper bound: of everything scanned, the fraction flagged.
107
+ alertRate: _pct(s.suspect || 0, s.total_scanned || 0)
108
+ };
109
+ const days = Array.isArray(stats.daily) ? stats.daily : [];
110
+ const window = typeof opts.trendDays === 'number' ? opts.trendDays : 14;
111
+ trend = days.slice(-window).map(d => ({
112
+ date: d.date,
113
+ scanned: d.scanned || 0,
114
+ suspect: d.suspect || 0,
115
+ confirmed: d.confirmed || 0,
116
+ alertRate: _pct(d.suspect || 0, d.scanned || 0)
117
+ }));
118
+ }
119
+
120
+ // 2. Recent rolling window detail, from the ledger.
121
+ const rollup = computeLedgerRollup(opts.since || null, { file: ledgerFile });
122
+ const detail = await _ledgerDetail(ledgerFile);
123
+
124
+ return {
125
+ generatedAt: rollup.generatedAt,
126
+ note: 'alertRate is the operational alert load (alerted/scanned). It is an UPPER BOUND on FPR — not the curated 1.10%. Precise FPR requires alert triage (TP/FP labelling).',
127
+ lifetime,
128
+ trend,
129
+ recentWindow: {
130
+ windowStart: rollup.windowStart,
131
+ windowEnd: rollup.windowEnd,
132
+ scanned: rollup.scanned,
133
+ dropped: rollup.dropped,
134
+ alerted: rollup.alerted,
135
+ alertRate: rollup.alertRate,
136
+ byEcosystem: rollup.byEcosystem,
137
+ scoreBuckets: detail.buckets,
138
+ alertsScored: detail.alertsScored,
139
+ alertsUnscored: detail.alertsUnscored,
140
+ topFiringRules: detail.topRules
141
+ }
142
+ };
143
+ }
144
+
145
+ function _printReport(r) {
146
+ console.log('\n MUAD\'DIB — Operational FPR / Alert-Rate (live)\n');
147
+ console.log(' ' + r.note + '\n');
148
+
149
+ if (r.lifetime) {
150
+ const l = r.lifetime;
151
+ console.log(' Lifetime (scan-stats.json cumulative)');
152
+ console.log(' ' + '-'.repeat(58));
153
+ console.log(` Total scanned: ${l.total_scanned.toLocaleString()}`);
154
+ console.log(` Flagged (suspect ≥20): ${l.suspect.toLocaleString()}`);
155
+ console.log(` Confirmed malware: ${l.confirmed_malicious.toLocaleString()}`);
156
+ console.log(` Alert rate (≈FPR↑): ${_fmtPct(l.alertRate)}`);
157
+ console.log('');
158
+ }
159
+
160
+ if (r.trend && r.trend.length) {
161
+ console.log(' Daily trend (alert rate)');
162
+ console.log(' ' + '-'.repeat(58));
163
+ console.log(' Date Scanned Suspect Confirmed AlertRate');
164
+ for (const d of r.trend) {
165
+ console.log(
166
+ ` ${d.date} ${String(d.scanned).padStart(6)} ${String(d.suspect).padStart(6)} ` +
167
+ `${String(d.confirmed).padStart(8)} ${_fmtPct(d.alertRate).padStart(8)}`
168
+ );
169
+ }
170
+ console.log('');
171
+ }
172
+
173
+ const w = r.recentWindow;
174
+ console.log(' Recent window detail (scan-ledger.jsonl)');
175
+ console.log(' ' + '-'.repeat(58));
176
+ console.log(` Window: ${w.windowStart || 'n/a'} → ${w.windowEnd || 'n/a'}`);
177
+ console.log(` Scanned: ${w.scanned.toLocaleString()} (dropped: ${w.dropped.toLocaleString()})`);
178
+ console.log(` Alerted: ${w.alerted.toLocaleString()} Alert rate: ${_fmtPct(w.alertRate)}`);
179
+ console.log('');
180
+ console.log(' By ecosystem:');
181
+ for (const [eco, n] of Object.entries(w.byEcosystem)) {
182
+ const ar = _pct(n.alerted, n.scanned);
183
+ console.log(` ${eco.padEnd(8)} scanned=${String(n.scanned).padStart(6)} alerted=${String(n.alerted).padStart(6)} rate=${_fmtPct(ar)}`);
184
+ }
185
+ console.log('');
186
+ console.log(' Alert score distribution:');
187
+ for (const [b, n] of Object.entries(w.scoreBuckets)) {
188
+ console.log(` ${b.padEnd(7)} ${String(n).padStart(6)}`);
189
+ }
190
+ if (w.alertsUnscored) console.log(` (unscored alerts: ${w.alertsUnscored})`);
191
+ console.log('');
192
+ console.log(' Top firing rules on alerts (the FP-load drivers):');
193
+ for (const r2 of w.topFiringRules) {
194
+ console.log(` ${String(r2.count).padStart(6)} ${r2.type}`);
195
+ }
196
+ console.log('');
197
+ }
198
+
199
+ async function runFprLive(opts = {}) {
200
+ const report = await buildFprLiveReport(opts);
201
+
202
+ if (opts.json) {
203
+ console.log(JSON.stringify(report, null, 2));
204
+ } else {
205
+ _printReport(report);
206
+ }
207
+
208
+ // Persist for trend tracking / future anomaly detection (best-effort).
209
+ try {
210
+ fs.writeFileSync(OUT_FILE, JSON.stringify(report, null, 2));
211
+ } catch (e) {
212
+ if (!opts.json) console.log(' [warn] could not persist fpr-live.json: ' + e.message);
213
+ }
214
+
215
+ return report;
216
+ }
217
+
218
+ module.exports = { runFprLive, buildFprLiveReport };