muaddib-scanner 2.10.39 → 2.10.41
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/integrations/canary-tokens.js +53 -0
- package/src/monitor/classify.js +1 -0
- package/src/response/playbooks.js +9 -0
- package/src/rules/index.js +23 -0
- package/src/sandbox/gvisor-parser.js +348 -0
- package/src/sandbox/index.js +133 -21
- package/src/sandbox/network-allowlist.js +162 -0
- package/iocs/builtin.yaml +0 -239
- package/iocs/hashes.yaml +0 -214
- package/iocs/packages.yaml +0 -481
- package/scripts/analyze-score0.js +0 -190
- package/scripts/archive-cleanup.sh +0 -7
- package/scripts/audit-archive.sh +0 -45
- package/scripts/benchmark.js +0 -326
- package/scripts/cleanup-fp-labels.js +0 -81
- package/scripts/ossf-benchmark.js +0 -548
- package/scripts/sample-npm-random.js +0 -339
- package/src/ioc/data/.ossf-tree-sha +0 -1
|
@@ -1,548 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* MUAD'DIB OpenSSF Benchmark
|
|
6
|
-
*
|
|
7
|
-
* Fetches the OpenSSF malicious-packages dataset (via OSV.dev API),
|
|
8
|
-
* downloads available npm packages, scans them with MUAD'DIB, and
|
|
9
|
-
* produces a benchmark results file consumed by `muaddib evaluate`.
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* node scripts/ossf-benchmark.js [--sample N] [--seed N] [--refresh]
|
|
13
|
-
*
|
|
14
|
-
* Options:
|
|
15
|
-
* --sample N Number of packages to sample (default: 500)
|
|
16
|
-
* --seed N Random seed for reproducibility (default: 42)
|
|
17
|
-
* --refresh Force re-download of cached tarballs
|
|
18
|
-
*
|
|
19
|
-
* Output:
|
|
20
|
-
* datasets/real-world/ossf-benchmark-results.json
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const fs = require('fs');
|
|
24
|
-
const path = require('path');
|
|
25
|
-
const zlib = require('zlib');
|
|
26
|
-
const { execSync } = require('child_process');
|
|
27
|
-
|
|
28
|
-
const ROOT = path.join(__dirname, '..');
|
|
29
|
-
const RESULTS_FILE = path.join(ROOT, 'datasets', 'real-world', 'ossf-benchmark-results.json');
|
|
30
|
-
const CACHE_DIR = path.join(ROOT, '.muaddib-cache', 'ossf-tarballs');
|
|
31
|
-
const PACK_TIMEOUT_MS = 30000;
|
|
32
|
-
const SCAN_TIMEOUT_MS = 30000;
|
|
33
|
-
const SAFE_PKG_RE = /^(@[\w._-]+\/)?[\w._-]+$/;
|
|
34
|
-
|
|
35
|
-
// --- CLI args ---
|
|
36
|
-
const SAMPLE_SIZE = parseInt(process.argv.find((a, i) => process.argv[i - 1] === '--sample') || '500', 10);
|
|
37
|
-
const SEED = parseInt(process.argv.find((a, i) => process.argv[i - 1] === '--seed') || '42', 10);
|
|
38
|
-
const REFRESH = process.argv.includes('--refresh');
|
|
39
|
-
|
|
40
|
-
// --- Seeded PRNG (Mulberry32) ---
|
|
41
|
-
function mulberry32(seed) {
|
|
42
|
-
let s = seed | 0;
|
|
43
|
-
return function() {
|
|
44
|
-
s = (s + 0x6D2B79F5) | 0;
|
|
45
|
-
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
46
|
-
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
47
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// --- Native tgz extraction (same as evaluate.js) ---
|
|
52
|
-
function extractTgz(tgzPath, destDir) {
|
|
53
|
-
const compressed = fs.readFileSync(tgzPath);
|
|
54
|
-
const tarData = zlib.gunzipSync(compressed);
|
|
55
|
-
|
|
56
|
-
let offset = 0;
|
|
57
|
-
while (offset + 512 <= tarData.length) {
|
|
58
|
-
const header = tarData.subarray(offset, offset + 512);
|
|
59
|
-
if (header.every(b => b === 0)) break;
|
|
60
|
-
|
|
61
|
-
const name = header.subarray(0, 100).toString('utf8').replace(/\0+$/, '');
|
|
62
|
-
const sizeOctal = header.subarray(124, 136).toString('utf8').replace(/\0+$/, '').trim();
|
|
63
|
-
const size = parseInt(sizeOctal, 8) || 0;
|
|
64
|
-
const typeFlag = String.fromCharCode(header[156]);
|
|
65
|
-
|
|
66
|
-
offset += 512;
|
|
67
|
-
|
|
68
|
-
if (name && (typeFlag === '0' || typeFlag === '\0') && size > 0) {
|
|
69
|
-
const resolved = path.resolve(destDir, name);
|
|
70
|
-
const rel = path.relative(path.resolve(destDir), resolved);
|
|
71
|
-
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
72
|
-
offset += Math.ceil(size / 512) * 512;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
76
|
-
fs.writeFileSync(resolved, tarData.subarray(offset, offset + size));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
offset += Math.ceil(size / 512) * 512;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function pkgToCacheName(name, version) {
|
|
84
|
-
return (name + '@' + version).replace(/\//g, '_').replace(/@/g, '_');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// --- Step 1: Fetch OSV npm MAL-* index via zip dump ---
|
|
88
|
-
async function fetchOSSFIndex() {
|
|
89
|
-
console.log('\n[1/5] Fetching OSV npm malware index...');
|
|
90
|
-
|
|
91
|
-
// Use the OSV zip dump (same as scrapeOSVDataDump) for the full index
|
|
92
|
-
// This is more complete than the query API which has pagination limits
|
|
93
|
-
const https = require('https');
|
|
94
|
-
const AdmZip = require('adm-zip');
|
|
95
|
-
|
|
96
|
-
const zipUrl = 'https://osv-vulnerabilities.storage.googleapis.com/npm/all.zip';
|
|
97
|
-
|
|
98
|
-
console.log(' Downloading npm OSV zip...');
|
|
99
|
-
const zipBuffer = await new Promise(function(resolve, reject) {
|
|
100
|
-
const chunks = [];
|
|
101
|
-
let totalBytes = 0;
|
|
102
|
-
|
|
103
|
-
https.get(zipUrl, { headers: { 'User-Agent': 'MUADDIB-Scanner/3.0' } }, function(res) {
|
|
104
|
-
if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
105
|
-
https.get(res.headers.location, { headers: { 'User-Agent': 'MUADDIB-Scanner/3.0' } }, function(res2) {
|
|
106
|
-
res2.on('data', function(chunk) {
|
|
107
|
-
chunks.push(chunk);
|
|
108
|
-
totalBytes += chunk.length;
|
|
109
|
-
if (totalBytes % (10 * 1024 * 1024) < chunk.length) {
|
|
110
|
-
process.stdout.write('\r Downloaded: ' + (totalBytes / 1024 / 1024).toFixed(1) + ' MB');
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
res2.on('end', function() {
|
|
114
|
-
process.stdout.write('\r Downloaded: ' + (totalBytes / 1024 / 1024).toFixed(1) + ' MB\n');
|
|
115
|
-
resolve(Buffer.concat(chunks));
|
|
116
|
-
});
|
|
117
|
-
res2.on('error', reject);
|
|
118
|
-
}).on('error', reject);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
res.on('data', function(chunk) {
|
|
123
|
-
chunks.push(chunk);
|
|
124
|
-
totalBytes += chunk.length;
|
|
125
|
-
if (totalBytes % (10 * 1024 * 1024) < chunk.length) {
|
|
126
|
-
process.stdout.write('\r Downloaded: ' + (totalBytes / 1024 / 1024).toFixed(1) + ' MB');
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
res.on('end', function() {
|
|
130
|
-
process.stdout.write('\r Downloaded: ' + (totalBytes / 1024 / 1024).toFixed(1) + ' MB\n');
|
|
131
|
-
resolve(Buffer.concat(chunks));
|
|
132
|
-
});
|
|
133
|
-
res.on('error', reject);
|
|
134
|
-
}).on('error', reject);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
console.log(' Parsing MAL-* entries...');
|
|
138
|
-
const zip = new AdmZip(zipBuffer);
|
|
139
|
-
const entries = zip.getEntries();
|
|
140
|
-
|
|
141
|
-
// Deduplicate by name@version, keep first MAL-* ID encountered
|
|
142
|
-
const dedupMap = new Map(); // key: "name@version" -> entry
|
|
143
|
-
let malCount = 0;
|
|
144
|
-
|
|
145
|
-
for (const entry of entries) {
|
|
146
|
-
const entryName = entry.entryName;
|
|
147
|
-
if (!entryName.startsWith('MAL-') || !entryName.endsWith('.json')) continue;
|
|
148
|
-
|
|
149
|
-
// Size guard
|
|
150
|
-
const entrySize = entry.header ? entry.header.size : 0;
|
|
151
|
-
if (entrySize > 10 * 1024 * 1024) continue; // skip >10MB entries
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const vuln = JSON.parse(entry.getData().toString('utf8'));
|
|
155
|
-
if (!vuln.affected) continue;
|
|
156
|
-
|
|
157
|
-
for (const affected of vuln.affected) {
|
|
158
|
-
if (!affected.package || affected.package.ecosystem !== 'npm') continue;
|
|
159
|
-
|
|
160
|
-
const pkgName = affected.package.name;
|
|
161
|
-
const versions = [];
|
|
162
|
-
|
|
163
|
-
// Extract versions from ranges or explicit list
|
|
164
|
-
if (affected.versions && affected.versions.length > 0) {
|
|
165
|
-
for (const v of affected.versions) versions.push(v);
|
|
166
|
-
} else if (affected.ranges) {
|
|
167
|
-
for (const range of affected.ranges) {
|
|
168
|
-
if (range.events) {
|
|
169
|
-
for (const evt of range.events) {
|
|
170
|
-
if (evt.introduced && evt.introduced !== '0') versions.push(evt.introduced);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// If no specific version, use wildcard marker
|
|
177
|
-
if (versions.length === 0) versions.push('*');
|
|
178
|
-
|
|
179
|
-
// Determine source from database_specific
|
|
180
|
-
let source = 'unknown';
|
|
181
|
-
if (vuln.database_specific && vuln.database_specific['malicious-packages-origins']) {
|
|
182
|
-
const origins = vuln.database_specific['malicious-packages-origins'];
|
|
183
|
-
if (origins.length > 0 && origins[0].source) {
|
|
184
|
-
source = origins[0].source;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
for (const ver of versions) {
|
|
189
|
-
const key = pkgName + '@' + ver;
|
|
190
|
-
if (!dedupMap.has(key)) {
|
|
191
|
-
dedupMap.set(key, {
|
|
192
|
-
name: pkgName,
|
|
193
|
-
version: ver,
|
|
194
|
-
osv_id: vuln.id,
|
|
195
|
-
source: source,
|
|
196
|
-
summary: (vuln.summary || '').slice(0, 200),
|
|
197
|
-
published: vuln.published || null
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
malCount++;
|
|
204
|
-
} catch { /* skip unparseable */ }
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const index = Array.from(dedupMap.values());
|
|
208
|
-
console.log(' Parsed ' + malCount + ' MAL-* reports -> ' + index.length + ' unique name@version entries');
|
|
209
|
-
|
|
210
|
-
return index;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// --- Step 2: Stratified sampling ---
|
|
214
|
-
function stratifySample(index, sampleSize, seed) {
|
|
215
|
-
console.log('\n[2/5] Stratified sampling (' + sampleSize + ' packages, seed=' + seed + ')...');
|
|
216
|
-
|
|
217
|
-
const rng = mulberry32(seed);
|
|
218
|
-
|
|
219
|
-
// Filter out wildcard versions (can't download *)
|
|
220
|
-
const downloadable = index.filter(e => e.version !== '*' && SAFE_PKG_RE.test(e.name));
|
|
221
|
-
console.log(' Downloadable (non-wildcard, valid name): ' + downloadable.length);
|
|
222
|
-
|
|
223
|
-
// Group by source
|
|
224
|
-
const bySource = {};
|
|
225
|
-
for (const entry of downloadable) {
|
|
226
|
-
const src = entry.source || 'unknown';
|
|
227
|
-
if (!bySource[src]) bySource[src] = [];
|
|
228
|
-
bySource[src].push(entry);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
console.log(' Sources: ' + Object.entries(bySource).map(([k, v]) => k + '=' + v.length).join(', '));
|
|
232
|
-
|
|
233
|
-
// Shuffle each source group with seeded RNG
|
|
234
|
-
for (const src of Object.keys(bySource)) {
|
|
235
|
-
const arr = bySource[src];
|
|
236
|
-
for (let i = arr.length - 1; i > 0; i--) {
|
|
237
|
-
const j = Math.floor(rng() * (i + 1));
|
|
238
|
-
const tmp = arr[i];
|
|
239
|
-
arr[i] = arr[j];
|
|
240
|
-
arr[j] = tmp;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Proportional allocation per source
|
|
245
|
-
const sources = Object.keys(bySource);
|
|
246
|
-
const totalDownloadable = downloadable.length;
|
|
247
|
-
const sample = [];
|
|
248
|
-
|
|
249
|
-
for (const src of sources) {
|
|
250
|
-
const proportion = bySource[src].length / totalDownloadable;
|
|
251
|
-
const count = Math.max(1, Math.round(proportion * sampleSize));
|
|
252
|
-
const take = bySource[src].slice(0, count);
|
|
253
|
-
sample.push(...take);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Trim to exact sample size
|
|
257
|
-
while (sample.length > sampleSize) sample.pop();
|
|
258
|
-
|
|
259
|
-
console.log(' Sampled: ' + sample.length + ' packages');
|
|
260
|
-
return sample;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// --- Step 3: Check npm availability ---
|
|
264
|
-
async function checkAvailability(sample) {
|
|
265
|
-
console.log('\n[3/5] Checking npm availability...');
|
|
266
|
-
|
|
267
|
-
let available = 0;
|
|
268
|
-
let unavailable = 0;
|
|
269
|
-
let errors = 0;
|
|
270
|
-
|
|
271
|
-
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
272
|
-
|
|
273
|
-
for (let i = 0; i < sample.length; i++) {
|
|
274
|
-
const entry = sample[i];
|
|
275
|
-
|
|
276
|
-
if (process.stdout.isTTY) {
|
|
277
|
-
process.stdout.write('\r Checking [' + (i + 1) + '/' + sample.length + '] ' + entry.name + '@' + entry.version + ' ');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
execSync(npmCmd + ' view ' + entry.name + '@' + entry.version + ' version --json', {
|
|
282
|
-
encoding: 'utf8',
|
|
283
|
-
timeout: 10000,
|
|
284
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
285
|
-
});
|
|
286
|
-
entry.status = 'available';
|
|
287
|
-
available++;
|
|
288
|
-
} catch (err) {
|
|
289
|
-
const stderr = (err.stderr || '').toLowerCase();
|
|
290
|
-
if (stderr.includes('404') || stderr.includes('not found') || stderr.includes('not in this registry')) {
|
|
291
|
-
entry.status = 'unavailable';
|
|
292
|
-
unavailable++;
|
|
293
|
-
} else {
|
|
294
|
-
entry.status = 'error';
|
|
295
|
-
entry.error = (err.message || '').slice(0, 100);
|
|
296
|
-
errors++;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (process.stdout.isTTY) {
|
|
302
|
-
process.stdout.write('\r' + ''.padEnd(80) + '\r');
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
console.log(' Available: ' + available + ', Unavailable: ' + unavailable + ', Errors: ' + errors);
|
|
306
|
-
return sample;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// --- Step 4: Download and scan ---
|
|
310
|
-
async function downloadAndScan(sample) {
|
|
311
|
-
console.log('\n[4/5] Downloading and scanning available packages...');
|
|
312
|
-
|
|
313
|
-
const { run } = require('../src/index.js');
|
|
314
|
-
const { clearFileListCache } = require('../src/utils.js');
|
|
315
|
-
|
|
316
|
-
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
317
|
-
|
|
318
|
-
const scannable = sample.filter(e => e.status === 'available');
|
|
319
|
-
console.log(' Scannable: ' + scannable.length + ' packages');
|
|
320
|
-
|
|
321
|
-
let scanned = 0;
|
|
322
|
-
let detected = 0;
|
|
323
|
-
let scanErrors = 0;
|
|
324
|
-
let scanCount = 0;
|
|
325
|
-
|
|
326
|
-
for (let i = 0; i < scannable.length; i++) {
|
|
327
|
-
const entry = scannable[i];
|
|
328
|
-
const progress = '[' + (i + 1) + '/' + scannable.length + ']';
|
|
329
|
-
|
|
330
|
-
if (process.stdout.isTTY) {
|
|
331
|
-
process.stdout.write('\r Scanning ' + progress + ' ' + entry.name + '@' + entry.version + ' ');
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Download
|
|
335
|
-
const cacheName = pkgToCacheName(entry.name, entry.version);
|
|
336
|
-
const pkgCacheDir = path.join(CACHE_DIR, cacheName);
|
|
337
|
-
let extractedDir = null;
|
|
338
|
-
|
|
339
|
-
if (!REFRESH && fs.existsSync(path.join(pkgCacheDir, 'package'))) {
|
|
340
|
-
extractedDir = path.join(pkgCacheDir, 'package');
|
|
341
|
-
} else {
|
|
342
|
-
fs.mkdirSync(pkgCacheDir, { recursive: true });
|
|
343
|
-
try {
|
|
344
|
-
const output = execSync('npm pack ' + entry.name + '@' + entry.version, {
|
|
345
|
-
cwd: pkgCacheDir,
|
|
346
|
-
encoding: 'utf8',
|
|
347
|
-
timeout: PACK_TIMEOUT_MS,
|
|
348
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
349
|
-
});
|
|
350
|
-
const tgzFilename = output.trim().split(/\r?\n/).pop().trim();
|
|
351
|
-
const tgzPath = path.join(pkgCacheDir, tgzFilename);
|
|
352
|
-
|
|
353
|
-
if (fs.existsSync(tgzPath)) {
|
|
354
|
-
extractTgz(tgzPath, pkgCacheDir);
|
|
355
|
-
try { fs.unlinkSync(tgzPath); } catch { /* ignore */ }
|
|
356
|
-
if (fs.existsSync(path.join(pkgCacheDir, 'package'))) {
|
|
357
|
-
extractedDir = path.join(pkgCacheDir, 'package');
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
} catch {
|
|
361
|
-
// Download failed — mark as unavailable (possibly removed between check and download)
|
|
362
|
-
entry.status = 'unavailable';
|
|
363
|
-
entry.error = 'npm pack failed';
|
|
364
|
-
fs.rmSync(pkgCacheDir, { recursive: true, force: true });
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (!extractedDir) {
|
|
369
|
-
if (entry.status === 'available') {
|
|
370
|
-
entry.status = 'error';
|
|
371
|
-
entry.error = 'extraction failed';
|
|
372
|
-
scanErrors++;
|
|
373
|
-
}
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Scan
|
|
378
|
-
try {
|
|
379
|
-
const result = await Promise.race([
|
|
380
|
-
run(extractedDir, { _capture: true }),
|
|
381
|
-
new Promise(function(_, reject) {
|
|
382
|
-
setTimeout(function() { reject(new Error('scan timeout')); }, SCAN_TIMEOUT_MS);
|
|
383
|
-
})
|
|
384
|
-
]);
|
|
385
|
-
|
|
386
|
-
const score = result.summary.riskScore || 0;
|
|
387
|
-
entry.score = score;
|
|
388
|
-
entry.detected = score >= 20;
|
|
389
|
-
entry.threat_count = result.summary.total || 0;
|
|
390
|
-
entry.threats = (result.threats || []).slice(0, 20).map(function(t) {
|
|
391
|
-
return { type: t.type, severity: t.severity, file: t.file };
|
|
392
|
-
});
|
|
393
|
-
entry.status = 'scanned';
|
|
394
|
-
|
|
395
|
-
scanned++;
|
|
396
|
-
if (entry.detected) detected++;
|
|
397
|
-
} catch (err) {
|
|
398
|
-
entry.status = 'error';
|
|
399
|
-
entry.error = (err.message || '').slice(0, 100);
|
|
400
|
-
entry.score = 0;
|
|
401
|
-
entry.detected = false;
|
|
402
|
-
scanErrors++;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Memory management
|
|
406
|
-
clearFileListCache();
|
|
407
|
-
scanCount++;
|
|
408
|
-
if (scanCount % 20 === 0 && global.gc) {
|
|
409
|
-
global.gc();
|
|
410
|
-
const used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
411
|
-
console.log('\n [Memory] ' + used + ' MB after ' + scanCount + ' scans');
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (process.stdout.isTTY) {
|
|
416
|
-
process.stdout.write('\r' + ''.padEnd(80) + '\r');
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
console.log(' Scanned: ' + scanned + ', Detected: ' + detected + ', Errors: ' + scanErrors);
|
|
420
|
-
return { scanned, detected, scanErrors };
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// --- Step 5: Save results ---
|
|
424
|
-
function saveResults(sample, index, stats) {
|
|
425
|
-
console.log('\n[5/5] Saving results...');
|
|
426
|
-
|
|
427
|
-
// Compute per-source breakdown
|
|
428
|
-
const bySource = {};
|
|
429
|
-
for (const entry of sample) {
|
|
430
|
-
const src = entry.source || 'unknown';
|
|
431
|
-
if (!bySource[src]) bySource[src] = { total: 0, scanned: 0, detected: 0, unavailable: 0 };
|
|
432
|
-
bySource[src].total++;
|
|
433
|
-
if (entry.status === 'scanned') {
|
|
434
|
-
bySource[src].scanned++;
|
|
435
|
-
if (entry.detected) bySource[src].detected++;
|
|
436
|
-
} else if (entry.status === 'unavailable') {
|
|
437
|
-
bySource[src].unavailable++;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Compute TPR per source
|
|
442
|
-
for (const src of Object.keys(bySource)) {
|
|
443
|
-
const d = bySource[src];
|
|
444
|
-
d.tpr = d.scanned > 0 ? d.detected / d.scanned : 0;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Score distribution
|
|
448
|
-
const scannedEntries = sample.filter(e => e.status === 'scanned');
|
|
449
|
-
const scoreDistribution = { '0': 0, '1-9': 0, '10-19': 0, '20-49': 0, '50-74': 0, '75-100': 0 };
|
|
450
|
-
for (const e of scannedEntries) {
|
|
451
|
-
const s = e.score || 0;
|
|
452
|
-
if (s === 0) scoreDistribution['0']++;
|
|
453
|
-
else if (s <= 9) scoreDistribution['1-9']++;
|
|
454
|
-
else if (s <= 19) scoreDistribution['10-19']++;
|
|
455
|
-
else if (s <= 49) scoreDistribution['20-49']++;
|
|
456
|
-
else if (s <= 74) scoreDistribution['50-74']++;
|
|
457
|
-
else scoreDistribution['75-100']++;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const results = {
|
|
461
|
-
metadata: {
|
|
462
|
-
benchmark: 'OpenSSF Malicious Packages',
|
|
463
|
-
version: 'v1',
|
|
464
|
-
repo: 'https://github.com/ossf/malicious-packages',
|
|
465
|
-
scanned_at: new Date().toISOString(),
|
|
466
|
-
seed: SEED,
|
|
467
|
-
total_osv_npm_unique: index.length,
|
|
468
|
-
sampled: sample.length,
|
|
469
|
-
available_on_npm: sample.filter(e => e.status === 'scanned' || e.status === 'available').length,
|
|
470
|
-
scanned: stats.scanned,
|
|
471
|
-
detected: stats.detected,
|
|
472
|
-
missed: stats.scanned - stats.detected,
|
|
473
|
-
errors: stats.scanErrors,
|
|
474
|
-
unavailable: sample.filter(e => e.status === 'unavailable').length,
|
|
475
|
-
threshold: 20,
|
|
476
|
-
tpr: stats.scanned > 0 ? ((stats.detected / stats.scanned * 100).toFixed(1) + '%') : 'N/A',
|
|
477
|
-
coverage: ((stats.scanned / sample.length * 100).toFixed(1) + '%'),
|
|
478
|
-
by_source: bySource,
|
|
479
|
-
score_distribution: scoreDistribution
|
|
480
|
-
},
|
|
481
|
-
results: sample.map(function(e) {
|
|
482
|
-
return {
|
|
483
|
-
name: e.name,
|
|
484
|
-
version: e.version,
|
|
485
|
-
osv_id: e.osv_id,
|
|
486
|
-
source: e.source,
|
|
487
|
-
status: e.status,
|
|
488
|
-
score: e.score || 0,
|
|
489
|
-
detected: e.detected || false,
|
|
490
|
-
threat_count: e.threat_count || 0,
|
|
491
|
-
threats: e.threats || [],
|
|
492
|
-
error: e.error || undefined
|
|
493
|
-
};
|
|
494
|
-
})
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
fs.mkdirSync(path.dirname(RESULTS_FILE), { recursive: true });
|
|
498
|
-
fs.writeFileSync(RESULTS_FILE, JSON.stringify(results, null, 2));
|
|
499
|
-
console.log(' Saved to: ' + RESULTS_FILE);
|
|
500
|
-
|
|
501
|
-
return results;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// --- Main ---
|
|
505
|
-
async function main() {
|
|
506
|
-
const startTime = Date.now();
|
|
507
|
-
|
|
508
|
-
console.log('='.repeat(60));
|
|
509
|
-
console.log(' MUAD\'DIB OpenSSF Benchmark');
|
|
510
|
-
console.log(' Sample: ' + SAMPLE_SIZE + ', Seed: ' + SEED);
|
|
511
|
-
console.log('='.repeat(60));
|
|
512
|
-
|
|
513
|
-
// 1. Fetch full OSV npm index
|
|
514
|
-
const index = await fetchOSSFIndex();
|
|
515
|
-
|
|
516
|
-
// 2. Stratified sample
|
|
517
|
-
const sample = stratifySample(index, SAMPLE_SIZE, SEED);
|
|
518
|
-
|
|
519
|
-
// 3. Check npm availability
|
|
520
|
-
await checkAvailability(sample);
|
|
521
|
-
|
|
522
|
-
// 4. Download + scan
|
|
523
|
-
const stats = await downloadAndScan(sample);
|
|
524
|
-
|
|
525
|
-
// 5. Save results
|
|
526
|
-
const results = saveResults(sample, index, stats);
|
|
527
|
-
|
|
528
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
529
|
-
|
|
530
|
-
console.log('\n' + '='.repeat(60));
|
|
531
|
-
console.log(' RESULTS');
|
|
532
|
-
console.log('='.repeat(60));
|
|
533
|
-
console.log(' Total OSV npm unique: ' + index.length);
|
|
534
|
-
console.log(' Sampled: ' + sample.length);
|
|
535
|
-
console.log(' Available on npm: ' + results.metadata.available_on_npm);
|
|
536
|
-
console.log(' Scanned: ' + stats.scanned);
|
|
537
|
-
console.log(' Detected (score>=20): ' + stats.detected);
|
|
538
|
-
console.log(' TPR: ' + results.metadata.tpr);
|
|
539
|
-
console.log(' Coverage: ' + results.metadata.coverage);
|
|
540
|
-
console.log(' Elapsed: ' + elapsed + 's');
|
|
541
|
-
console.log('='.repeat(60) + '\n');
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
main().catch(function(err) {
|
|
545
|
-
console.error('[FATAL] ' + err.message);
|
|
546
|
-
console.error(err.stack);
|
|
547
|
-
process.exit(1);
|
|
548
|
-
});
|