muaddib-scanner 2.2.16 → 2.2.17

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
@@ -812,6 +812,34 @@ if (command === 'version' || command === '--version' || command === '-v') {
812
812
  console.error('[ERROR]', err.message);
813
813
  process.exit(1);
814
814
  });
815
+ } else if (command === 'report') {
816
+ // Hidden/internal — not in --help
817
+ if (options.includes('--now')) {
818
+ const { sendReportNow } = require('../src/monitor.js');
819
+ sendReportNow().then(result => {
820
+ if (result.sent) {
821
+ console.log(`\n \x1b[32m\u2713\x1b[0m ${result.message}\n`);
822
+ } else {
823
+ console.log(`\n \x1b[33m!\x1b[0m ${result.message}\n`);
824
+ }
825
+ process.exit(result.sent ? 0 : 1);
826
+ }).catch(err => {
827
+ console.error('[ERROR]', err.message);
828
+ process.exit(1);
829
+ });
830
+ } else if (options.includes('--status')) {
831
+ const { getReportStatus } = require('../src/monitor.js');
832
+ const status = getReportStatus();
833
+ console.log('\n MUAD\'DIB Report Status\n');
834
+ console.log(` Last report sent: ${status.lastDailyReportDate || 'Never'}`);
835
+ console.log(` Packages scanned since: ${status.scannedSince}`);
836
+ console.log(` Next scheduled report: ${status.nextReport}`);
837
+ console.log('');
838
+ process.exit(0);
839
+ } else {
840
+ console.log('Usage: muaddib report --now | --status');
841
+ process.exit(1);
842
+ }
815
843
  } else if (command === 'help') {
816
844
  console.log(helpText);
817
845
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.2.16",
3
+ "version": "2.2.17",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/monitor.js CHANGED
@@ -1119,6 +1119,157 @@ async function sendDailyReport() {
1119
1119
  stats.lastDailyReportDate = getParisDateString();
1120
1120
  }
1121
1121
 
1122
+ // --- CLI report helpers (muaddib report --now / --status) ---
1123
+
1124
+ /**
1125
+ * Read raw state file (without restoring into stats).
1126
+ */
1127
+ function loadStateRaw() {
1128
+ try {
1129
+ const raw = fs.readFileSync(STATE_FILE, 'utf8');
1130
+ return JSON.parse(raw);
1131
+ } catch {
1132
+ return {};
1133
+ }
1134
+ }
1135
+
1136
+ /**
1137
+ * Reconstruct daily report data from persisted files (no in-memory stats needed).
1138
+ * Used by `muaddib report --now` to send a report from a separate CLI process.
1139
+ */
1140
+ function buildReportFromDisk() {
1141
+ const scanData = loadScanStats();
1142
+ const stateRaw = loadStateRaw();
1143
+ const lastDate = stateRaw.lastDailyReportDate || null;
1144
+
1145
+ // Filter daily entries since last report
1146
+ const sinceDays = lastDate
1147
+ ? scanData.daily.filter(d => d.date > lastDate)
1148
+ : scanData.daily;
1149
+
1150
+ // Aggregate counters
1151
+ const agg = { scanned: 0, clean: 0, suspect: 0 };
1152
+ for (const d of sinceDays) {
1153
+ agg.scanned += d.scanned || 0;
1154
+ agg.clean += d.clean || 0;
1155
+ agg.suspect += d.suspect || 0;
1156
+ }
1157
+
1158
+ // Load detections since last report for top suspects
1159
+ const detections = loadDetections();
1160
+ const recentDetections = lastDate
1161
+ ? detections.detections.filter(d => d.first_seen_at && d.first_seen_at.slice(0, 10) > lastDate)
1162
+ : detections.detections;
1163
+
1164
+ const top3 = recentDetections
1165
+ .slice()
1166
+ .sort((a, b) => (b.findings ? b.findings.length : 0) - (a.findings ? a.findings.length : 0))
1167
+ .slice(0, 3);
1168
+
1169
+ return { agg, top3, hasData: agg.scanned > 0 };
1170
+ }
1171
+
1172
+ /**
1173
+ * Build a Discord embed from disk data (same format as buildDailyReportEmbed).
1174
+ */
1175
+ function buildReportEmbedFromDisk() {
1176
+ const { agg, top3, hasData } = buildReportFromDisk();
1177
+ if (!hasData) return null;
1178
+
1179
+ const top3Text = top3.length > 0
1180
+ ? top3.map((a, i) => `${i + 1}. **${a.ecosystem}/${a.package}@${a.version}** — ${a.findings ? a.findings.length : 0} finding(s)`).join('\n')
1181
+ : 'None';
1182
+
1183
+ const now = new Date();
1184
+ const readableTime = now.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
1185
+
1186
+ return {
1187
+ embeds: [{
1188
+ title: '\uD83D\uDCCA MUAD\'DIB Daily Report (manual)',
1189
+ color: 0x3498db,
1190
+ fields: [
1191
+ { name: 'Packages Scanned', value: `${agg.scanned}`, inline: true },
1192
+ { name: 'Clean', value: `${agg.clean}`, inline: true },
1193
+ { name: 'Suspects', value: `${agg.suspect}`, inline: true },
1194
+ { name: 'Top Suspects', value: top3Text, inline: false }
1195
+ ],
1196
+ footer: {
1197
+ text: `MUAD'DIB - Manual report | ${readableTime}`
1198
+ },
1199
+ timestamp: now.toISOString()
1200
+ }]
1201
+ };
1202
+ }
1203
+
1204
+ /**
1205
+ * Force send a daily report from persisted data.
1206
+ * Returns { sent: boolean, message: string }.
1207
+ */
1208
+ async function sendReportNow() {
1209
+ const url = getWebhookUrl();
1210
+ if (!url) {
1211
+ return { sent: false, message: 'MUADDIB_WEBHOOK_URL not configured' };
1212
+ }
1213
+
1214
+ const payload = buildReportEmbedFromDisk();
1215
+ if (!payload) {
1216
+ return { sent: false, message: 'No data to report' };
1217
+ }
1218
+
1219
+ try {
1220
+ await sendWebhook(url, payload, { rawPayload: true });
1221
+ } catch (err) {
1222
+ return { sent: false, message: `Webhook failed: ${err.message}` };
1223
+ }
1224
+
1225
+ // Update lastDailyReportDate on disk
1226
+ const stateRaw = loadStateRaw();
1227
+ const state = {
1228
+ npmLastPackage: stateRaw.npmLastPackage || '',
1229
+ pypiLastPackage: stateRaw.pypiLastPackage || ''
1230
+ };
1231
+ stats.lastDailyReportDate = getParisDateString();
1232
+ saveState(state);
1233
+
1234
+ return { sent: true, message: 'Daily report sent' };
1235
+ }
1236
+
1237
+ /**
1238
+ * Get report status for `muaddib report --status`.
1239
+ */
1240
+ function getReportStatus() {
1241
+ const stateRaw = loadStateRaw();
1242
+ const lastDate = stateRaw.lastDailyReportDate || null;
1243
+
1244
+ // Count packages scanned since last report
1245
+ const scanData = loadScanStats();
1246
+ const sinceDays = lastDate
1247
+ ? scanData.daily.filter(d => d.date > lastDate)
1248
+ : scanData.daily;
1249
+
1250
+ let scannedSince = 0;
1251
+ for (const d of sinceDays) {
1252
+ scannedSince += d.scanned || 0;
1253
+ }
1254
+
1255
+ // Compute next report time
1256
+ const today = getParisDateString();
1257
+ const parisHour = getParisHour();
1258
+ let nextReport;
1259
+ if (lastDate === today || (lastDate !== today && parisHour >= DAILY_REPORT_HOUR)) {
1260
+ // Already sent today OR past 08:00 but not sent (will fire soon if monitor runs)
1261
+ if (lastDate === today) {
1262
+ nextReport = 'Tomorrow 08:00 (Europe/Paris)';
1263
+ } else {
1264
+ nextReport = 'Today 08:00 (Europe/Paris) — pending, monitor must be running';
1265
+ }
1266
+ } else {
1267
+ nextReport = 'Today 08:00 (Europe/Paris)';
1268
+ }
1269
+
1270
+ return { lastDailyReportDate: lastDate, scannedSince, nextReport };
1271
+ }
1272
+
1122
1273
  // --- npm polling ---
1123
1274
 
1124
1275
  /**
@@ -1588,7 +1739,11 @@ module.exports = {
1588
1739
  getDetectionStats,
1589
1740
  SCAN_STATS_FILE,
1590
1741
  loadScanStats,
1591
- updateScanStats
1742
+ updateScanStats,
1743
+ buildReportFromDisk,
1744
+ buildReportEmbedFromDisk,
1745
+ sendReportNow,
1746
+ getReportStatus
1592
1747
  };
1593
1748
 
1594
1749
  // Standalone entry point: node src/monitor.js