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 +4 -4
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/daemon.js +178 -178
- package/src/ioc/scraper.js +11 -1
- package/src/ioc/updater.js +424 -424
- package/src/safe-install.js +294 -294
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.
|
|
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.
|
|
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
|
-
- **
|
|
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.
|
|
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.
|
|
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
|
-
- **
|
|
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
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 };
|
package/src/ioc/scraper.js
CHANGED
|
@@ -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 = {
|
|
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) {
|