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 +28 -0
- package/package.json +1 -1
- package/src/monitor.js +156 -1
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
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
|