muaddib-scanner 1.2.3 → 1.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Supply-chain threat detection & response for npm",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -16004,7 +16004,7 @@
16004
16004
  "pigS3cr3ts.json"
16005
16005
  ],
16006
16006
  "files": [],
16007
- "updated": "2026-01-15T08:38:48.160Z",
16007
+ "updated": "2026-01-15T08:57:29.829Z",
16008
16008
  "sources": [
16009
16009
  "shai-hulud-detector",
16010
16010
  "datadog-consolidated",
@@ -20,7 +20,7 @@ function loadStaticIOCs() {
20
20
  return { socket: [], phylum: [], npmRemoved: [] };
21
21
  }
22
22
 
23
- async function fetchJSON(url, options = {}) {
23
+ function fetchJSON(url, options = {}) {
24
24
  return new Promise((resolve, reject) => {
25
25
  const urlObj = new URL(url);
26
26
  const reqOptions = {
@@ -28,13 +28,19 @@ async function fetchJSON(url, options = {}) {
28
28
  path: urlObj.pathname + urlObj.search,
29
29
  method: options.method || 'GET',
30
30
  headers: {
31
- 'User-Agent': 'MUADDIB-Scanner/2.0',
31
+ 'User-Agent': 'MUADDIB-Scanner/3.0',
32
32
  'Accept': 'application/json',
33
33
  ...options.headers
34
34
  }
35
35
  };
36
36
 
37
37
  const req = https.request(reqOptions, (res) => {
38
+ // Handle redirects
39
+ if (res.statusCode === 301 || res.statusCode === 302) {
40
+ fetchJSON(res.headers.location, options).then(resolve).catch(reject);
41
+ return;
42
+ }
43
+
38
44
  let data = '';
39
45
  res.on('data', chunk => data += chunk);
40
46
  res.on('end', () => {
@@ -60,7 +66,7 @@ async function fetchJSON(url, options = {}) {
60
66
  });
61
67
  }
62
68
 
63
- async function fetchText(url) {
69
+ function fetchText(url) {
64
70
  return new Promise((resolve, reject) => {
65
71
  const urlObj = new URL(url);
66
72
  const reqOptions = {
@@ -68,11 +74,17 @@ async function fetchText(url) {
68
74
  path: urlObj.pathname + urlObj.search,
69
75
  method: 'GET',
70
76
  headers: {
71
- 'User-Agent': 'MUADDIB-Scanner/2.0'
77
+ 'User-Agent': 'MUADDIB-Scanner/3.0'
72
78
  }
73
79
  };
74
80
 
75
81
  const req = https.request(reqOptions, (res) => {
82
+ // Handle redirects
83
+ if (res.statusCode === 301 || res.statusCode === 302) {
84
+ fetchText(res.headers.location).then(resolve).catch(reject);
85
+ return;
86
+ }
87
+
76
88
  let data = '';
77
89
  res.on('data', chunk => data += chunk);
78
90
  res.on('end', () => {
@@ -92,7 +104,7 @@ async function fetchText(url) {
92
104
 
93
105
  // ============================================
94
106
  // SOURCE 1: GenSecAI Shai-Hulud 2.0 Detector
95
- // La meilleure source consolidée (700+ packages)
107
+ // Consolidated list (700+ packages)
96
108
  // ============================================
97
109
  async function scrapeShaiHuludDetector() {
98
110
  console.log('[SCRAPER] GenSecAI Shai-Hulud 2.0 Detector...');
@@ -104,7 +116,7 @@ async function scrapeShaiHuludDetector() {
104
116
  const { status, data } = await fetchJSON(url);
105
117
 
106
118
  if (status === 200 && data) {
107
- // Extraire les packages
119
+ // Extract packages
108
120
  const pkgList = data.packages || [];
109
121
  for (const pkg of pkgList) {
110
122
  const versions = pkg.affectedVersions || ['*'];
@@ -115,16 +127,16 @@ async function scrapeShaiHuludDetector() {
115
127
  severity: pkg.severity || 'critical',
116
128
  confidence: 'high',
117
129
  source: 'shai-hulud-detector',
118
- description: `Compromised by Shai-Hulud 2.0 supply chain attack`,
130
+ description: 'Compromised by Shai-Hulud 2.0 supply chain attack',
119
131
  references: ['https://github.com/gensecaihq/Shai-Hulud-2.0-Detector'],
120
132
  mitre: 'T1195.002'
121
133
  });
122
134
  }
123
135
 
124
- // Extraire les hashes si disponibles
125
- if (data.indicators?.fileHashes) {
136
+ // Extract hashes
137
+ if (data.indicators && data.indicators.fileHashes) {
126
138
  const fileHashes = data.indicators.fileHashes;
127
- for (const [filename, hashData] of Object.entries(fileHashes)) {
139
+ for (const hashData of Object.values(fileHashes)) {
128
140
  if (hashData.sha256) {
129
141
  const sha256List = Array.isArray(hashData.sha256) ? hashData.sha256 : [hashData.sha256];
130
142
  for (const hash of sha256List) {
@@ -147,15 +159,14 @@ async function scrapeShaiHuludDetector() {
147
159
 
148
160
  // ============================================
149
161
  // SOURCE 2: DataDog Consolidated IOCs
150
- // URL corrigée - consolidated_iocs.csv
162
+ // Fixed URLs - consolidated_iocs.csv
151
163
  // ============================================
152
164
  async function scrapeDatadogIOCs() {
153
165
  console.log('[SCRAPER] DataDog Security Labs IOCs...');
154
166
  const packages = [];
155
- const hashes = [];
156
167
 
157
168
  try {
158
- // Fichier consolidé (plusieurs vendors)
169
+ // Consolidated file (multiple vendors)
159
170
  const consolidatedUrl = 'https://raw.githubusercontent.com/DataDog/indicators-of-compromise/main/shai-hulud-2.0/consolidated_iocs.csv';
160
171
  const consolidatedResp = await fetchText(consolidatedUrl);
161
172
 
@@ -187,7 +198,7 @@ async function scrapeDatadogIOCs() {
187
198
  console.log(`[SCRAPER] ${packages.length} packages (consolidated)`);
188
199
  }
189
200
 
190
- // Fichier DataDog spécifique
201
+ // DataDog specific file
191
202
  const ddUrl = 'https://raw.githubusercontent.com/DataDog/indicators-of-compromise/main/shai-hulud-2.0/shai-hulud-2.0.csv';
192
203
  const ddResp = await fetchText(ddUrl);
193
204
 
@@ -201,10 +212,10 @@ async function scrapeDatadogIOCs() {
201
212
  const version = parts[1].trim().replace(/"/g, '');
202
213
 
203
214
  if (name && name !== 'package_name') {
204
- // Vérifier si pas déjà ajouté
215
+ // Check if not already added
205
216
  if (!packages.find(p => p.name === name && p.version === version)) {
206
217
  packages.push({
207
- id: `DATADOG-DD-${name}-${version}`,
218
+ id: `DATADOG-DD-${name}-${version}`.replace(/[^a-zA-Z0-9-]/g, '-'),
208
219
  name: name,
209
220
  version: version,
210
221
  severity: 'critical',
@@ -226,80 +237,60 @@ async function scrapeDatadogIOCs() {
226
237
  console.log(`[SCRAPER] Erreur: ${e.message}`);
227
238
  }
228
239
 
229
- return { packages, hashes };
240
+ return { packages, hashes: [] };
230
241
  }
231
242
 
232
243
  // ============================================
233
- // SOURCE 3: OSSF Malicious Packages (via OSV API)
234
- // La source la plus complète - 8000+ reports
244
+ // SOURCE 3: OSSF Malicious Packages
245
+ // Direct download from GitHub API
235
246
  // ============================================
236
247
  async function scrapeOSSFMaliciousPackages() {
237
- console.log('[SCRAPER] OSSF Malicious Packages (via OSV.dev)...');
248
+ console.log('[SCRAPER] OSSF Malicious Packages...');
238
249
  const packages = [];
239
250
 
240
251
  try {
241
- // L'API OSV agrège les données OSSF malicious-packages
242
- // On requête par écosystème npm sans version pour tout récupérer
243
- // Malheureusement l'API OSV ne permet pas de lister tous les packages
244
- // On va donc utiliser des requêtes ciblées par préfixe commun
245
-
246
- // Liste des préfixes de packages malveillants connus
247
- const maliciousPrefixes = [
248
- 'MAL-', // Prefix OSSF pour malware
249
- ];
252
+ // Use GitHub API to list files in the npm malware directory
253
+ // We'll fetch the index file that lists all malware
254
+ const indexUrl = 'https://raw.githubusercontent.com/ossf/malicious-packages/main/osv/malicious/npm/index.json';
255
+ const indexResp = await fetchJSON(indexUrl);
250
256
 
251
- // Requête batch pour les vulns de type malware
252
- const { status, data } = await fetchJSON('https://api.osv.dev/v1/query', {
253
- method: 'POST',
254
- headers: { 'Content-Type': 'application/json' },
255
- body: {
256
- package: { ecosystem: 'npm' }
257
- }
258
- });
259
-
260
- // Note: Cette requête retourne TOUTES les vulns npm, pas juste malware
261
- // On va filtrer par ID commençant par MAL-
262
- if (status === 200 && data?.vulns) {
263
- for (const vuln of data.vulns) {
264
- // Filtrer uniquement les malware (ID commence par MAL-)
265
- if (vuln.id && vuln.id.startsWith('MAL-')) {
266
- for (const affected of vuln.affected || []) {
267
- if (affected.package?.ecosystem === 'npm') {
268
- packages.push({
269
- id: vuln.id,
270
- name: affected.package.name,
271
- version: '*',
272
- severity: 'critical',
273
- confidence: 'high',
274
- source: 'ossf-malicious',
275
- description: (vuln.summary || vuln.details || 'Malicious package').slice(0, 200),
276
- references: (vuln.references || []).map(r => r.url).slice(0, 3),
277
- mitre: 'T1195.002'
278
- });
279
- }
280
- }
257
+ if (indexResp.status === 200 && indexResp.data) {
258
+ // If index exists, parse it
259
+ const entries = Array.isArray(indexResp.data) ? indexResp.data : [];
260
+ for (const entry of entries) {
261
+ if (entry.name) {
262
+ packages.push({
263
+ id: entry.id || `OSSF-${entry.name}`,
264
+ name: entry.name,
265
+ version: entry.version || '*',
266
+ severity: 'critical',
267
+ confidence: 'high',
268
+ source: 'ossf-malicious',
269
+ description: entry.summary || 'Malicious package from OSSF database',
270
+ references: ['https://github.com/ossf/malicious-packages'],
271
+ mitre: 'T1195.002'
272
+ });
281
273
  }
282
274
  }
283
- }
284
-
285
- // Requêtes supplémentaires pour packages spécifiques connus
286
- const knownMalwarePatterns = ['typosquat', 'cryptominer', 'backdoor', 'infostealer'];
287
-
288
- for (const pattern of knownMalwarePatterns) {
289
- try {
290
- const resp = await fetchJSON('https://api.osv.dev/v1/query', {
291
- method: 'POST',
292
- headers: { 'Content-Type': 'application/json' },
293
- body: { query: pattern }
294
- });
295
-
296
- if (resp.status === 200 && resp.data?.vulns) {
297
- for (const vuln of resp.data.vulns) {
298
- if (vuln.id?.startsWith('MAL-')) {
299
- for (const affected of vuln.affected || []) {
300
- if (affected.package?.ecosystem === 'npm') {
301
- const exists = packages.find(p => p.id === vuln.id && p.name === affected.package.name);
302
- if (!exists) {
275
+ } else {
276
+ // Fallback: use OSV API to query for MAL- prefixed vulnerabilities
277
+ // This is limited but better than nothing
278
+ const ecosystems = ['npm'];
279
+
280
+ for (const ecosystem of ecosystems) {
281
+ try {
282
+ const resp = await fetchJSON('https://api.osv.dev/v1/query', {
283
+ method: 'POST',
284
+ headers: { 'Content-Type': 'application/json' },
285
+ body: { package: { ecosystem } }
286
+ });
287
+
288
+ if (resp.status === 200 && resp.data && resp.data.vulns) {
289
+ for (const vuln of resp.data.vulns) {
290
+ // Filter only malware (ID starts with MAL-)
291
+ if (vuln.id && vuln.id.startsWith('MAL-')) {
292
+ for (const affected of vuln.affected || []) {
293
+ if (affected.package && affected.package.ecosystem === 'npm') {
303
294
  packages.push({
304
295
  id: vuln.id,
305
296
  name: affected.package.name,
@@ -307,7 +298,7 @@ async function scrapeOSSFMaliciousPackages() {
307
298
  severity: 'critical',
308
299
  confidence: 'high',
309
300
  source: 'ossf-malicious',
310
- description: (vuln.summary || `${pattern} malware`).slice(0, 200),
301
+ description: (vuln.summary || vuln.details || 'Malicious package').slice(0, 200),
311
302
  references: (vuln.references || []).map(r => r.url).slice(0, 3),
312
303
  mitre: 'T1195.002'
313
304
  });
@@ -316,9 +307,9 @@ async function scrapeOSSFMaliciousPackages() {
316
307
  }
317
308
  }
318
309
  }
310
+ } catch {
311
+ // Continue silently
319
312
  }
320
- } catch (e) {
321
- // Continue with other patterns
322
313
  }
323
314
  }
324
315
 
@@ -338,19 +329,16 @@ async function scrapeGitHubAdvisory() {
338
329
  const packages = [];
339
330
 
340
331
  try {
341
- // L'API GitHub Advisory nécessite un token, on passe par OSV qui l'agrège
342
- const { status, data } = await fetchJSON('https://api.osv.dev/v1/query', {
332
+ const resp = await fetchJSON('https://api.osv.dev/v1/query', {
343
333
  method: 'POST',
344
334
  headers: { 'Content-Type': 'application/json' },
345
- body: {
346
- package: { ecosystem: 'npm' }
347
- }
335
+ body: { package: { ecosystem: 'npm' } }
348
336
  });
349
337
 
350
- if (status === 200 && data?.vulns) {
351
- for (const vuln of data.vulns) {
352
- // Filtrer les GHSA avec mention de malware
353
- if (vuln.id?.startsWith('GHSA-')) {
338
+ if (resp.status === 200 && resp.data && resp.data.vulns) {
339
+ for (const vuln of resp.data.vulns) {
340
+ // Filter GHSA with malware mention
341
+ if (vuln.id && vuln.id.startsWith('GHSA-')) {
354
342
  const summary = (vuln.summary || '').toLowerCase();
355
343
  const details = (vuln.details || '').toLowerCase();
356
344
  const isMalware = summary.includes('malware') ||
@@ -362,7 +350,7 @@ async function scrapeGitHubAdvisory() {
362
350
 
363
351
  if (isMalware) {
364
352
  for (const affected of vuln.affected || []) {
365
- if (affected.package?.ecosystem === 'npm') {
353
+ if (affected.package && affected.package.ecosystem === 'npm') {
366
354
  packages.push({
367
355
  id: vuln.id,
368
356
  name: affected.package.name,
@@ -371,7 +359,7 @@ async function scrapeGitHubAdvisory() {
371
359
  confidence: 'high',
372
360
  source: 'github-advisory',
373
361
  description: (vuln.summary || 'Malicious package').slice(0, 200),
374
- references: [`https://github.com/advisories/${vuln.id}`],
362
+ references: ['https://github.com/advisories/' + vuln.id],
375
363
  mitre: 'T1195.002'
376
364
  });
377
365
  }
@@ -391,7 +379,7 @@ async function scrapeGitHubAdvisory() {
391
379
 
392
380
  // ============================================
393
381
  // SOURCE 5: Static IOCs (Socket, Phylum, npm removed)
394
- // Fichier local maintenu manuellement
382
+ // Local file maintained manually
395
383
  // ============================================
396
384
  async function scrapeStaticIOCs() {
397
385
  console.log('[SCRAPER] Static IOCs (local file)...');
@@ -408,7 +396,7 @@ async function scrapeStaticIOCs() {
408
396
  confidence: 'high',
409
397
  source: 'socket-dev',
410
398
  description: pkg.description || 'Malicious package reported by Socket.dev',
411
- references: [`https://socket.dev/npm/package/${pkg.name}`],
399
+ references: ['https://socket.dev/npm/package/' + pkg.name],
412
400
  mitre: 'T1195.002'
413
401
  });
414
402
  }
@@ -437,7 +425,7 @@ async function scrapeStaticIOCs() {
437
425
  severity: 'critical',
438
426
  confidence: 'high',
439
427
  source: 'npm-removed',
440
- description: `Removed from npm: ${pkg.reason || 'security violation'}`,
428
+ description: 'Removed from npm: ' + (pkg.reason || 'security violation'),
441
429
  references: ['https://www.npmjs.com/policies/security'],
442
430
  mitre: 'T1195.002'
443
431
  });
@@ -448,15 +436,13 @@ async function scrapeStaticIOCs() {
448
436
  }
449
437
 
450
438
  // ============================================
451
- // SOURCE 6: Snyk Vulnerability DB (malware only)
452
- // Via API publique limitée
439
+ // SOURCE 6: Snyk Known Malware
440
+ // Historical attacks database
453
441
  // ============================================
454
442
  async function scrapeSnykMalware() {
455
443
  console.log('[SCRAPER] Snyk Malware DB...');
456
444
  const packages = [];
457
445
 
458
- // Snyk n'a pas d'API publique pour lister les malwares
459
- // On utilise des packages connus documentés dans leurs blogs
460
446
  const knownSnykMalware = [
461
447
  { name: 'event-stream', version: '3.3.6', description: 'Flatmap-stream backdoor (2018)' },
462
448
  { name: 'flatmap-stream', version: '*', description: 'Malicious dependency of event-stream' },
@@ -486,7 +472,7 @@ async function scrapeSnykMalware() {
486
472
 
487
473
  for (const pkg of knownSnykMalware) {
488
474
  packages.push({
489
- id: `SNYK-${pkg.name}-${pkg.version}`.replace(/[^a-zA-Z0-9-]/g, '-'),
475
+ id: ('SNYK-' + pkg.name + '-' + pkg.version).replace(/[^a-zA-Z0-9-]/g, '-'),
490
476
  name: pkg.name,
491
477
  version: pkg.version,
492
478
  severity: 'critical',
@@ -506,43 +492,36 @@ async function scrapeSnykMalware() {
506
492
  // MAIN SCRAPER
507
493
  // ============================================
508
494
  async function runScraper() {
509
- console.log('\n╔════════════════════════════════════════════════════════╗');
510
- console.log('MUAD\'DIB IOC Scraper v3.0');
511
- console.log('Optimized sources - No dead links');
512
- console.log('╚════════════════════════════════════════════════════════╝\n');
495
+ console.log('\n' + '='.repeat(60));
496
+ console.log(' MUAD\'DIB IOC Scraper v3.0');
497
+ console.log(' Optimized sources - No dead links');
498
+ console.log('='.repeat(60) + '\n');
513
499
 
514
- // Créer le dossier data si nécessaire
500
+ // Create data directory if needed
515
501
  const dataDir = path.dirname(IOC_FILE);
516
502
  if (!fs.existsSync(dataDir)) {
517
503
  fs.mkdirSync(dataDir, { recursive: true });
518
504
  }
519
505
 
520
- // Charger les IOCs existants
506
+ // Load existing IOCs
521
507
  let existingIOCs = { packages: [], hashes: [], markers: [], files: [] };
522
508
  if (fs.existsSync(IOC_FILE)) {
523
509
  try {
524
510
  existingIOCs = JSON.parse(fs.readFileSync(IOC_FILE, 'utf8'));
525
- } catch (e) {
526
- console.log('[WARN] Fichier IOCs corrompu, réinitialisation...');
511
+ } catch {
512
+ console.log('[WARN] IOCs file corrupted, resetting...');
527
513
  }
528
514
  }
529
515
 
530
- const existingNames = new Set(existingIOCs.packages.map(p => `${p.name}@${p.version}`));
516
+ const existingNames = new Set(existingIOCs.packages.map(p => p.name + '@' + p.version));
531
517
  const existingHashes = new Set(existingIOCs.hashes || []);
532
518
  const initialCount = existingIOCs.packages.length;
533
- const initialHashCount = existingIOCs.hashes?.length || 0;
534
-
535
- console.log(`[INFO] IOCs existants: ${initialCount} packages, ${initialHashCount} hashes\n`);
536
-
537
- // Scraper toutes les sources en parallèle
538
- const [
539
- shaiHuludResult,
540
- datadogResult,
541
- ossfPackages,
542
- githubPackages,
543
- staticPackages,
544
- snykPackages
545
- ] = await Promise.all([
519
+ const initialHashCount = existingIOCs.hashes ? existingIOCs.hashes.length : 0;
520
+
521
+ console.log('[INFO] IOCs existants: ' + initialCount + ' packages, ' + initialHashCount + ' hashes\n');
522
+
523
+ // Scrape all sources in parallel
524
+ const results = await Promise.all([
546
525
  scrapeShaiHuludDetector(),
547
526
  scrapeDatadogIOCs(),
548
527
  scrapeOSSFMaliciousPackages(),
@@ -551,7 +530,14 @@ async function runScraper() {
551
530
  scrapeSnykMalware()
552
531
  ]);
553
532
 
554
- // Merger tous les packages
533
+ const shaiHuludResult = results[0];
534
+ const datadogResult = results[1];
535
+ const ossfPackages = results[2];
536
+ const githubPackages = results[3];
537
+ const staticPackages = results[4];
538
+ const snykPackages = results[5];
539
+
540
+ // Merge all packages
555
541
  const allPackages = [
556
542
  ...shaiHuludResult.packages,
557
543
  ...datadogResult.packages,
@@ -561,16 +547,16 @@ async function runScraper() {
561
547
  ...snykPackages
562
548
  ];
563
549
 
564
- // Merger tous les hashes
550
+ // Merge all hashes
565
551
  const allHashes = [
566
552
  ...(shaiHuludResult.hashes || []),
567
553
  ...(datadogResult.hashes || [])
568
554
  ];
569
555
 
570
- // Dédupliquer et ajouter les nouveaux packages
556
+ // Deduplicate and add new packages
571
557
  let addedPackages = 0;
572
558
  for (const pkg of allPackages) {
573
- const key = `${pkg.name}@${pkg.version}`;
559
+ const key = pkg.name + '@' + pkg.version;
574
560
  if (!existingNames.has(key)) {
575
561
  existingIOCs.packages.push(pkg);
576
562
  existingNames.add(key);
@@ -578,7 +564,7 @@ async function runScraper() {
578
564
  }
579
565
  }
580
566
 
581
- // Dédupliquer et ajouter les nouveaux hashes
567
+ // Deduplicate and add new hashes
582
568
  let addedHashes = 0;
583
569
  for (const hash of allHashes) {
584
570
  if (!existingHashes.has(hash)) {
@@ -589,7 +575,7 @@ async function runScraper() {
589
575
  }
590
576
  }
591
577
 
592
- // Ajouter les marqueurs Shai-Hulud si pas présents
578
+ // Add Shai-Hulud markers if not present
593
579
  if (!existingIOCs.markers || existingIOCs.markers.length === 0) {
594
580
  existingIOCs.markers = [
595
581
  'setup_bun.js',
@@ -609,7 +595,7 @@ async function runScraper() {
609
595
  ];
610
596
  }
611
597
 
612
- // Mettre à jour les métadonnées
598
+ // Update metadata
613
599
  existingIOCs.updated = new Date().toISOString();
614
600
  existingIOCs.sources = [
615
601
  'shai-hulud-detector',
@@ -623,29 +609,30 @@ async function runScraper() {
623
609
  'snyk-known'
624
610
  ];
625
611
 
626
- // Sauvegarder
612
+ // Save
627
613
  fs.writeFileSync(IOC_FILE, JSON.stringify(existingIOCs, null, 2));
628
614
 
629
- // Afficher le résumé
630
- console.log('\n╔════════════════════════════════════════════════════════╗');
631
- console.log('║ RÉSULTATS ║');
632
- console.log('╚════════════════════════════════════════════════════════╝');
633
- console.log(` Packages avant: ${initialCount}`);
634
- console.log(` Packages après: ${existingIOCs.packages.length}`);
635
- console.log(` Nouveaux: +${addedPackages}`);
636
- console.log(` Hashes avant: ${initialHashCount}`);
637
- console.log(` Hashes après: ${existingIOCs.hashes?.length || 0}`);
638
- console.log(` Nouveaux: +${addedHashes}`);
639
- console.log(` Fichier: ${IOC_FILE}`);
640
-
641
- // Stats par source
642
- console.log('\n Répartition par source:');
615
+ // Display summary
616
+ console.log('\n' + '='.repeat(60));
617
+ console.log(' RESULTATS');
618
+ console.log('='.repeat(60));
619
+ console.log(' Packages avant: ' + initialCount);
620
+ console.log(' Packages apres: ' + existingIOCs.packages.length);
621
+ console.log(' Nouveaux: +' + addedPackages);
622
+ console.log(' Hashes avant: ' + initialHashCount);
623
+ console.log(' Hashes apres: ' + (existingIOCs.hashes ? existingIOCs.hashes.length : 0));
624
+ console.log(' Nouveaux: +' + addedHashes);
625
+ console.log(' Fichier: ' + IOC_FILE);
626
+
627
+ // Stats by source
628
+ console.log('\n Repartition par source:');
643
629
  const sourceCounts = {};
644
630
  for (const pkg of existingIOCs.packages) {
645
631
  sourceCounts[pkg.source] = (sourceCounts[pkg.source] || 0) + 1;
646
632
  }
647
- for (const [source, count] of Object.entries(sourceCounts).sort((a, b) => b[1] - a[1])) {
648
- console.log(` - ${source}: ${count}`);
633
+ const sortedSources = Object.entries(sourceCounts).sort((a, b) => b[1] - a[1]);
634
+ for (const [source, count] of sortedSources) {
635
+ console.log(' - ' + source + ': ' + count);
649
636
  }
650
637
 
651
638
  console.log('\n');
@@ -654,22 +641,21 @@ async function runScraper() {
654
641
  added: addedPackages,
655
642
  total: existingIOCs.packages.length,
656
643
  addedHashes: addedHashes,
657
- totalHashes: existingIOCs.hashes?.length || 0
644
+ totalHashes: existingIOCs.hashes ? existingIOCs.hashes.length : 0
658
645
  };
659
646
  }
660
647
 
661
- // Export pour utilisation en module
662
648
  module.exports = { runScraper };
663
649
 
664
- // Exécution directe si appelé en CLI
650
+ // Direct execution if called as CLI
665
651
  if (require.main === module) {
666
652
  runScraper()
667
- .then(result => {
668
- console.log('Scraping terminé avec succès');
653
+ .then(function(result) {
654
+ console.log('[OK] ' + result.added + ' new IOCs (total: ' + result.total + ')');
669
655
  process.exit(0);
670
656
  })
671
- .catch(err => {
672
- console.error('Erreur:', err.message);
657
+ .catch(function(err) {
658
+ console.error('[ERROR] ' + err.message);
673
659
  process.exit(1);
674
660
  });
675
661
  }
@@ -3,16 +3,12 @@ const path = require('path');
3
3
  const https = require('https');
4
4
 
5
5
  const CACHE_PATH = path.join(__dirname, '../../.muaddib-cache');
6
- const IOC_FILE = path.join(CACHE_PATH, 'iocs.json');
6
+ const CACHE_IOC_FILE = path.join(CACHE_PATH, 'iocs.json');
7
+ const LOCAL_IOC_FILE = path.join(__dirname, 'data/iocs.json');
7
8
  const { loadYAMLIOCs } = require('./yaml-loader.js');
8
9
 
9
- const EXTERNAL_FEEDS = [
10
- {
11
- name: 'muaddib-community',
12
- url: 'https://raw.githubusercontent.com/DNSZLSK/muad-dib/master/data/iocs.json',
13
- parser: parseMuaddibFeed
14
- }
15
- ];
10
+ // Remote feed - only used as fallback if local scrape doesn't exist
11
+ const REMOTE_FEED_URL = 'https://raw.githubusercontent.com/DNSZLSK/muad-dib/master/data/iocs.json';
16
12
 
17
13
  async function updateIOCs() {
18
14
  console.log('[MUADDIB] Mise a jour des IOCs...\n');
@@ -21,134 +17,149 @@ async function updateIOCs() {
21
17
  fs.mkdirSync(CACHE_PATH, { recursive: true });
22
18
  }
23
19
 
24
- // Charger les IOCs depuis les fichiers YAML (incluant builtin.yaml)
20
+ // Priority 1: YAML files (builtin.yaml, etc.)
25
21
  const yamlIOCs = loadYAMLIOCs();
26
22
 
27
23
  const iocs = {
28
24
  packages: [...yamlIOCs.packages],
29
- hashes: yamlIOCs.hashes.map(h => h.sha256),
30
- markers: yamlIOCs.markers.map(m => m.pattern),
31
- files: yamlIOCs.files.map(f => f.name)
25
+ hashes: yamlIOCs.hashes.map(function(h) { return h.sha256; }),
26
+ markers: yamlIOCs.markers.map(function(m) { return m.pattern; }),
27
+ files: yamlIOCs.files.map(function(f) { return f.name; })
32
28
  };
33
29
 
34
- for (const feed of EXTERNAL_FEEDS) {
30
+ console.log('[INFO] YAML IOCs: ' + yamlIOCs.packages.length + ' packages');
31
+
32
+ // Priority 2: Local scraped IOCs (from muaddib scrape)
33
+ let localScrapedCount = 0;
34
+ if (fs.existsSync(LOCAL_IOC_FILE)) {
35
35
  try {
36
- console.log(`[INFO] Telechargement depuis ${feed.name}...`);
37
- const data = await fetchUrl(feed.url);
38
- const externalIOCs = feed.parser(data);
39
-
40
- // Merge packages
41
- for (const pkg of externalIOCs.packages || []) {
42
- if (!iocs.packages.find(p => p.name === pkg.name && p.version === pkg.version)) {
43
- iocs.packages.push(pkg);
44
- }
45
- }
46
-
47
- // Merge hashes
48
- for (const hash of externalIOCs.hashes || []) {
49
- if (!iocs.hashes.includes(hash)) {
50
- iocs.hashes.push(hash);
51
- }
52
- }
53
-
54
- // Merge markers
55
- for (const marker of externalIOCs.markers || []) {
56
- if (!iocs.markers.includes(marker)) {
57
- iocs.markers.push(marker);
58
- }
59
- }
60
-
61
- // Merge files
62
- for (const file of externalIOCs.files || []) {
63
- if (!iocs.files.includes(file)) {
64
- iocs.files.push(file);
65
- }
66
- }
67
-
68
- console.log(`[OK] IOCs externes merges depuis ${feed.name}`);
69
- } catch (err) {
70
- console.log(`[WARN] Echec ${feed.name}: ${err.message}`);
36
+ const localIOCs = JSON.parse(fs.readFileSync(LOCAL_IOC_FILE, 'utf8'));
37
+ localScrapedCount = mergeIOCs(iocs, localIOCs);
38
+ console.log('[INFO] Local scraped IOCs: +' + localScrapedCount + ' packages');
39
+ } catch (e) {
40
+ console.log('[WARN] Erreur lecture IOCs locaux: ' + e.message);
71
41
  }
42
+ } else {
43
+ console.log('[INFO] Pas d\'IOCs locaux (lancez "muaddib scrape" pour en generer)');
72
44
  }
73
45
 
46
+ // Priority 3: Remote feed (fallback / additional source)
47
+ let remoteCount = 0;
48
+ try {
49
+ console.log('[INFO] Telechargement depuis GitHub...');
50
+ const remoteData = await fetchUrl(REMOTE_FEED_URL);
51
+ const remoteIOCs = JSON.parse(remoteData);
52
+ remoteCount = mergeIOCs(iocs, remoteIOCs);
53
+ console.log('[INFO] Remote IOCs: +' + remoteCount + ' packages');
54
+ } catch (e) {
55
+ console.log('[WARN] Echec telechargement distant: ' + e.message);
56
+ console.log('[INFO] Utilisation des IOCs locaux uniquement');
57
+ }
58
+
59
+ // Update metadata
74
60
  iocs.updated = new Date().toISOString();
75
61
 
76
- fs.writeFileSync(IOC_FILE, JSON.stringify(iocs, null, 2));
77
- console.log(`\n[OK] IOCs sauvegardes:`);
78
- console.log(` - ${iocs.packages.length} packages malveillants`);
79
- console.log(` - ${iocs.files.length} fichiers suspects`);
80
- console.log(` - ${iocs.hashes.length} hashes connus`);
81
- console.log(` - ${iocs.markers.length} marqueurs\n`);
62
+ // Save to cache
63
+ fs.writeFileSync(CACHE_IOC_FILE, JSON.stringify(iocs, null, 2));
64
+
65
+ console.log('\n[OK] IOCs sauvegardes:');
66
+ console.log(' - ' + iocs.packages.length + ' packages malveillants');
67
+ console.log(' - ' + iocs.files.length + ' fichiers suspects');
68
+ console.log(' - ' + iocs.hashes.length + ' hashes connus');
69
+ console.log(' - ' + iocs.markers.length + ' marqueurs\n');
82
70
 
83
71
  return iocs;
84
72
  }
85
73
 
74
+ /**
75
+ * Merge source IOCs into target without duplicates
76
+ * Returns number of packages added
77
+ */
78
+ function mergeIOCs(target, source) {
79
+ let added = 0;
80
+
81
+ // Merge packages
82
+ for (const pkg of source.packages || []) {
83
+ const exists = target.packages.find(function(p) {
84
+ return p.name === pkg.name && p.version === pkg.version;
85
+ });
86
+ if (!exists) {
87
+ target.packages.push(pkg);
88
+ added++;
89
+ }
90
+ }
91
+
92
+ // Merge hashes
93
+ for (const hash of source.hashes || []) {
94
+ if (!target.hashes.includes(hash)) {
95
+ target.hashes.push(hash);
96
+ }
97
+ }
98
+
99
+ // Merge markers
100
+ for (const marker of source.markers || []) {
101
+ if (!target.markers.includes(marker)) {
102
+ target.markers.push(marker);
103
+ }
104
+ }
105
+
106
+ // Merge files
107
+ for (const file of source.files || []) {
108
+ if (!target.files.includes(file)) {
109
+ target.files.push(file);
110
+ }
111
+ }
112
+
113
+ return added;
114
+ }
115
+
86
116
  function fetchUrl(url) {
87
- return new Promise((resolve, reject) => {
88
- https.get(url, (res) => {
117
+ return new Promise(function(resolve, reject) {
118
+ https.get(url, function(res) {
119
+ // Handle redirects
89
120
  if (res.statusCode === 301 || res.statusCode === 302) {
90
121
  fetchUrl(res.headers.location).then(resolve).catch(reject);
91
122
  return;
92
123
  }
93
124
  if (res.statusCode !== 200) {
94
- reject(new Error(`HTTP ${res.statusCode}`));
125
+ reject(new Error('HTTP ' + res.statusCode));
95
126
  return;
96
127
  }
97
128
  let data = '';
98
- res.on('data', chunk => data += chunk);
99
- res.on('end', () => resolve(data));
129
+ res.on('data', function(chunk) { data += chunk; });
130
+ res.on('end', function() { resolve(data); });
100
131
  }).on('error', reject);
101
132
  });
102
133
  }
103
134
 
104
- function parseMuaddibFeed(data) {
105
- try {
106
- return JSON.parse(data);
107
- } catch {
108
- return { packages: [], hashes: [], markers: [], files: [] };
109
- }
110
- }
111
-
112
135
  function loadCachedIOCs() {
113
- // Priorite 1 : IOCs YAML locaux
136
+ // Priority 1: YAML IOCs
114
137
  const yamlIOCs = loadYAMLIOCs();
115
138
 
116
- // Priorite 2 : Cache telecharge
117
- let cachedIOCs = { packages: [], hashes: [], markers: [], files: [] };
118
- if (fs.existsSync(IOC_FILE)) {
119
- cachedIOCs = JSON.parse(fs.readFileSync(IOC_FILE, 'utf8'));
120
- }
121
-
122
- // Merge : YAML + Cache
123
139
  const merged = {
124
140
  packages: [...yamlIOCs.packages],
125
- hashes: yamlIOCs.hashes.map(h => h.sha256),
126
- markers: yamlIOCs.markers.map(m => m.pattern),
127
- files: yamlIOCs.files.map(f => f.name)
141
+ hashes: yamlIOCs.hashes.map(function(h) { return h.sha256; }),
142
+ markers: yamlIOCs.markers.map(function(m) { return m.pattern; }),
143
+ files: yamlIOCs.files.map(function(f) { return f.name; })
128
144
  };
129
145
 
130
- // Ajouter les IOCs du cache sans doublons
131
- for (const pkg of cachedIOCs.packages || []) {
132
- if (!merged.packages.find(p => p.name === pkg.name)) {
133
- merged.packages.push(pkg);
134
- }
135
- }
136
-
137
- for (const hash of cachedIOCs.hashes || []) {
138
- if (!merged.hashes.includes(hash)) {
139
- merged.hashes.push(hash);
140
- }
141
- }
142
-
143
- for (const marker of cachedIOCs.markers || []) {
144
- if (!merged.markers.includes(marker)) {
145
- merged.markers.push(marker);
146
+ // Priority 2: Local scraped IOCs
147
+ if (fs.existsSync(LOCAL_IOC_FILE)) {
148
+ try {
149
+ const localIOCs = JSON.parse(fs.readFileSync(LOCAL_IOC_FILE, 'utf8'));
150
+ mergeIOCs(merged, localIOCs);
151
+ } catch {
152
+ // Ignore errors
146
153
  }
147
154
  }
148
155
 
149
- for (const file of cachedIOCs.files || []) {
150
- if (!merged.files.includes(file)) {
151
- merged.files.push(file);
156
+ // Priority 3: Cached IOCs (from previous update)
157
+ if (fs.existsSync(CACHE_IOC_FILE)) {
158
+ try {
159
+ const cachedIOCs = JSON.parse(fs.readFileSync(CACHE_IOC_FILE, 'utf8'));
160
+ mergeIOCs(merged, cachedIOCs);
161
+ } catch {
162
+ // Ignore errors
152
163
  }
153
164
  }
154
165