muaddib-scanner 2.2.23 → 2.2.25

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
@@ -285,7 +285,7 @@ Ajoutez à `.pre-commit-config.yaml` :
285
285
  ```yaml
286
286
  repos:
287
287
  - repo: https://github.com/DNSZLSK/muad-dib
288
- rev: v2.2.21
288
+ rev: v2.2.24
289
289
  hooks:
290
290
  - id: muaddib-scan # Scanner toutes les menaces
291
291
  # - id: muaddib-diff # Ou: seulement les nouvelles
@@ -640,7 +640,7 @@ Les alertes apparaissent dans Security > Code scanning alerts.
640
640
  ## Architecture
641
641
 
642
642
  ```
643
- MUAD'DIB 2.2.21 Scanner
643
+ MUAD'DIB 2.2.24 Scanner
644
644
  |
645
645
  +-- IOC Match (225 000+ packages, JSON DB)
646
646
  | +-- OSV.dev npm dump (200K+ entrées MAL-*)
@@ -748,7 +748,7 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
748
748
  - **ADR** (Adversarial Detection Rate) : taux de detection sur 78 samples malveillants evasifs — 38 adversariaux (4 vagues red team + 3 bypasses) + 40 holdouts (5 batches de 10, testant obfuscation, dataflow inter-module, etc.)
749
749
  - **Holdout** (pre-tuning) : taux de detection sur 10 samples jamais vus avec regles gelees (mesure de generalisation)
750
750
 
751
- Datasets : 529 npm + 132 PyPI packages benins, 78 samples adversariaux/holdout, 51 attaques ground-truth (65 packages malveillants documentes).
751
+ Datasets : 529 npm + 132 PyPI packages benins, 78 samples adversariaux/holdout, 51 attaques ground-truth (65 packages malveillants documentes). **1317 tests**, 86% coverage.
752
752
 
753
753
  Voir [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) pour le protocole experimental complet.
754
754
 
@@ -784,7 +784,7 @@ npm test
784
784
 
785
785
  ### Tests
786
786
 
787
- - **862 tests unitaires/integration** sur 20 fichiers modulaires - 74% coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
787
+ - **1317 tests unitaires/integration** sur 20 fichiers modulaires - 86% coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
788
788
  - **56 tests de fuzzing** - YAML malforme, JSON invalide, fichiers binaires, ReDoS, unicode, inputs 10MB
789
789
  - **78 samples adversariaux/holdout** - 38 adversariaux + 40 holdouts, 78/78 taux de detection (100% ADR)
790
790
  - **Validation ground truth** - 51 attaques reelles (45/49 detectees = 91.8% TPR). 4 hors scope : browser-only (3) + risque FP (1)
package/README.md CHANGED
@@ -285,7 +285,7 @@ Add to `.pre-commit-config.yaml`:
285
285
  ```yaml
286
286
  repos:
287
287
  - repo: https://github.com/DNSZLSK/muad-dib
288
- rev: v2.2.21
288
+ rev: v2.2.24
289
289
  hooks:
290
290
  - id: muaddib-scan # Scan all threats
291
291
  # - id: muaddib-diff # Or: only new threats
@@ -641,7 +641,7 @@ Alerts appear in Security > Code scanning alerts.
641
641
  ## Architecture
642
642
 
643
643
  ```
644
- MUAD'DIB 2.2.21 Scanner
644
+ MUAD'DIB 2.2.24 Scanner
645
645
  |
646
646
  +-- IOC Match (225,000+ packages, JSON DB)
647
647
  | +-- OSV.dev npm dump (200K+ MAL-* entries)
@@ -751,7 +751,7 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
751
751
  - **ADR** (Adversarial Detection Rate): detection rate on 75 evasive malicious samples — 35 adversarial (4 red-team waves) + 40 holdout (5 batches of 10, testing obfuscation, inter-module dataflow, etc.)
752
752
  - **Holdout** (pre-tuning): detection rate on 10 unseen samples with rules frozen (measures generalization)
753
753
 
754
- Datasets: 529 npm + 132 PyPI benign packages, 78 adversarial/holdout samples, 51 ground-truth attacks (65 documented malware packages).
754
+ Datasets: 529 npm + 132 PyPI benign packages, 78 adversarial/holdout samples, 51 ground-truth attacks (65 documented malware packages). **1317 tests**, 86% code coverage.
755
755
 
756
756
  See [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) for the full experimental protocol.
757
757
 
@@ -787,7 +787,7 @@ npm test
787
787
 
788
788
  ### Testing
789
789
 
790
- - **862 unit/integration tests** across 20 modular test files - 74% code coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
790
+ - **1317 unit/integration tests** across 20 modular test files - 86% code coverage via [Codecov](https://codecov.io/gh/DNSZLSK/muad-dib)
791
791
  - **56 fuzz tests** - Malformed YAML, invalid JSON, binary files, ReDoS, unicode, 10MB inputs
792
792
  - **78 adversarial/holdout samples** - 38 adversarial + 40 holdout, 78/78 detection rate (100% ADR)
793
793
  - **Ground truth validation** - 51 real-world attacks (45/49 detected = 91.8% TPR). 4 out-of-scope: browser-only (3) + FP-risky (1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.2.23",
3
+ "version": "2.2.25",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/daemon.js CHANGED
@@ -1,178 +1,178 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { run } = require('./index.js');
4
-
5
- let webhookUrl = null;
6
-
7
- async function startDaemon(options = {}) {
8
- webhookUrl = options.webhook || null;
9
-
10
- console.log(`
11
- ╔════════════════════════════════════════════╗
12
- ║ MUAD'DIB Security Daemon ║
13
- ║ Surveillance npm install active ║
14
- ╚════════════════════════════════════════════╝
15
- `);
16
-
17
- console.log('[DAEMON] Demarrage...');
18
- console.log(`[DAEMON] Webhook: ${webhookUrl ? 'Configure' : 'Non configure'}`);
19
- console.log('[DAEMON] Ctrl+C pour arreter\n');
20
-
21
- // Surveille le dossier courant
22
- const cwd = process.cwd();
23
- const watchers = watchDirectory(cwd);
24
-
25
- // Cleanup function to close all watchers
26
- function cleanup() {
27
- for (const w of watchers) {
28
- try { w.close(); } catch { /* ignore */ }
29
- }
30
- }
31
-
32
- // Keep process alive until SIGINT
33
- await new Promise((resolve) => {
34
- process.once('SIGINT', () => {
35
- console.log('\n[DAEMON] Arret...');
36
- cleanup();
37
- resolve();
38
- });
39
- });
40
-
41
- process.exit(0);
42
- }
43
-
44
- function watchDirectory(dir) {
45
- const watchers = [];
46
- const nodeModulesPath = path.join(dir, 'node_modules');
47
- const packageLockPath = path.join(dir, 'package-lock.json');
48
- const yarnLockPath = path.join(dir, 'yarn.lock');
49
-
50
- console.log(`[DAEMON] Surveillance de ${dir}`);
51
-
52
- // Surveille package-lock.json
53
- if (fs.existsSync(packageLockPath)) {
54
- const w = watchFile(packageLockPath, dir);
55
- if (w) watchers.push(w);
56
- }
57
-
58
- // Surveille yarn.lock
59
- if (fs.existsSync(yarnLockPath)) {
60
- const w = watchFile(yarnLockPath, dir);
61
- if (w) watchers.push(w);
62
- }
63
-
64
- // Surveille node_modules
65
- if (fs.existsSync(nodeModulesPath)) {
66
- watchers.push(watchNodeModules(nodeModulesPath, dir));
67
- }
68
-
69
- // Surveille la creation de node_modules
70
- if (process.platform === 'linux') {
71
- console.log('[DAEMON] Note: recursive fs.watch may not work on Linux');
72
- }
73
-
74
- const dirWatcher = fs.watch(dir, (eventType, filename) => {
75
- if (filename === 'node_modules' && eventType === 'rename') {
76
- const nmPath = path.join(dir, 'node_modules');
77
- if (fs.existsSync(nmPath)) {
78
- console.log('[DAEMON] node_modules detecte, scan en cours...');
79
- triggerScan(dir);
80
- }
81
- }
82
- if (filename === 'package-lock.json' || filename === 'yarn.lock') {
83
- console.log(`[DAEMON] ${filename} modifie, scan en cours...`);
84
- triggerScan(dir);
85
- }
86
- });
87
- dirWatcher.on('error', (err) => {
88
- console.log(`[DAEMON] Watcher error on ${dir}: ${err.message}`);
89
- });
90
- watchers.push(dirWatcher);
91
-
92
- return watchers;
93
- }
94
-
95
- function watchFile(filePath, projectDir) {
96
- let lastMtime;
97
- try {
98
- lastMtime = fs.statSync(filePath).mtime.getTime();
99
- } catch {
100
- return null; // File deleted between existsSync and statSync
101
- }
102
-
103
- const watcher = fs.watch(filePath, (eventType) => {
104
- if (eventType === 'change') {
105
- try {
106
- const currentMtime = fs.statSync(filePath).mtime.getTime();
107
- if (currentMtime !== lastMtime) {
108
- lastMtime = currentMtime;
109
- console.log(`[DAEMON] ${path.basename(filePath)} modifie`);
110
- triggerScan(projectDir);
111
- }
112
- } catch {
113
- // File may have been deleted between watch trigger and stat
114
- }
115
- }
116
- });
117
- watcher.on('error', (err) => {
118
- console.log(`[DAEMON] Watcher error on ${filePath}: ${err.message}`);
119
- });
120
- return watcher;
121
- }
122
-
123
- function watchNodeModules(nodeModulesPath, projectDir) {
124
- const watcher = fs.watch(nodeModulesPath, { recursive: true }, (eventType, filename) => {
125
- if (filename && filename.includes('package.json')) {
126
- console.log(`[DAEMON] Nouveau package detecte: ${filename}`);
127
- triggerScan(projectDir);
128
- }
129
- });
130
- watcher.on('error', (err) => {
131
- console.log(`[DAEMON] Watcher error on ${nodeModulesPath}: ${err.message}`);
132
- });
133
- return watcher;
134
- }
135
-
136
- // Per-directory scan state to prevent cross-directory scan suppression
137
- const scanState = new Map();
138
-
139
- function getScanState(dir) {
140
- if (!scanState.has(dir)) {
141
- scanState.set(dir, { timeout: null, lastScanTime: 0 });
142
- }
143
- return scanState.get(dir);
144
- }
145
-
146
- function triggerScan(dir) {
147
- const now = Date.now();
148
- const state = getScanState(dir);
149
-
150
- // Debounce: attend 3 secondes avant de scanner
151
- if (state.timeout) {
152
- clearTimeout(state.timeout);
153
- }
154
-
155
- // Evite les scans trop frequents (minimum 10 secondes entre chaque)
156
- if (now - state.lastScanTime < 10000) {
157
- state.timeout = setTimeout(() => triggerScan(dir), 10000 - (now - state.lastScanTime));
158
- return;
159
- }
160
-
161
- state.timeout = setTimeout(async () => {
162
- state.lastScanTime = Date.now();
163
- console.log(`\n[DAEMON] ========== SCAN AUTOMATIQUE ==========`);
164
- console.log(`[DAEMON] Cible: ${dir}`);
165
- console.log(`[DAEMON] Heure: ${new Date().toLocaleTimeString()}\n`);
166
-
167
- try {
168
- await run(dir, { webhook: webhookUrl });
169
- } catch (err) {
170
- console.log(`[DAEMON] Erreur scan: ${err.message}`);
171
- }
172
-
173
- console.log(`\n[DAEMON] ======================================\n`);
174
- console.log('[DAEMON] En attente de modifications...');
175
- }, 3000);
176
- }
177
-
178
- module.exports = { startDaemon };
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { run } = require('./index.js');
4
+
5
+ let webhookUrl = null;
6
+
7
+ async function startDaemon(options = {}) {
8
+ webhookUrl = options.webhook || null;
9
+
10
+ console.log(`
11
+ ╔════════════════════════════════════════════╗
12
+ ║ MUAD'DIB Security Daemon ║
13
+ ║ Surveillance npm install active ║
14
+ ╚════════════════════════════════════════════╝
15
+ `);
16
+
17
+ console.log('[DAEMON] Demarrage...');
18
+ console.log(`[DAEMON] Webhook: ${webhookUrl ? 'Configure' : 'Non configure'}`);
19
+ console.log('[DAEMON] Ctrl+C pour arreter\n');
20
+
21
+ // Surveille le dossier courant
22
+ const cwd = process.cwd();
23
+ const watchers = watchDirectory(cwd);
24
+
25
+ // Cleanup function to close all watchers
26
+ function cleanup() {
27
+ for (const w of watchers) {
28
+ try { w.close(); } catch { /* ignore */ }
29
+ }
30
+ }
31
+
32
+ // Keep process alive until SIGINT
33
+ await new Promise((resolve) => {
34
+ process.once('SIGINT', () => {
35
+ console.log('\n[DAEMON] Arret...');
36
+ cleanup();
37
+ resolve();
38
+ });
39
+ });
40
+
41
+ process.exit(0);
42
+ }
43
+
44
+ function watchDirectory(dir) {
45
+ const watchers = [];
46
+ const nodeModulesPath = path.join(dir, 'node_modules');
47
+ const packageLockPath = path.join(dir, 'package-lock.json');
48
+ const yarnLockPath = path.join(dir, 'yarn.lock');
49
+
50
+ console.log(`[DAEMON] Surveillance de ${dir}`);
51
+
52
+ // Surveille package-lock.json
53
+ if (fs.existsSync(packageLockPath)) {
54
+ const w = watchFile(packageLockPath, dir);
55
+ if (w) watchers.push(w);
56
+ }
57
+
58
+ // Surveille yarn.lock
59
+ if (fs.existsSync(yarnLockPath)) {
60
+ const w = watchFile(yarnLockPath, dir);
61
+ if (w) watchers.push(w);
62
+ }
63
+
64
+ // Surveille node_modules
65
+ if (fs.existsSync(nodeModulesPath)) {
66
+ watchers.push(watchNodeModules(nodeModulesPath, dir));
67
+ }
68
+
69
+ // Surveille la creation de node_modules
70
+ if (process.platform === 'linux') {
71
+ console.log('[DAEMON] Note: recursive fs.watch may not work on Linux');
72
+ }
73
+
74
+ const dirWatcher = fs.watch(dir, (eventType, filename) => {
75
+ if (filename === 'node_modules' && eventType === 'rename') {
76
+ const nmPath = path.join(dir, 'node_modules');
77
+ if (fs.existsSync(nmPath)) {
78
+ console.log('[DAEMON] node_modules detecte, scan en cours...');
79
+ triggerScan(dir);
80
+ }
81
+ }
82
+ if (filename === 'package-lock.json' || filename === 'yarn.lock') {
83
+ console.log(`[DAEMON] ${filename} modifie, scan en cours...`);
84
+ triggerScan(dir);
85
+ }
86
+ });
87
+ dirWatcher.on('error', (err) => {
88
+ console.log(`[DAEMON] Watcher error on ${dir}: ${err.message}`);
89
+ });
90
+ watchers.push(dirWatcher);
91
+
92
+ return watchers;
93
+ }
94
+
95
+ function watchFile(filePath, projectDir) {
96
+ let lastMtime;
97
+ try {
98
+ lastMtime = fs.statSync(filePath).mtime.getTime();
99
+ } catch {
100
+ return null; // File deleted between existsSync and statSync
101
+ }
102
+
103
+ const watcher = fs.watch(filePath, (eventType) => {
104
+ if (eventType === 'change') {
105
+ try {
106
+ const currentMtime = fs.statSync(filePath).mtime.getTime();
107
+ if (currentMtime !== lastMtime) {
108
+ lastMtime = currentMtime;
109
+ console.log(`[DAEMON] ${path.basename(filePath)} modifie`);
110
+ triggerScan(projectDir);
111
+ }
112
+ } catch {
113
+ // File may have been deleted between watch trigger and stat
114
+ }
115
+ }
116
+ });
117
+ watcher.on('error', (err) => {
118
+ console.log(`[DAEMON] Watcher error on ${filePath}: ${err.message}`);
119
+ });
120
+ return watcher;
121
+ }
122
+
123
+ function watchNodeModules(nodeModulesPath, projectDir) {
124
+ const watcher = fs.watch(nodeModulesPath, { recursive: true }, (eventType, filename) => {
125
+ if (filename && filename.includes('package.json')) {
126
+ console.log(`[DAEMON] Nouveau package detecte: ${filename}`);
127
+ triggerScan(projectDir);
128
+ }
129
+ });
130
+ watcher.on('error', (err) => {
131
+ console.log(`[DAEMON] Watcher error on ${nodeModulesPath}: ${err.message}`);
132
+ });
133
+ return watcher;
134
+ }
135
+
136
+ // Per-directory scan state to prevent cross-directory scan suppression
137
+ const scanState = new Map();
138
+
139
+ function getScanState(dir) {
140
+ if (!scanState.has(dir)) {
141
+ scanState.set(dir, { timeout: null, lastScanTime: 0 });
142
+ }
143
+ return scanState.get(dir);
144
+ }
145
+
146
+ function triggerScan(dir) {
147
+ const now = Date.now();
148
+ const state = getScanState(dir);
149
+
150
+ // Debounce: attend 3 secondes avant de scanner
151
+ if (state.timeout) {
152
+ clearTimeout(state.timeout);
153
+ }
154
+
155
+ // Evite les scans trop frequents (minimum 10 secondes entre chaque)
156
+ if (now - state.lastScanTime < 10000) {
157
+ state.timeout = setTimeout(() => triggerScan(dir), 10000 - (now - state.lastScanTime));
158
+ return;
159
+ }
160
+
161
+ state.timeout = setTimeout(async () => {
162
+ state.lastScanTime = Date.now();
163
+ console.log(`\n[DAEMON] ========== SCAN AUTOMATIQUE ==========`);
164
+ console.log(`[DAEMON] Cible: ${dir}`);
165
+ console.log(`[DAEMON] Heure: ${new Date().toLocaleTimeString()}\n`);
166
+
167
+ try {
168
+ await run(dir, { webhook: webhookUrl });
169
+ } catch (err) {
170
+ console.log(`[DAEMON] Erreur scan: ${err.message}`);
171
+ }
172
+
173
+ console.log(`\n[DAEMON] ======================================\n`);
174
+ console.log('[DAEMON] En attente de modifications...');
175
+ }, 3000);
176
+ }
177
+
178
+ module.exports = { startDaemon, watchDirectory, watchFile, watchNodeModules, triggerScan, getScanState };
@@ -1089,6 +1089,8 @@ async function runScraper() {
1089
1089
 
1090
1090
  // Smart deduplication: build map of best entry per key
1091
1091
  // For duplicates, keep the one with highest confidence, then most recent date
1092
+ const dedupSpinner = new Spinner();
1093
+ dedupSpinner.start('Deduplicating ' + allPackages.length + ' npm + ' + pypiPackages.length + ' PyPI entries...');
1092
1094
  const dedupMap = new Map();
1093
1095
 
1094
1096
  // Seed with existing IOCs
@@ -1182,6 +1184,8 @@ async function runScraper() {
1182
1184
  ];
1183
1185
  }
1184
1186
 
1187
+ dedupSpinner.succeed('Deduplicated: ' + existingIOCs.packages.length + ' npm + ' + existingIOCs.pypi_packages.length + ' PyPI packages (' + addedPackages + ' new, ' + upgradedPackages + ' upgraded)');
1188
+
1185
1189
  // Update metadata
1186
1190
  existingIOCs.updated = new Date().toISOString();
1187
1191
  existingIOCs.sources = [
@@ -1278,7 +1282,13 @@ async function runScraper() {
1278
1282
  };
1279
1283
  }
1280
1284
 
1281
- module.exports = { runScraper, scrapeShaiHuludDetector, scrapeDatadogIOCs };
1285
+ module.exports = {
1286
+ runScraper, scrapeShaiHuludDetector, scrapeDatadogIOCs,
1287
+ // Pure utility functions (exported for testing)
1288
+ parseCSVLine, parseCSV, extractVersions, parseOSVEntry,
1289
+ createFreshness, isAllowedRedirect, loadStaticIOCs,
1290
+ CONFIDENCE_ORDER, ALLOWED_REDIRECT_DOMAINS
1291
+ };
1282
1292
 
1283
1293
  // Direct execution if called as CLI
1284
1294
  if (require.main === module) {