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 +1 -1
- package/src/ioc/data/iocs.json +1 -1
- package/src/ioc/scraper.js +140 -154
- package/src/ioc/updater.js +108 -97
package/package.json
CHANGED
package/src/ioc/data/iocs.json
CHANGED
|
@@ -16004,7 +16004,7 @@
|
|
|
16004
16004
|
"pigS3cr3ts.json"
|
|
16005
16005
|
],
|
|
16006
16006
|
"files": [],
|
|
16007
|
-
"updated": "2026-01-15T08:
|
|
16007
|
+
"updated": "2026-01-15T08:57:29.829Z",
|
|
16008
16008
|
"sources": [
|
|
16009
16009
|
"shai-hulud-detector",
|
|
16010
16010
|
"datadog-consolidated",
|
package/src/ioc/scraper.js
CHANGED
|
@@ -20,7 +20,7 @@ function loadStaticIOCs() {
|
|
|
20
20
|
return { socket: [], phylum: [], npmRemoved: [] };
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
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/
|
|
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
|
-
|
|
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/
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
//
|
|
125
|
-
if (data.indicators
|
|
136
|
+
// Extract hashes
|
|
137
|
+
if (data.indicators && data.indicators.fileHashes) {
|
|
126
138
|
const fileHashes = data.indicators.fileHashes;
|
|
127
|
-
for (const
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
234
|
-
//
|
|
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
|
|
248
|
+
console.log('[SCRAPER] OSSF Malicious Packages...');
|
|
238
249
|
const packages = [];
|
|
239
250
|
|
|
240
251
|
try {
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
351
|
-
for (const vuln of data.vulns) {
|
|
352
|
-
//
|
|
353
|
-
if (vuln.id
|
|
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
|
|
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: [
|
|
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
|
-
//
|
|
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: [
|
|
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:
|
|
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
|
|
452
|
-
//
|
|
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:
|
|
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('
|
|
511
|
-
console.log('
|
|
512
|
-
console.log('
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
526
|
-
console.log('[WARN]
|
|
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 =>
|
|
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
|
|
534
|
-
|
|
535
|
-
console.log(
|
|
536
|
-
|
|
537
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
550
|
+
// Merge all hashes
|
|
565
551
|
const allHashes = [
|
|
566
552
|
...(shaiHuludResult.hashes || []),
|
|
567
553
|
...(datadogResult.hashes || [])
|
|
568
554
|
];
|
|
569
555
|
|
|
570
|
-
//
|
|
556
|
+
// Deduplicate and add new packages
|
|
571
557
|
let addedPackages = 0;
|
|
572
558
|
for (const pkg of allPackages) {
|
|
573
|
-
const key =
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
612
|
+
// Save
|
|
627
613
|
fs.writeFileSync(IOC_FILE, JSON.stringify(existingIOCs, null, 2));
|
|
628
614
|
|
|
629
|
-
//
|
|
630
|
-
console.log('\n
|
|
631
|
-
console.log('
|
|
632
|
-
console.log('
|
|
633
|
-
console.log(
|
|
634
|
-
console.log(
|
|
635
|
-
console.log(
|
|
636
|
-
console.log(
|
|
637
|
-
console.log(
|
|
638
|
-
console.log(
|
|
639
|
-
console.log(
|
|
640
|
-
|
|
641
|
-
// Stats
|
|
642
|
-
console.log('\n
|
|
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
|
-
|
|
648
|
-
|
|
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
|
|
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
|
-
//
|
|
650
|
+
// Direct execution if called as CLI
|
|
665
651
|
if (require.main === module) {
|
|
666
652
|
runScraper()
|
|
667
|
-
.then(result
|
|
668
|
-
console.log('
|
|
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('
|
|
657
|
+
.catch(function(err) {
|
|
658
|
+
console.error('[ERROR] ' + err.message);
|
|
673
659
|
process.exit(1);
|
|
674
660
|
});
|
|
675
661
|
}
|
package/src/ioc/updater.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
30
|
-
markers: yamlIOCs.markers.map(m
|
|
31
|
-
files: yamlIOCs.files.map(f
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
console.log(
|
|
80
|
-
console.log(
|
|
81
|
-
console.log(
|
|
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(
|
|
125
|
+
reject(new Error('HTTP ' + res.statusCode));
|
|
95
126
|
return;
|
|
96
127
|
}
|
|
97
128
|
let data = '';
|
|
98
|
-
res.on('data', chunk
|
|
99
|
-
res.on('end', ()
|
|
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
|
-
//
|
|
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
|
|
126
|
-
markers: yamlIOCs.markers.map(m
|
|
127
|
-
files: yamlIOCs.files.map(f
|
|
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
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|