muaddib-scanner 1.7.0 → 1.8.1

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/README.fr.md CHANGED
@@ -283,7 +283,7 @@ Ajoutez à `.pre-commit-config.yaml` :
283
283
  ```yaml
284
284
  repos:
285
285
  - repo: https://github.com/DNSZLSK/muad-dib
286
- rev: v1.6.11
286
+ rev: v1.8.0
287
287
  hooks:
288
288
  - id: muaddib-scan # Scanner toutes les menaces
289
289
  # - id: muaddib-diff # Ou: seulement les nouvelles
@@ -313,6 +313,10 @@ muaddib init-hooks --type git
313
313
  # Crée .git/hooks/pre-commit
314
314
  ```
315
315
 
316
+ ### Moniteur Zero-Day
317
+
318
+ MUAD'DIB intègre un moniteur zero-day capable de scanner chaque nouveau package npm et PyPI en temps réel avec analyse sandbox Docker et alertes webhook.
319
+
316
320
  ### Version check
317
321
 
318
322
  MUAD'DIB vérifie automatiquement les nouvelles versions au démarrage et vous notifie si une mise à jour est disponible.
@@ -504,6 +508,7 @@ MUAD'DIB Scanner
504
508
  +-- GitHub Actions Scanner
505
509
  +-- Paranoid Mode (ultra-strict)
506
510
  +-- Docker Sandbox (behavioral analysis, network capture)
511
+ +-- Moniteur Zero-Day (polling RSS npm + PyPI, alertes Discord, rapport quotidien)
507
512
  |
508
513
  v
509
514
  Dataflow Analysis (credential read -> network send)
@@ -547,9 +552,10 @@ npm test
547
552
 
548
553
  ### Tests
549
554
 
550
- - **296 tests unitaires/intégration** - 80% coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
555
+ - **326 tests unitaires/intégration** - 80% coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
551
556
  - **56 tests de fuzzing** - YAML malformé, JSON invalide, fichiers binaires, ReDoS, unicode, inputs 10MB
552
557
  - **15 tests adversariaux** - Packages malveillants simulés, taux de détection 15/15
558
+ - **8 tests multi-facteur typosquat** - Cas limites et comportement cache
553
559
  - **Audit ESLint sécurité** - `eslint-plugin-security` avec 14 règles activées
554
560
 
555
561
  ---
package/README.md CHANGED
@@ -283,7 +283,7 @@ Add to `.pre-commit-config.yaml`:
283
283
  ```yaml
284
284
  repos:
285
285
  - repo: https://github.com/DNSZLSK/muad-dib
286
- rev: v1.6.18
286
+ rev: v1.8.0
287
287
  hooks:
288
288
  - id: muaddib-scan # Scan all threats
289
289
  # - id: muaddib-diff # Or: only new threats
@@ -313,6 +313,10 @@ muaddib init-hooks --type git
313
313
  # Creates .git/hooks/pre-commit
314
314
  ```
315
315
 
316
+ ### Zero-Day Monitor
317
+
318
+ MUAD'DIB includes a continuous zero-day monitor capable of scanning every new npm and PyPI package in real-time with Docker sandbox analysis and webhook alerting.
319
+
316
320
  ### Version check
317
321
 
318
322
  MUAD'DIB automatically checks for new versions on startup and notifies you if an update is available.
@@ -510,6 +514,7 @@ MUAD'DIB Scanner
510
514
  |
511
515
  +-- Paranoid Mode (ultra-strict)
512
516
  +-- Docker Sandbox (behavioral analysis, network capture)
517
+ +-- Zero-Day Monitor (npm + PyPI RSS polling, Discord alerts, daily report)
513
518
  |
514
519
  v
515
520
  Dataflow Analysis (credential read -> network send)
@@ -553,9 +558,10 @@ npm test
553
558
 
554
559
  ### Testing
555
560
 
556
- - **316 unit/integration tests** - 80% code coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
561
+ - **326 unit/integration tests** - 80% code coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
557
562
  - **56 fuzz tests** - Malformed YAML, invalid JSON, binary files, ReDoS, unicode, 10MB inputs
558
563
  - **15 adversarial tests** - Simulated malicious packages, 15/15 detection rate
564
+ - **8 multi-factor typosquat tests** - Edge cases and cache behavior
559
565
  - **False positive validation** - 0 false positives on express, lodash, axios, react
560
566
  - **ESLint security audit** - `eslint-plugin-security` with 14 rules enabled
561
567
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -5,7 +5,7 @@ const AdmZip = require('adm-zip');
5
5
 
6
6
  const IOC_FILE = path.join(__dirname, 'data/iocs.json');
7
7
  const COMPACT_IOC_FILE = path.join(__dirname, 'data/iocs-compact.json');
8
- const STATIC_IOCS_FILE = path.join(__dirname, 'data/static-iocs.json');
8
+ const STATIC_IOCS_FILE = path.join(__dirname, '../../data/static-iocs.json');
9
9
  const { generateCompactIOCs } = require('./updater.js');
10
10
  const { Spinner } = require('../utils.js');
11
11
 
package/src/monitor.js CHANGED
@@ -21,9 +21,13 @@ const stats = {
21
21
  suspect: 0,
22
22
  errors: 0,
23
23
  totalTimeMs: 0,
24
- lastReportTime: Date.now()
24
+ lastReportTime: Date.now(),
25
+ lastDailyReportTime: Date.now()
25
26
  };
26
27
 
28
+ // Track daily suspects for the daily report (name, version, ecosystem, findingsCount)
29
+ const dailyAlerts = [];
30
+
27
31
  // --- Scan queue (FIFO, sequential) ---
28
32
 
29
33
  const scanQueue = [];
@@ -76,17 +80,43 @@ function buildMonitorWebhookPayload(name, version, ecosystem, result, sandboxRes
76
80
  return payload;
77
81
  }
78
82
 
83
+ function computeRiskLevel(summary) {
84
+ if (summary.critical > 0) return 'CRITICAL';
85
+ if (summary.high > 0) return 'HIGH';
86
+ if (summary.medium > 0) return 'MEDIUM';
87
+ if (summary.low > 0) return 'LOW';
88
+ return 'CLEAN';
89
+ }
90
+
91
+ function computeRiskScore(summary) {
92
+ const raw = (summary.critical || 0) * 25
93
+ + (summary.high || 0) * 15
94
+ + (summary.medium || 0) * 5
95
+ + (summary.low || 0) * 1;
96
+ return Math.min(raw, 100);
97
+ }
98
+
79
99
  async function trySendWebhook(name, version, ecosystem, result, sandboxResult) {
80
100
  if (!shouldSendWebhook(result, sandboxResult)) return;
81
101
  const url = getWebhookUrl();
82
102
  const payload = buildMonitorWebhookPayload(name, version, ecosystem, result, sandboxResult);
83
- // sendWebhook expects a results-like object; wrap payload for formatGeneric
84
103
  const webhookData = {
85
104
  target: `${ecosystem}/${name}@${version}`,
86
105
  timestamp: payload.timestamp,
87
- summary: result.summary,
106
+ ecosystem,
107
+ summary: {
108
+ ...result.summary,
109
+ riskLevel: computeRiskLevel(result.summary),
110
+ riskScore: computeRiskScore(result.summary)
111
+ },
88
112
  threats: result.threats
89
113
  };
114
+ if (sandboxResult && sandboxResult.score > 0) {
115
+ webhookData.sandbox = {
116
+ score: sandboxResult.score,
117
+ severity: sandboxResult.severity
118
+ };
119
+ }
90
120
  try {
91
121
  await sendWebhook(url, webhookData);
92
122
  console.log(`[MONITOR] Webhook sent for ${name}@${version}`);
@@ -102,11 +132,11 @@ function loadState() {
102
132
  const raw = fs.readFileSync(STATE_FILE, 'utf8');
103
133
  const state = JSON.parse(raw);
104
134
  return {
105
- npmLastKey: typeof state.npmLastKey === 'number' ? state.npmLastKey : 0,
135
+ npmLastPackage: typeof state.npmLastPackage === 'string' ? state.npmLastPackage : '',
106
136
  pypiLastPackage: typeof state.pypiLastPackage === 'string' ? state.pypiLastPackage : ''
107
137
  };
108
138
  } catch {
109
- return { npmLastKey: 0, pypiLastPackage: '' };
139
+ return { npmLastPackage: '', pypiLastPackage: '' };
110
140
  }
111
141
  }
112
142
 
@@ -264,6 +294,19 @@ function appendAlert(alert) {
264
294
  }
265
295
  }
266
296
 
297
+ // --- Bundled tooling false-positive filter ---
298
+
299
+ const KNOWN_BUNDLED_FILES = ['yarn.js', 'webpack.js', 'terser.js', 'esbuild.js', 'polyfills.js'];
300
+
301
+ function isBundledToolingOnly(threats) {
302
+ if (threats.length === 0) return false;
303
+ return threats.every(t => {
304
+ if (!t.file) return false;
305
+ const basename = path.basename(t.file);
306
+ return KNOWN_BUNDLED_FILES.includes(basename);
307
+ });
308
+ }
309
+
267
310
  // --- Package scanning ---
268
311
 
269
312
  async function scanPackage(name, version, ecosystem, tarballUrl) {
@@ -294,53 +337,78 @@ async function scanPackage(name, version, ecosystem, tarballUrl) {
294
337
  stats.clean++;
295
338
  console.log(`[MONITOR] CLEAN: ${name}@${version} (0 findings, ${(elapsed / 1000).toFixed(1)}s)`);
296
339
  } else {
297
- stats.suspect++;
298
340
  const counts = [];
299
341
  if (result.summary.critical > 0) counts.push(`${result.summary.critical} CRITICAL`);
300
342
  if (result.summary.high > 0) counts.push(`${result.summary.high} HIGH`);
301
343
  if (result.summary.medium > 0) counts.push(`${result.summary.medium} MEDIUM`);
302
344
  if (result.summary.low > 0) counts.push(`${result.summary.low} LOW`);
303
- console.log(`[MONITOR] SUSPECT: ${name}@${version} (${counts.join(', ')})`);
304
-
305
- // Sandbox: run dynamic analysis on HIGH/CRITICAL findings
306
- let sandboxResult = null;
307
- if (hasHighOrCritical(result) && isSandboxEnabled() && sandboxAvailable) {
308
- try {
309
- console.log(`[MONITOR] SANDBOX: launching for ${name}@${version}...`);
310
- sandboxResult = await runSandbox(name);
311
- console.log(`[MONITOR] SANDBOX: ${name}@${version} → score: ${sandboxResult.score}, severity: ${sandboxResult.severity}`);
312
- } catch (err) {
313
- console.error(`[MONITOR] SANDBOX error for ${name}@${version}: ${err.message}`);
314
- }
315
- }
316
345
 
317
- stats.scanned++;
318
- const elapsed = Date.now() - startTime;
319
- stats.totalTimeMs += elapsed;
320
- console.log(`[MONITOR] ${name}@${version} total time: ${(elapsed / 1000).toFixed(1)}s`);
346
+ // Check if all findings come from bundled tooling files
347
+ if (isBundledToolingOnly(result.threats)) {
348
+ stats.scanned++;
349
+ const elapsed = Date.now() - startTime;
350
+ stats.totalTimeMs += elapsed;
351
+ stats.clean++;
352
+ console.log(`[MONITOR] SKIPPED (bundled tooling): ${name}@${version} (${counts.join(', ')})`);
353
+
354
+ const alert = {
355
+ timestamp: new Date().toISOString(),
356
+ name,
357
+ version,
358
+ ecosystem,
359
+ skipped: true,
360
+ findings: result.threats.map(t => ({
361
+ rule: t.rule_id || t.type,
362
+ severity: t.severity,
363
+ file: t.file
364
+ }))
365
+ };
366
+ appendAlert(alert);
367
+ } else {
368
+ stats.suspect++;
369
+ console.log(`[MONITOR] SUSPECT: ${name}@${version} (${counts.join(', ')})`);
370
+
371
+ // Sandbox: run dynamic analysis on HIGH/CRITICAL findings
372
+ let sandboxResult = null;
373
+ if (hasHighOrCritical(result) && isSandboxEnabled() && sandboxAvailable) {
374
+ try {
375
+ console.log(`[MONITOR] SANDBOX: launching for ${name}@${version}...`);
376
+ sandboxResult = await runSandbox(name);
377
+ console.log(`[MONITOR] SANDBOX: ${name}@${version} → score: ${sandboxResult.score}, severity: ${sandboxResult.severity}`);
378
+ } catch (err) {
379
+ console.error(`[MONITOR] SANDBOX error for ${name}@${version}: ${err.message}`);
380
+ }
381
+ }
321
382
 
322
- const alert = {
323
- timestamp: new Date().toISOString(),
324
- name,
325
- version,
326
- ecosystem,
327
- findings: result.threats.map(t => ({
328
- rule: t.rule_id || t.type,
329
- severity: t.severity,
330
- file: t.file
331
- }))
332
- };
333
-
334
- if (sandboxResult && sandboxResult.score > 0) {
335
- alert.sandbox = {
336
- score: sandboxResult.score,
337
- severity: sandboxResult.severity,
338
- findings: sandboxResult.findings
383
+ stats.scanned++;
384
+ const elapsed = Date.now() - startTime;
385
+ stats.totalTimeMs += elapsed;
386
+ console.log(`[MONITOR] ${name}@${version} total time: ${(elapsed / 1000).toFixed(1)}s`);
387
+
388
+ const alert = {
389
+ timestamp: new Date().toISOString(),
390
+ name,
391
+ version,
392
+ ecosystem,
393
+ findings: result.threats.map(t => ({
394
+ rule: t.rule_id || t.type,
395
+ severity: t.severity,
396
+ file: t.file
397
+ }))
339
398
  };
340
- }
341
399
 
342
- appendAlert(alert);
343
- await trySendWebhook(name, version, ecosystem, result, sandboxResult);
400
+ if (sandboxResult && sandboxResult.score > 0) {
401
+ alert.sandbox = {
402
+ score: sandboxResult.score,
403
+ severity: sandboxResult.severity,
404
+ findings: sandboxResult.findings
405
+ };
406
+ }
407
+
408
+ appendAlert(alert);
409
+ dailyAlerts.push({ name, version, ecosystem, findingsCount: result.summary.total });
410
+ await trySendWebhook(name, version, ecosystem, result, sandboxResult);
411
+ }
344
412
  }
345
413
  } catch (err) {
346
414
  stats.errors++;
@@ -364,7 +432,7 @@ async function processQueue() {
364
432
  const item = scanQueue.shift();
365
433
  try {
366
434
  await Promise.race([
367
- scanPackage(item.name, item.version, item.ecosystem, item.tarballUrl),
435
+ resolveTarballAndScan(item),
368
436
  timeoutPromise(SCAN_TIMEOUT_MS)
369
437
  ]);
370
438
  } catch (err) {
@@ -382,71 +450,140 @@ function reportStats() {
382
450
  stats.lastReportTime = Date.now();
383
451
  }
384
452
 
385
- // --- npm polling ---
453
+ const DAILY_REPORT_INTERVAL = 24 * 3600_000; // 24 hours
386
454
 
387
- /**
388
- * Parse the npm /-/all/since response.
389
- * Returns array of { name, version, tarball } and the max timestamp seen.
390
- */
391
- function parseNpmResponse(body) {
392
- let data;
455
+ function buildDailyReportEmbed() {
456
+ const avg = stats.scanned > 0 ? (stats.totalTimeMs / stats.scanned / 1000).toFixed(1) : '0.0';
457
+
458
+ // Top 3 suspects sorted by findings count descending
459
+ const top3 = dailyAlerts
460
+ .slice()
461
+ .sort((a, b) => b.findingsCount - a.findingsCount)
462
+ .slice(0, 3);
463
+
464
+ const top3Text = top3.length > 0
465
+ ? top3.map((a, i) => `${i + 1}. **${a.ecosystem}/${a.name}@${a.version}** — ${a.findingsCount} finding(s)`).join('\n')
466
+ : 'None';
467
+
468
+ const now = new Date();
469
+ const readableTime = now.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
470
+
471
+ return {
472
+ embeds: [{
473
+ title: '\uD83D\uDCCA MUAD\'DIB Daily Report',
474
+ color: 0x3498db,
475
+ fields: [
476
+ { name: 'Packages Scanned', value: `${stats.scanned}`, inline: true },
477
+ { name: 'Clean', value: `${stats.clean}`, inline: true },
478
+ { name: 'Suspects', value: `${stats.suspect}`, inline: true },
479
+ { name: 'Errors', value: `${stats.errors}`, inline: true },
480
+ { name: 'Avg Scan Time', value: `${avg}s/pkg`, inline: true },
481
+ { name: 'Top Suspects', value: top3Text, inline: false }
482
+ ],
483
+ footer: {
484
+ text: `MUAD'DIB - Daily summary | ${readableTime}`
485
+ },
486
+ timestamp: now.toISOString()
487
+ }]
488
+ };
489
+ }
490
+
491
+ async function sendDailyReport() {
492
+ const url = getWebhookUrl();
493
+ if (!url) return;
494
+
495
+ const payload = buildDailyReportEmbed();
393
496
  try {
394
- data = JSON.parse(body);
395
- } catch {
396
- return { packages: [], maxTimestamp: 0 };
497
+ await sendWebhook(url, payload, { rawPayload: true });
498
+ console.log('[MONITOR] Daily report sent');
499
+ } catch (err) {
500
+ console.error(`[MONITOR] Daily report webhook failed: ${err.message}`);
397
501
  }
398
502
 
503
+ // Reset daily counters
504
+ stats.scanned = 0;
505
+ stats.clean = 0;
506
+ stats.suspect = 0;
507
+ stats.errors = 0;
508
+ stats.totalTimeMs = 0;
509
+ dailyAlerts.length = 0;
510
+ stats.lastDailyReportTime = Date.now();
511
+ }
512
+
513
+ // --- npm polling ---
514
+
515
+ /**
516
+ * Parse npm RSS XML (same regex approach as parsePyPIRss).
517
+ * Returns array of package names from <title> tags inside <item>.
518
+ */
519
+ function parseNpmRss(xml) {
399
520
  const packages = [];
400
- let maxTimestamp = 0;
401
-
402
- // The response is an object keyed by package name.
403
- // Each value has name, "dist-tags", time, etc.
404
- // There is a special "_updated" key with the latest timestamp.
405
- for (const key of Object.keys(data)) {
406
- if (key === '_updated') {
407
- const ts = Number(data[key]);
408
- if (ts > maxTimestamp) maxTimestamp = ts;
409
- continue;
521
+ const itemRegex = /<item>([\s\S]*?)<\/item>/g;
522
+ let match;
523
+ while ((match = itemRegex.exec(xml)) !== null) {
524
+ const itemContent = match[1];
525
+ const titleMatch = itemContent.match(/<title>([^<]+)<\/title>/);
526
+ if (titleMatch) {
527
+ const title = titleMatch[1].trim();
528
+ const name = title.split(/\s+/)[0];
529
+ if (name) {
530
+ packages.push(name);
531
+ }
410
532
  }
411
- const pkg = data[key];
412
- if (!pkg || typeof pkg !== 'object' || !pkg.name) continue;
413
- const version = (pkg['dist-tags'] && pkg['dist-tags'].latest) || '';
414
- const tarball = (pkg.dist && pkg.dist.tarball) || '';
415
- packages.push({ name: pkg.name, version, tarball });
416
533
  }
534
+ return packages;
535
+ }
417
536
 
418
- return { packages, maxTimestamp };
537
+ /**
538
+ * Fetch latest version metadata for an npm package.
539
+ * Returns { version, tarball } or null on failure.
540
+ */
541
+ async function getNpmLatestTarball(packageName) {
542
+ const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
543
+ const body = await httpsGet(url);
544
+ const data = JSON.parse(body);
545
+ const version = data.version || '';
546
+ const tarball = (data.dist && data.dist.tarball) || null;
547
+ return { version, tarball };
419
548
  }
420
549
 
421
550
  async function pollNpm(state) {
422
- // First run: use "now - 120s" so we don't get the entire registry
423
- const startKey = state.npmLastKey || (Date.now() - 120_000);
424
- const url = `https://registry.npmjs.org/-/all/since?stale=update_after&startkey=${startKey}`;
551
+ const url = 'https://registry.npmjs.org/-/rss?descending=true&limit=50';
425
552
 
426
553
  try {
427
554
  const body = await httpsGet(url);
428
- const { packages, maxTimestamp } = parseNpmResponse(body);
429
-
430
- for (const pkg of packages) {
431
- console.log(`[MONITOR] New npm: ${pkg.name}@${pkg.version}`);
432
- if (pkg.tarball) {
433
- scanQueue.push({
434
- name: pkg.name,
435
- version: pkg.version,
436
- ecosystem: 'npm',
437
- tarballUrl: pkg.tarball
438
- });
555
+ const packages = parseNpmRss(body);
556
+
557
+ // Find new packages (those after the last seen one)
558
+ let newPackages;
559
+ if (!state.npmLastPackage) {
560
+ newPackages = packages;
561
+ } else {
562
+ const lastIdx = packages.indexOf(state.npmLastPackage);
563
+ if (lastIdx === -1) {
564
+ newPackages = packages;
565
+ } else {
566
+ newPackages = packages.slice(0, lastIdx);
439
567
  }
440
568
  }
441
569
 
442
- if (maxTimestamp > 0) {
443
- state.npmLastKey = maxTimestamp;
444
- } else if (packages.length > 0) {
445
- // Fallback: advance timestamp to now
446
- state.npmLastKey = Date.now();
570
+ for (const name of newPackages) {
571
+ console.log(`[MONITOR] New npm: ${name}`);
572
+ // Queue npm packages tarball URL resolved during scan
573
+ scanQueue.push({
574
+ name,
575
+ version: '',
576
+ ecosystem: 'npm',
577
+ tarballUrl: null // resolved lazily via resolveTarballAndScan
578
+ });
579
+ }
580
+
581
+ // Remember the most recent package (first in RSS)
582
+ if (packages.length > 0) {
583
+ state.npmLastPackage = packages[0];
447
584
  }
448
585
 
449
- return packages.length;
586
+ return newPackages.length;
450
587
  } catch (err) {
451
588
  console.error(`[MONITOR] npm poll error: ${err.message}`);
452
589
  return 0;
@@ -550,7 +687,7 @@ async function startMonitor() {
550
687
  }
551
688
 
552
689
  const state = loadState();
553
- console.log(`[MONITOR] State loaded — npm startKey: ${state.npmLastKey || 'none'}, pypi last: ${state.pypiLastPackage || 'none'}`);
690
+ console.log(`[MONITOR] State loaded — npm last: ${state.npmLastPackage || 'none'}, pypi last: ${state.pypiLastPackage || 'none'}`);
554
691
  console.log(`[MONITOR] Polling every ${POLL_INTERVAL / 1000}s. Ctrl+C to stop.\n`);
555
692
 
556
693
  let running = true;
@@ -582,6 +719,11 @@ async function startMonitor() {
582
719
  if (Date.now() - stats.lastReportTime >= 3600_000) {
583
720
  reportStats();
584
721
  }
722
+
723
+ // Daily webhook report
724
+ if (Date.now() - stats.lastDailyReportTime >= DAILY_REPORT_INTERVAL) {
725
+ await sendDailyReport();
726
+ }
585
727
  }
586
728
  }
587
729
 
@@ -603,6 +745,21 @@ async function poll(state) {
603
745
  * For PyPI packages, we need to fetch the JSON API to get the tarball URL.
604
746
  */
605
747
  async function resolveTarballAndScan(item) {
748
+ if (item.ecosystem === 'npm' && !item.tarballUrl) {
749
+ try {
750
+ const npmInfo = await getNpmLatestTarball(item.name);
751
+ if (!npmInfo.tarball) {
752
+ console.log(`[MONITOR] SKIP: ${item.name} — no tarball URL found on npm`);
753
+ return;
754
+ }
755
+ item.tarballUrl = npmInfo.tarball;
756
+ if (npmInfo.version) item.version = npmInfo.version;
757
+ } catch (err) {
758
+ console.error(`[MONITOR] ERROR resolving npm tarball for ${item.name}: ${err.message}`);
759
+ stats.errors++;
760
+ return;
761
+ }
762
+ }
606
763
  if (item.ecosystem === 'pypi' && !item.tarballUrl) {
607
764
  try {
608
765
  const pypiInfo = await getPyPITarballUrl(item.name);
@@ -627,7 +784,7 @@ function sleep(ms) {
627
784
 
628
785
  module.exports = {
629
786
  startMonitor,
630
- parseNpmResponse,
787
+ parseNpmRss,
631
788
  parsePyPIRss,
632
789
  loadState,
633
790
  saveState,
@@ -636,6 +793,7 @@ module.exports = {
636
793
  downloadToFile,
637
794
  extractTarGz,
638
795
  getNpmTarballUrl,
796
+ getNpmLatestTarball,
639
797
  getPyPITarballUrl,
640
798
  scanPackage,
641
799
  scanQueue,
@@ -644,8 +802,11 @@ module.exports = {
644
802
  timeoutPromise,
645
803
  reportStats,
646
804
  stats,
805
+ dailyAlerts,
647
806
  resolveTarballAndScan,
648
807
  MAX_TARBALL_SIZE,
808
+ KNOWN_BUNDLED_FILES,
809
+ isBundledToolingOnly,
649
810
  isSandboxEnabled,
650
811
  hasHighOrCritical,
651
812
  get sandboxAvailable() { return sandboxAvailable; },
@@ -653,7 +814,12 @@ module.exports = {
653
814
  getWebhookUrl,
654
815
  shouldSendWebhook,
655
816
  buildMonitorWebhookPayload,
656
- trySendWebhook
817
+ trySendWebhook,
818
+ computeRiskLevel,
819
+ computeRiskScore,
820
+ buildDailyReportEmbed,
821
+ sendDailyReport,
822
+ DAILY_REPORT_INTERVAL
657
823
  };
658
824
 
659
825
  // Standalone entry point: node src/monitor.js
@@ -125,6 +125,13 @@ const PLAYBOOKS = {
125
125
  ssh_key_read:
126
126
  'Lecture des cles SSH. Regenerer immediatement toutes les cles: ssh-keygen -t ed25519',
127
127
 
128
+ python_reverse_shell:
129
+ 'CRITIQUE: Reverse shell Python detecte. Machine potentiellement compromise. Isoler immediatement.',
130
+ perl_reverse_shell:
131
+ 'CRITIQUE: Reverse shell Perl detecte. Machine potentiellement compromise. Isoler immediatement.',
132
+ fifo_reverse_shell:
133
+ 'CRITIQUE: Reverse shell FIFO/named pipe detecte. Machine potentiellement compromise. Isoler immediatement.',
134
+
128
135
  shai_hulud_backdoor:
129
136
  'CRITIQUE: Backdoor Shai-Hulud dans GitHub Actions. Supprimer le workflow et auditer les runs precedents.',
130
137
 
@@ -347,6 +347,33 @@ const RULES = {
347
347
  references: ['https://attack.mitre.org/techniques/T1552/004/'],
348
348
  mitre: 'T1552.004'
349
349
  },
350
+ python_reverse_shell: {
351
+ id: 'MUADDIB-SHELL-010',
352
+ name: 'Python Reverse Shell',
353
+ severity: 'CRITICAL',
354
+ confidence: 'high',
355
+ description: 'Reverse shell via python -c import socket detecte',
356
+ references: ['https://attack.mitre.org/techniques/T1059/004/'],
357
+ mitre: 'T1059.006'
358
+ },
359
+ perl_reverse_shell: {
360
+ id: 'MUADDIB-SHELL-011',
361
+ name: 'Perl Reverse Shell',
362
+ severity: 'CRITICAL',
363
+ confidence: 'high',
364
+ description: 'Reverse shell via perl -e socket detecte',
365
+ references: ['https://attack.mitre.org/techniques/T1059/004/'],
366
+ mitre: 'T1059.006'
367
+ },
368
+ fifo_reverse_shell: {
369
+ id: 'MUADDIB-SHELL-012',
370
+ name: 'FIFO Reverse Shell',
371
+ severity: 'CRITICAL',
372
+ confidence: 'high',
373
+ description: 'Reverse shell via mkfifo /dev/tcp detecte',
374
+ references: ['https://attack.mitre.org/techniques/T1059/004/'],
375
+ mitre: 'T1059.004'
376
+ },
350
377
 
351
378
  // AST additional patterns
352
379
  possible_obfuscation: {