muaddib-scanner 2.10.28 → 2.10.29

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.28",
3
+ "version": "2.10.29",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -863,10 +863,10 @@ async function resolveTarballAndScan(item, stats, dailyAlerts, recentlyScanned,
863
863
  // Run all 4 temporal checks in parallel — each is independent.
864
864
  // With metadata cache (temporal-analysis.js), the 4 modules share 1 HTTP request.
865
865
  const [tempRes, astRes, pubRes, maintRes] = await Promise.allSettled([
866
- runTemporalCheck(item.name),
867
- runTemporalAstCheck(item.name),
868
- runTemporalPublishCheck(item.name),
869
- runTemporalMaintainerCheck(item.name)
866
+ runTemporalCheck(item.name, dailyAlerts),
867
+ runTemporalAstCheck(item.name, dailyAlerts),
868
+ runTemporalPublishCheck(item.name, dailyAlerts),
869
+ runTemporalMaintainerCheck(item.name, dailyAlerts)
870
870
  ]);
871
871
  temporalResult = tempRes.status === 'fulfilled' ? tempRes.value : null;
872
872
  astResult = astRes.status === 'fulfilled' ? astRes.value : null;
@@ -744,6 +744,37 @@ function buildCanaryExfiltrationWebhookEmbed(packageName, version, exfiltrations
744
744
  * @param {Object} stats - In-memory stats object (scanned, clean, suspect, errors, errorsByType, totalTimeMs, suspectByTier, mlFiltered)
745
745
  * @param {Array} dailyAlerts - In-memory daily alerts array
746
746
  */
747
+ /**
748
+ * Load yesterday's persisted report metrics for J-1 comparison.
749
+ * @returns {Object|null} yesterday's raw metrics or null if unavailable
750
+ */
751
+ function loadYesterdayMetrics() {
752
+ try {
753
+ // Use Paris timezone to match persistDailyReport() which uses getParisDateString()
754
+ const todayParis = getParisDateString(); // YYYY-MM-DD in Europe/Paris
755
+ const [y, m, d] = todayParis.split('-').map(Number);
756
+ const yesterday = new Date(y, m - 1, d);
757
+ yesterday.setDate(yesterday.getDate() - 1);
758
+ const yStr = yesterday.toISOString().slice(0, 10);
759
+ const filePath = path.join(DAILY_REPORTS_LOG_DIR, `${yStr}.json`);
760
+ if (!fs.existsSync(filePath)) return null;
761
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
762
+ return data.metrics || null;
763
+ } catch {
764
+ return null;
765
+ }
766
+ }
767
+
768
+ /**
769
+ * Format a delta with sign: "+1200" or "-50" or "=0"
770
+ */
771
+ function formatDelta(current, previous) {
772
+ const d = current - previous;
773
+ if (d > 0) return `+${d}`;
774
+ if (d < 0) return `${d}`;
775
+ return '=0';
776
+ }
777
+
747
778
  function buildDailyReportEmbed(stats, dailyAlerts) {
748
779
  // Use in-memory stats (accumulated since last reset, restored from disk on restart)
749
780
  // instead of disk-based daily entries which can undercount due to UTC/Paris date mismatch
@@ -766,6 +797,56 @@ function buildDailyReportEmbed(stats, dailyAlerts) {
766
797
  // Avg scan time from in-memory stats
767
798
  const avg = stats.scanned > 0 ? (stats.totalTimeMs / stats.scanned / 1000).toFixed(1) : '0.0';
768
799
 
800
+ // --- Coverage estimation ---
801
+ // changesStreamPackages = total versions seen from npm changes stream (≈ published today)
802
+ const published = stats.changesStreamPackages || 0;
803
+ const coverageText = published > 0
804
+ ? `${stats.scanned}/${published} (${(stats.scanned / published * 100).toFixed(0)}%)`
805
+ : `${stats.scanned} scanned`;
806
+
807
+ // --- Timeouts ---
808
+ const staticTimeouts = (stats.errorsByType && stats.errorsByType.static_timeout) || 0;
809
+ const httpTimeouts = (stats.errorsByType && stats.errorsByType.timeout) || 0;
810
+ const timeoutPct = stats.scanned > 0 ? (staticTimeouts / stats.scanned * 100) : 0;
811
+ const timeoutWarning = timeoutPct > 15 ? ' \u26a0\ufe0f' : '';
812
+ const timeoutText = `Static: ${staticTimeouts}/${stats.scanned} (${timeoutPct.toFixed(1)}%)${timeoutWarning}\nHTTP: ${httpTimeouts}`;
813
+
814
+ // --- J-1 trends ---
815
+ const yesterday = loadYesterdayMetrics();
816
+ let trendsText = 'No data (first day or missing)';
817
+ if (yesterday) {
818
+ const dScanned = formatDelta(stats.scanned, yesterday.scanned || 0);
819
+ const dSuspect = formatDelta(stats.suspect, yesterday.suspect || 0);
820
+ const dErrors = formatDelta(stats.errors, yesterday.errors || 0);
821
+ trendsText = `${dScanned} scanned, ${dSuspect} suspects, ${dErrors} errors`;
822
+ }
823
+
824
+ // --- ML stats ---
825
+ let mlText;
826
+ try {
827
+ const { isModelAvailable } = require('../ml/classifier.js');
828
+ if (isModelAvailable()) {
829
+ mlText = stats.mlFiltered > 0 ? `${stats.mlFiltered} filtered` : '0 filtered';
830
+ } else {
831
+ mlText = 'No model loaded';
832
+ }
833
+ } catch {
834
+ mlText = 'No model loaded';
835
+ }
836
+
837
+ // --- System health ---
838
+ const uptimeSec = Math.floor(process.uptime());
839
+ const uptimeH = Math.floor(uptimeSec / 3600);
840
+ const uptimeM = Math.floor((uptimeSec % 3600) / 60);
841
+ const heapMB = (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(0);
842
+ let jsonlInfo = '';
843
+ try {
844
+ const { getStats: getTrainingStats } = require('../ml/jsonl-writer.js');
845
+ const jStats = getTrainingStats();
846
+ jsonlInfo = ` | JSONL: ${jStats.recordCount} records (${jStats.fileSizeMB}MB)`;
847
+ } catch { /* non-fatal */ }
848
+ const healthText = `Up ${uptimeH}h${uptimeM}m | Heap ${heapMB}MB${jsonlInfo}`;
849
+
769
850
  const now = new Date();
770
851
  const readableTime = now.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
771
852
 
@@ -774,13 +855,16 @@ function buildDailyReportEmbed(stats, dailyAlerts) {
774
855
  title: '\uD83D\uDCCA MUAD\'DIB Daily Report',
775
856
  color: 0x3498db,
776
857
  fields: [
777
- { name: 'Packages Scanned', value: `${stats.scanned}`, inline: true },
858
+ { name: 'Coverage', value: coverageText, inline: true },
778
859
  { name: 'Clean', value: `${stats.clean}`, inline: true },
779
860
  { name: 'Suspects', value: `${stats.suspect}`, inline: true },
780
861
  { name: 'Errors', value: formatErrorBreakdown(stats.errors, stats.errorsByType), inline: true },
781
862
  { name: 'Avg Scan Time', value: `${avg}s/pkg`, inline: true },
782
- ...(stats.mlFiltered > 0 ? [{ name: 'ML Filtered', value: `${stats.mlFiltered}`, inline: true }] : []),
783
- { name: 'Top Suspects', value: top3Text, inline: false }
863
+ { name: 'Timeouts', value: timeoutText, inline: true },
864
+ { name: 'vs Yesterday', value: trendsText, inline: false },
865
+ { name: 'ML', value: mlText, inline: true },
866
+ { name: 'Top Suspects', value: top3Text, inline: false },
867
+ { name: 'System', value: healthText, inline: false }
784
868
  ],
785
869
  footer: {
786
870
  text: `MUAD'DIB - Daily summary | ${readableTime}`
@@ -822,6 +906,8 @@ async function sendDailyReport(stats, dailyAlerts, recentlyScanned, downloadsCac
822
906
  errorsByType: { ...stats.errorsByType },
823
907
  avgScanTimeMs: stats.scanned > 0 ? Math.round(stats.totalTimeMs / stats.scanned) : 0,
824
908
  suspectByTier: { ...stats.suspectByTier },
909
+ mlFiltered: stats.mlFiltered || 0,
910
+ changesStreamPackages: stats.changesStreamPackages || 0,
825
911
  topSuspects: dailyAlerts.slice().sort((a, b) => b.findingsCount - a.findingsCount).slice(0, 10)
826
912
  });
827
913
 
@@ -856,6 +942,8 @@ async function sendDailyReport(stats, dailyAlerts, recentlyScanned, downloadsCac
856
942
  stats.errorsByType.other = 0;
857
943
  stats.totalTimeMs = 0;
858
944
  stats.mlFiltered = 0;
945
+ stats.changesStreamPackages = 0;
946
+ stats.rssFallbackCount = 0;
859
947
  dailyAlerts.length = 0;
860
948
  recentlyScanned.clear();
861
949
  alertedPackageRules.clear();
@@ -1051,5 +1139,7 @@ module.exports = {
1051
1139
  buildReportFromDisk,
1052
1140
  buildReportEmbedFromDisk,
1053
1141
  sendReportNow,
1054
- getReportStatus
1142
+ getReportStatus,
1143
+ loadYesterdayMetrics,
1144
+ formatDelta
1055
1145
  };