bun-scan 1.0.1 → 1.1.0-beta.2

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/dist/cli.js ADDED
@@ -0,0 +1,1349 @@
1
+ // @bun
2
+ // ../core/src/types.ts
3
+ var DEFAULT_SOURCE = "osv";
4
+ // ../core/src/constants.ts
5
+ var OSV_API = {
6
+ BASE_URL: "https://api.osv.dev/v1",
7
+ TIMEOUT_MS: 30000,
8
+ MAX_BATCH_SIZE: 1000,
9
+ MAX_RETRY_ATTEMPTS: 2,
10
+ RETRY_DELAY_MS: 1000,
11
+ DEFAULT_ECOSYSTEM: "npm"
12
+ };
13
+ var HTTP = {
14
+ CONTENT_TYPE: "application/json",
15
+ USER_AGENT: "bun-osv-scanner/1.0.0"
16
+ };
17
+ var SECURITY = {
18
+ CVSS_FATAL_THRESHOLD: 7,
19
+ FATAL_SEVERITIES: ["CRITICAL", "HIGH"],
20
+ MAX_VULNERABILITIES_PER_PACKAGE: 100,
21
+ MAX_DESCRIPTION_LENGTH: 200
22
+ };
23
+ var PERFORMANCE = {
24
+ USE_BATCH_QUERIES: true,
25
+ MAX_CONCURRENT_DETAILS: 10,
26
+ MAX_RESPONSE_SIZE: 32 * 1024 * 1024
27
+ };
28
+ var ENV = {
29
+ LOG_LEVEL: "BUN_SCAN_LOG_LEVEL",
30
+ API_BASE_URL: "OSV_API_BASE_URL",
31
+ TIMEOUT_MS: "OSV_TIMEOUT_MS",
32
+ DISABLE_BATCH: "OSV_DISABLE_BATCH"
33
+ };
34
+ function getConfig(envVar, defaultValue, parser) {
35
+ const envValue = Bun.env[envVar];
36
+ if (!envValue)
37
+ return defaultValue;
38
+ if (parser) {
39
+ try {
40
+ return parser(envValue);
41
+ } catch {
42
+ return defaultValue;
43
+ }
44
+ }
45
+ if (typeof defaultValue === "number") {
46
+ const parsed = Number(envValue);
47
+ return Number.isNaN(parsed) ? defaultValue : parsed;
48
+ }
49
+ if (typeof defaultValue === "boolean") {
50
+ return envValue.toLowerCase() === "true";
51
+ }
52
+ return envValue;
53
+ }
54
+ // ../core/src/logger.ts
55
+ var LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
56
+ function parseLogLevel(level) {
57
+ if (!level)
58
+ return null;
59
+ const normalized = level.toLowerCase();
60
+ return normalized in LEVELS ? normalized : null;
61
+ }
62
+ function safeStringify(obj) {
63
+ try {
64
+ return JSON.stringify(obj);
65
+ } catch {
66
+ return "[Circular]";
67
+ }
68
+ }
69
+ function formatMessage(level, message, context) {
70
+ const timestamp = new Date().toISOString();
71
+ const prefix = `[${timestamp}] OSV-${level.toUpperCase()}:`;
72
+ const contextStr = context ? ` ${safeStringify(context)}` : "";
73
+ return `${prefix} ${message}${contextStr}`;
74
+ }
75
+ var noop = () => {};
76
+ var realDebug = (message, context) => console.debug(formatMessage("debug", message, context));
77
+ var realInfo = (message, context) => console.info(formatMessage("info", message, context));
78
+ var realWarn = (message, context) => console.warn(formatMessage("warn", message, context));
79
+ var realError = (message, context) => console.error(formatMessage("error", message, context));
80
+ function createLogger(level) {
81
+ const minLevel = LEVELS[level ?? parseLogLevel(Bun.env[ENV.LOG_LEVEL]) ?? "info"];
82
+ return {
83
+ debug: minLevel <= LEVELS.debug ? realDebug : noop,
84
+ info: minLevel <= LEVELS.info ? realInfo : noop,
85
+ warn: minLevel <= LEVELS.warn ? realWarn : noop,
86
+ error: realError
87
+ };
88
+ }
89
+ var logger = createLogger();
90
+ // ../core/src/retry.ts
91
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
92
+ var sleep = defaultSleep;
93
+ var DEFAULT_RETRY_CONFIG = {
94
+ maxAttempts: OSV_API.MAX_RETRY_ATTEMPTS + 1,
95
+ delayMs: OSV_API.RETRY_DELAY_MS,
96
+ shouldRetry: (error) => {
97
+ if (error.message.includes("400") || error.message.includes("401") || error.message.includes("403")) {
98
+ return false;
99
+ }
100
+ if (error.message.includes("404")) {
101
+ return false;
102
+ }
103
+ return true;
104
+ }
105
+ };
106
+ async function withRetry(operation, operationName, config = DEFAULT_RETRY_CONFIG) {
107
+ let lastError = new Error("Unknown error");
108
+ for (let attempt = 1;attempt <= config.maxAttempts; attempt++) {
109
+ try {
110
+ const result = await operation();
111
+ if (attempt > 1) {
112
+ logger.info(`${operationName} succeeded on attempt ${attempt}`);
113
+ }
114
+ return result;
115
+ } catch (error) {
116
+ lastError = error instanceof Error ? error : new Error(String(error));
117
+ const isLastAttempt = attempt === config.maxAttempts;
118
+ const shouldRetry = config.shouldRetry?.(lastError) ?? true;
119
+ if (isLastAttempt || !shouldRetry) {
120
+ logger.error(`${operationName} failed after ${attempt} attempts`, {
121
+ error: lastError.message,
122
+ attempts: attempt
123
+ });
124
+ break;
125
+ }
126
+ const delay = config.delayMs * 1.5 ** (attempt - 1);
127
+ logger.warn(`${operationName} attempt ${attempt} failed, retrying in ${delay}ms`, {
128
+ error: lastError.message,
129
+ nextDelay: delay
130
+ });
131
+ await sleep(delay);
132
+ }
133
+ }
134
+ throw lastError;
135
+ }
136
+ // ../core/src/config.ts
137
+ import { z } from "zod";
138
+ var IgnorePackageRuleSchema = z.object({
139
+ vulnerabilities: z.array(z.string()).optional(),
140
+ until: z.string().optional(),
141
+ reason: z.string().optional()
142
+ });
143
+ var IgnoreConfigSchema = z.object({
144
+ ignore: z.array(z.string()).optional(),
145
+ packages: z.record(z.string(), IgnorePackageRuleSchema).optional()
146
+ });
147
+ var ConfigSchema = z.object({
148
+ source: z.enum(["osv", "npm", "both"]).catch(DEFAULT_SOURCE).optional(),
149
+ ignore: z.array(z.string()).optional(),
150
+ packages: z.record(z.string(), IgnorePackageRuleSchema).optional()
151
+ });
152
+ function compileIgnoreConfig(config) {
153
+ const ignoreSet = new Set(config.ignore ?? []);
154
+ const packages = new Map;
155
+ for (const [name, rule] of Object.entries(config.packages ?? {})) {
156
+ packages.set(name, {
157
+ vulnerabilitiesSet: new Set(rule.vulnerabilities ?? []),
158
+ until: rule.until,
159
+ reason: rule.reason
160
+ });
161
+ }
162
+ return { ignoreSet, packages };
163
+ }
164
+ var CONFIG_FILES = [".bun-scan.json", ".bun-scan.config.json"];
165
+ async function loadConfig() {
166
+ for (const filename of CONFIG_FILES) {
167
+ const config = await tryLoadConfigFile(filename);
168
+ if (config) {
169
+ return { source: DEFAULT_SOURCE, ...config };
170
+ }
171
+ }
172
+ return { source: DEFAULT_SOURCE };
173
+ }
174
+ async function tryLoadConfigFile(filename) {
175
+ try {
176
+ const file = Bun.file(filename);
177
+ const exists = await file.exists();
178
+ if (!exists) {
179
+ return null;
180
+ }
181
+ const content = await file.json();
182
+ const parsed = ConfigSchema.parse(content);
183
+ logger.info(`Loaded configuration from ${filename}`);
184
+ logConfigStats(parsed);
185
+ return parsed;
186
+ } catch (error) {
187
+ if (error instanceof z.ZodError) {
188
+ logger.warn(`Invalid config in ${filename}`, {
189
+ errors: error.issues.map((e) => `${e.path.join(".")}: ${e.message}`)
190
+ });
191
+ } else if (error instanceof SyntaxError) {
192
+ logger.warn(`Failed to parse ${filename} as JSON`, {
193
+ error: error.message
194
+ });
195
+ }
196
+ return null;
197
+ }
198
+ }
199
+ function logConfigStats(config) {
200
+ const globalIgnores = config.ignore?.length ?? 0;
201
+ const packageRules = Object.keys(config.packages ?? {}).length;
202
+ const source = config.source ?? DEFAULT_SOURCE;
203
+ if (globalIgnores > 0 || packageRules > 0) {
204
+ logger.info(`Configuration loaded`, {
205
+ source,
206
+ globalIgnores,
207
+ packageRules
208
+ });
209
+ }
210
+ }
211
+ function shouldIgnoreVulnerability(vulnId, vulnAliases, packageName, config) {
212
+ const idsToCheck = [vulnId, ...vulnAliases ?? []];
213
+ for (const id of idsToCheck) {
214
+ if (config.ignoreSet.has(id)) {
215
+ return { ignored: true, reason: `globally ignored (${id})` };
216
+ }
217
+ }
218
+ if (packageName) {
219
+ const rule = config.packages.get(packageName);
220
+ if (rule) {
221
+ if (rule.until) {
222
+ const untilDate = new Date(rule.until);
223
+ if (untilDate < new Date) {
224
+ logger.debug(`Ignore rule for ${packageName} expired on ${rule.until}`);
225
+ return { ignored: false };
226
+ }
227
+ }
228
+ for (const id of idsToCheck) {
229
+ if (rule.vulnerabilitiesSet.has(id)) {
230
+ const reason = rule.reason ? `package rule: ${rule.reason}` : `ignored for ${packageName} (${id})`;
231
+ return { ignored: true, reason };
232
+ }
233
+ }
234
+ }
235
+ }
236
+ return { ignored: false };
237
+ }
238
+ // ../source-osv/src/schema.ts
239
+ import { z as z2 } from "zod";
240
+ var OSVQuerySchema = z2.object({
241
+ commit: z2.string().optional(),
242
+ version: z2.string().optional(),
243
+ package: z2.object({
244
+ name: z2.string(),
245
+ ecosystem: z2.string(),
246
+ purl: z2.string().optional()
247
+ }).optional(),
248
+ page_token: z2.string().optional()
249
+ });
250
+ var OSVAffectedSchema = z2.object({
251
+ package: z2.object({
252
+ name: z2.string(),
253
+ ecosystem: z2.string(),
254
+ purl: z2.string().optional()
255
+ }),
256
+ ranges: z2.array(z2.object({
257
+ type: z2.string(),
258
+ repo: z2.string().optional(),
259
+ events: z2.array(z2.object({
260
+ introduced: z2.string().optional(),
261
+ fixed: z2.string().optional(),
262
+ last_affected: z2.string().optional()
263
+ }))
264
+ })).optional(),
265
+ versions: z2.array(z2.string()).optional(),
266
+ ecosystem_specific: z2.record(z2.string(), z2.any()).optional(),
267
+ database_specific: z2.record(z2.string(), z2.any()).optional()
268
+ });
269
+ var OSVVulnerabilitySchema = z2.object({
270
+ id: z2.string(),
271
+ summary: z2.string().optional(),
272
+ details: z2.string().optional(),
273
+ modified: z2.string().optional(),
274
+ published: z2.string().optional(),
275
+ withdrawn: z2.string().optional(),
276
+ aliases: z2.array(z2.string()).optional(),
277
+ related: z2.array(z2.string()).optional(),
278
+ schema_version: z2.string().optional(),
279
+ affected: z2.array(OSVAffectedSchema).optional(),
280
+ references: z2.array(z2.object({
281
+ type: z2.string().optional(),
282
+ url: z2.string()
283
+ })).optional(),
284
+ database_specific: z2.record(z2.string(), z2.any()).optional(),
285
+ severity: z2.array(z2.object({
286
+ type: z2.string(),
287
+ score: z2.string()
288
+ })).optional(),
289
+ ecosystem_specific: z2.record(z2.string(), z2.any()).optional(),
290
+ credits: z2.array(z2.object({
291
+ name: z2.string(),
292
+ contact: z2.array(z2.string()).optional(),
293
+ type: z2.string().optional()
294
+ })).optional()
295
+ });
296
+ var OSVResponseSchema = z2.object({
297
+ vulns: z2.array(OSVVulnerabilitySchema).optional(),
298
+ next_page_token: z2.string().optional()
299
+ });
300
+ var OSVBatchQuerySchema = z2.object({
301
+ queries: z2.array(OSVQuerySchema)
302
+ });
303
+ var OSVBatchResponseSchema = z2.object({
304
+ results: z2.array(z2.object({
305
+ vulns: z2.array(z2.object({
306
+ id: z2.string(),
307
+ modified: z2.string()
308
+ })).optional(),
309
+ next_page_token: z2.string().optional()
310
+ }))
311
+ });
312
+
313
+ // ../source-osv/src/client.ts
314
+ function createOSVClient() {
315
+ const baseUrl = getConfig(ENV.API_BASE_URL, OSV_API.BASE_URL);
316
+ const timeout = getConfig(ENV.TIMEOUT_MS, OSV_API.TIMEOUT_MS);
317
+ const useBatch = !getConfig(ENV.DISABLE_BATCH, false);
318
+ function deduplicatePackages(packages) {
319
+ const packageMap = new Map;
320
+ for (const pkg of packages) {
321
+ const key = `${pkg.name}@${pkg.version}`;
322
+ if (!packageMap.has(key)) {
323
+ packageMap.set(key, pkg);
324
+ }
325
+ }
326
+ const uniquePackages = Array.from(packageMap.values());
327
+ if (uniquePackages.length < packages.length) {
328
+ logger.debug(`Deduplicated ${packages.length} packages to ${uniquePackages.length} unique packages`);
329
+ }
330
+ return uniquePackages;
331
+ }
332
+ async function executeBatchQuery(queries) {
333
+ const vulnerabilityIds = [];
334
+ const response = await withRetry(async () => {
335
+ const res = await fetch(`${baseUrl}/querybatch`, {
336
+ method: "POST",
337
+ headers: {
338
+ "Content-Type": HTTP.CONTENT_TYPE,
339
+ "User-Agent": HTTP.USER_AGENT
340
+ },
341
+ body: JSON.stringify({ queries }),
342
+ signal: AbortSignal.timeout(timeout)
343
+ });
344
+ if (!res.ok) {
345
+ throw new Error(`OSV API returned ${res.status}: ${res.statusText}`);
346
+ }
347
+ return res;
348
+ }, `OSV batch query (${queries.length} packages)`);
349
+ const data = await response.json();
350
+ const parsed = OSVBatchResponseSchema.parse(data);
351
+ for (const result of parsed.results) {
352
+ if (result.vulns) {
353
+ vulnerabilityIds.push(...result.vulns.map((v) => v.id));
354
+ }
355
+ }
356
+ const vulnCount = parsed.results.reduce((sum, r) => sum + (r.vulns?.length || 0), 0);
357
+ logger.info(`Batch query found ${vulnCount} vulnerabilities across ${queries.length} packages`);
358
+ return [...new Set(vulnerabilityIds)];
359
+ }
360
+ async function fetchSingleVulnerability(id) {
361
+ try {
362
+ return await withRetry(async () => {
363
+ const response = await fetch(`${baseUrl}/vulns/${id}`, {
364
+ headers: {
365
+ "User-Agent": HTTP.USER_AGENT
366
+ },
367
+ signal: AbortSignal.timeout(timeout)
368
+ });
369
+ if (!response.ok) {
370
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
371
+ }
372
+ const data = await response.json();
373
+ return OSVVulnerabilitySchema.parse(data);
374
+ }, `Get vulnerability ${id}`);
375
+ } catch (error) {
376
+ logger.warn(`Failed to fetch vulnerability ${id}`, {
377
+ error: error instanceof Error ? error.message : String(error)
378
+ });
379
+ return null;
380
+ }
381
+ }
382
+ async function fetchVulnerabilityDetails(ids) {
383
+ if (ids.length === 0)
384
+ return [];
385
+ const uniqueIds = [...new Set(ids)];
386
+ logger.info(`Fetching details for ${uniqueIds.length} vulnerabilities`);
387
+ const chunkSize = PERFORMANCE.MAX_CONCURRENT_DETAILS;
388
+ const vulnerabilities = [];
389
+ for (let i = 0;i < uniqueIds.length; i += chunkSize) {
390
+ const chunk = uniqueIds.slice(i, i + chunkSize);
391
+ const chunkResults = await Promise.allSettled(chunk.map((id) => fetchSingleVulnerability(id)));
392
+ for (const result of chunkResults) {
393
+ if (result.status === "fulfilled" && result.value) {
394
+ vulnerabilities.push(result.value);
395
+ }
396
+ }
397
+ }
398
+ logger.info(`Retrieved ${vulnerabilities.length}/${uniqueIds.length} vulnerability details`);
399
+ return vulnerabilities;
400
+ }
401
+ async function queryWithBatch(queries) {
402
+ const vulnerabilityIds = [];
403
+ for (let i = 0;i < queries.length; i += OSV_API.MAX_BATCH_SIZE) {
404
+ const batchQueries = queries.slice(i, i + OSV_API.MAX_BATCH_SIZE);
405
+ try {
406
+ const batchIds = await executeBatchQuery(batchQueries);
407
+ vulnerabilityIds.push(...batchIds);
408
+ } catch (error) {
409
+ logger.error(`Batch query failed for ${batchQueries.length} packages`, {
410
+ error: error instanceof Error ? error.message : String(error),
411
+ startIndex: i
412
+ });
413
+ }
414
+ }
415
+ return await fetchVulnerabilityDetails(vulnerabilityIds);
416
+ }
417
+ async function querySinglePackage(query) {
418
+ const allVulns = [];
419
+ let currentQuery = { ...query };
420
+ while (true) {
421
+ try {
422
+ const response = await withRetry(async () => {
423
+ const res = await fetch(`${baseUrl}/query`, {
424
+ method: "POST",
425
+ headers: {
426
+ "Content-Type": HTTP.CONTENT_TYPE,
427
+ "User-Agent": HTTP.USER_AGENT
428
+ },
429
+ body: JSON.stringify(currentQuery),
430
+ signal: AbortSignal.timeout(timeout)
431
+ });
432
+ if (!res.ok) {
433
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
434
+ }
435
+ return res;
436
+ }, `OSV query for ${query.package?.name || "unknown"}@${query.version || "unknown"}`);
437
+ const data = await response.json();
438
+ const parsed = OSVResponseSchema.parse(data);
439
+ if (parsed.vulns) {
440
+ allVulns.push(...parsed.vulns);
441
+ }
442
+ if (parsed.next_page_token) {
443
+ currentQuery = { ...query, page_token: parsed.next_page_token };
444
+ } else {
445
+ break;
446
+ }
447
+ } catch (error) {
448
+ logger.warn(`Query failed for ${query.package?.name || "unknown"}@${query.version || "unknown"}`, {
449
+ error: error instanceof Error ? error.message : String(error)
450
+ });
451
+ break;
452
+ }
453
+ }
454
+ return allVulns;
455
+ }
456
+ async function queryIndividually(queries) {
457
+ const responses = await Promise.allSettled(queries.map((query) => querySinglePackage(query)));
458
+ const vulnerabilities = [];
459
+ let successCount = 0;
460
+ for (const response of responses) {
461
+ if (response.status === "fulfilled") {
462
+ vulnerabilities.push(...response.value);
463
+ successCount++;
464
+ }
465
+ }
466
+ logger.info(`Individual queries completed: ${successCount}/${queries.length} successful`);
467
+ return vulnerabilities;
468
+ }
469
+ async function queryVulnerabilities(packages) {
470
+ if (packages.length === 0) {
471
+ return [];
472
+ }
473
+ const uniquePackages = deduplicatePackages(packages);
474
+ logger.info(`Scanning ${uniquePackages.length} unique packages (${packages.length} total)`);
475
+ const queries = uniquePackages.map((pkg) => ({
476
+ package: {
477
+ name: pkg.name,
478
+ ecosystem: OSV_API.DEFAULT_ECOSYSTEM
479
+ },
480
+ version: pkg.version
481
+ }));
482
+ if (useBatch && queries.length > 1) {
483
+ return await queryWithBatch(queries);
484
+ } else {
485
+ return await queryIndividually(queries);
486
+ }
487
+ }
488
+ return {
489
+ queryVulnerabilities
490
+ };
491
+ }
492
+
493
+ // ../source-osv/src/semver.ts
494
+ function isPackageAffected(pkg, affected) {
495
+ if (affected.package.name !== pkg.name) {
496
+ return false;
497
+ }
498
+ if (affected.versions?.includes(pkg.version)) {
499
+ logger.debug(`Package ${pkg.name}@${pkg.version} found in explicit versions list`);
500
+ return true;
501
+ }
502
+ if (affected.ranges) {
503
+ for (const range of affected.ranges) {
504
+ if (isVersionInRange(pkg.version, range)) {
505
+ return true;
506
+ }
507
+ }
508
+ }
509
+ return false;
510
+ }
511
+ function isVersionInRange(version, range) {
512
+ if (!range.events || range.events.length === 0) {
513
+ return false;
514
+ }
515
+ if (range.type === "SEMVER") {
516
+ return isVersionInSemverRange(version, range);
517
+ }
518
+ logger.debug(`Unsupported range type: ${range.type}`, { range });
519
+ return false;
520
+ }
521
+ function isVersionInSemverRange(version, range) {
522
+ try {
523
+ const rangeGroups = [];
524
+ let currentGroup = [];
525
+ for (const event of range.events) {
526
+ if (event.introduced) {
527
+ if (currentGroup.length > 0) {
528
+ rangeGroups.push(currentGroup.join(" "));
529
+ currentGroup = [];
530
+ }
531
+ if (event.introduced === "0") {
532
+ currentGroup.push("*");
533
+ } else {
534
+ currentGroup.push(`>=${event.introduced}`);
535
+ }
536
+ }
537
+ if (event.fixed) {
538
+ currentGroup.push(`<${event.fixed}`);
539
+ rangeGroups.push(currentGroup.join(" "));
540
+ currentGroup = [];
541
+ }
542
+ if (event.last_affected) {
543
+ currentGroup.push(`<=${event.last_affected}`);
544
+ rangeGroups.push(currentGroup.join(" "));
545
+ currentGroup = [];
546
+ }
547
+ }
548
+ if (currentGroup.length > 0) {
549
+ rangeGroups.push(currentGroup.join(" "));
550
+ }
551
+ if (rangeGroups.length === 0) {
552
+ return false;
553
+ }
554
+ const combinedRange = rangeGroups.join(" || ");
555
+ logger.debug(`Checking ${version} against range: ${combinedRange}`);
556
+ return Bun.semver.satisfies(version, combinedRange);
557
+ } catch (error) {
558
+ logger.warn(`Failed to parse semver range`, {
559
+ version,
560
+ range: range.events,
561
+ error: error instanceof Error ? error.message : String(error)
562
+ });
563
+ return false;
564
+ }
565
+ }
566
+
567
+ // ../source-osv/src/severity.ts
568
+ function mapSeverityToLevel(vuln) {
569
+ const dbSeverity = vuln.database_specific?.severity;
570
+ if (dbSeverity && isFatalSeverity(dbSeverity)) {
571
+ logger.debug(`Vulnerability ${vuln.id} marked fatal due to database severity: ${dbSeverity}`);
572
+ return "fatal";
573
+ }
574
+ if (vuln.severity) {
575
+ const cvssScore = extractHighestCVSSScore(vuln.severity);
576
+ if (cvssScore !== null && cvssScore >= SECURITY.CVSS_FATAL_THRESHOLD) {
577
+ logger.debug(`Vulnerability ${vuln.id} marked fatal due to CVSS score: ${cvssScore}`);
578
+ return "fatal";
579
+ }
580
+ }
581
+ logger.debug(`Vulnerability ${vuln.id} marked as warning (default)`);
582
+ return "warn";
583
+ }
584
+ function isFatalSeverity(severity) {
585
+ return typeof severity === "string" && SECURITY.FATAL_SEVERITIES.includes(severity);
586
+ }
587
+ function extractHighestCVSSScore(severities) {
588
+ if (!Array.isArray(severities))
589
+ return null;
590
+ let highestScore = null;
591
+ for (const severity of severities) {
592
+ if (!severity.type.startsWith("CVSS"))
593
+ continue;
594
+ const score = parseCVSSScore(severity.score, severity.type);
595
+ if (score !== null && (highestScore === null || score > highestScore)) {
596
+ highestScore = score;
597
+ }
598
+ }
599
+ return highestScore;
600
+ }
601
+ function parseCVSSScore(scoreString, type) {
602
+ try {
603
+ if (scoreString.includes("CVSS:")) {
604
+ const vectorMatch = scoreString.match(/CVSS:[\d.]+\/.*?(\d+\.\d+|\d+)$/);
605
+ if (vectorMatch?.[1]) {
606
+ return parseFloat(vectorMatch[1]);
607
+ }
608
+ const scoreMatch = scoreString.match(/(\d+\.\d+|\d+)$/);
609
+ if (scoreMatch?.[1]) {
610
+ return parseFloat(scoreMatch[1]);
611
+ }
612
+ }
613
+ const numericScore = parseFloat(scoreString);
614
+ if (!Number.isNaN(numericScore) && numericScore >= 0 && numericScore <= 10) {
615
+ return numericScore;
616
+ }
617
+ logger.debug(`Failed to parse CVSS score`, { type, scoreString });
618
+ return null;
619
+ } catch (error) {
620
+ logger.warn(`Error parsing CVSS score`, {
621
+ type,
622
+ scoreString,
623
+ error: error instanceof Error ? error.message : String(error)
624
+ });
625
+ return null;
626
+ }
627
+ }
628
+
629
+ // ../source-osv/src/processor.ts
630
+ var HOSTNAME_REGEX = /^https?:\/\/([^/:?#]+)(?::\d+)?(?:[/?#]|$)/;
631
+ var CVE_HOSTS = new Set(["cve.mitre.org", "nvd.nist.gov"]);
632
+ function createVulnerabilityProcessor(ignoreConfig = {}) {
633
+ const compiledIgnoreConfig = compileIgnoreConfig(ignoreConfig);
634
+ let ignoredCount = 0;
635
+ function getVulnerabilityUrl(vuln) {
636
+ if (!vuln.references || vuln.references.length === 0) {
637
+ return null;
638
+ }
639
+ const advisoryRef = vuln.references.find((ref) => ref.type === "ADVISORY" || ref.url.includes("github.com/advisories"));
640
+ if (advisoryRef) {
641
+ return advisoryRef.url;
642
+ }
643
+ const cveRef = vuln.references.find((ref) => {
644
+ const match = ref.url.match(HOSTNAME_REGEX);
645
+ const hostname = match?.[1]?.toLowerCase();
646
+ return hostname ? CVE_HOSTS.has(hostname) : false;
647
+ });
648
+ if (cveRef) {
649
+ return cveRef.url;
650
+ }
651
+ return vuln.references[0]?.url || null;
652
+ }
653
+ function getVulnerabilityDescription(vuln) {
654
+ if (vuln.summary?.trim()) {
655
+ return vuln.summary.trim();
656
+ }
657
+ if (vuln.details?.trim()) {
658
+ const details = vuln.details.trim();
659
+ if (details.length <= SECURITY.MAX_DESCRIPTION_LENGTH) {
660
+ return details;
661
+ }
662
+ const firstSentence = details.match(/^[^.!?]*[.!?]/)?.[0];
663
+ if (firstSentence && firstSentence.length <= SECURITY.MAX_DESCRIPTION_LENGTH) {
664
+ return firstSentence;
665
+ }
666
+ return `${details.substring(0, SECURITY.MAX_DESCRIPTION_LENGTH - 3)}...`;
667
+ }
668
+ return null;
669
+ }
670
+ function createAdvisory(vuln, pkg) {
671
+ const level = mapSeverityToLevel(vuln);
672
+ const url = getVulnerabilityUrl(vuln);
673
+ const description = getVulnerabilityDescription(vuln);
674
+ return {
675
+ id: vuln.id,
676
+ message: vuln.summary || vuln.details || vuln.id,
677
+ level,
678
+ package: pkg.name,
679
+ url,
680
+ description
681
+ };
682
+ }
683
+ function processVulnerability(vuln, packagesByName, processedPairs, compiledConfig) {
684
+ const advisories = [];
685
+ if (!vuln.affected) {
686
+ logger.debug(`Vulnerability ${vuln.id} has no affected packages`);
687
+ return advisories;
688
+ }
689
+ for (const affected of vuln.affected) {
690
+ const matchingPackages = packagesByName.get(affected.package.name);
691
+ if (!matchingPackages) {
692
+ continue;
693
+ }
694
+ for (const pkg of matchingPackages) {
695
+ const pairKey = `${vuln.id}:${pkg.name}@${pkg.version}`;
696
+ if (processedPairs.has(pairKey)) {
697
+ continue;
698
+ }
699
+ if (isPackageAffected(pkg, affected)) {
700
+ const ignoreResult = shouldIgnoreVulnerability(vuln.id, vuln.aliases, pkg.name, compiledConfig);
701
+ if (ignoreResult.ignored) {
702
+ logger.debug(`Ignoring ${vuln.id} for ${pkg.name}: ${ignoreResult.reason}`);
703
+ ignoredCount++;
704
+ processedPairs.add(pairKey);
705
+ continue;
706
+ }
707
+ const advisory = createAdvisory(vuln, pkg);
708
+ advisories.push(advisory);
709
+ processedPairs.add(pairKey);
710
+ logger.debug(`Created advisory for ${pkg.name}@${pkg.version}`, {
711
+ vulnerability: vuln.id,
712
+ level: advisory.level
713
+ });
714
+ break;
715
+ }
716
+ }
717
+ }
718
+ return advisories;
719
+ }
720
+ function processVulnerabilities(vulnerabilities, packages) {
721
+ if (vulnerabilities.length === 0 || packages.length === 0) {
722
+ return [];
723
+ }
724
+ logger.info(`Processing ${vulnerabilities.length} vulnerabilities against ${packages.length} packages`);
725
+ const packagesByName = new Map;
726
+ for (const pkg of packages) {
727
+ const existing = packagesByName.get(pkg.name);
728
+ if (existing) {
729
+ existing.push(pkg);
730
+ } else {
731
+ packagesByName.set(pkg.name, [pkg]);
732
+ }
733
+ }
734
+ const advisories = [];
735
+ const processedPairs = new Set;
736
+ ignoredCount = 0;
737
+ for (const vuln of vulnerabilities) {
738
+ const vulnAdvisories = processVulnerability(vuln, packagesByName, processedPairs, compiledIgnoreConfig);
739
+ advisories.push(...vulnAdvisories);
740
+ }
741
+ if (ignoredCount > 0) {
742
+ logger.info(`Ignored ${ignoredCount} vulnerabilities based on config`);
743
+ }
744
+ logger.info(`Generated ${advisories.length} security advisories`);
745
+ return advisories;
746
+ }
747
+ return {
748
+ processVulnerabilities
749
+ };
750
+ }
751
+
752
+ // ../source-osv/src/index.ts
753
+ function createOSVSource(ignoreConfig = {}) {
754
+ const client = createOSVClient();
755
+ const processor = createVulnerabilityProcessor(ignoreConfig);
756
+ return {
757
+ name: "osv",
758
+ async scan(packages) {
759
+ logger.info(`[OSV] Starting scan for ${packages.length} packages`);
760
+ const vulnerabilities = await client.queryVulnerabilities(packages);
761
+ const advisories = processor.processVulnerabilities(vulnerabilities, packages);
762
+ logger.info(`[OSV] Scan complete: ${advisories.length} advisories found`);
763
+ return advisories;
764
+ }
765
+ };
766
+ }
767
+
768
+ // ../source-npm/src/schema.ts
769
+ import { z as z3 } from "zod";
770
+ var NpmAuditRequestSchema = z3.record(z3.string(), z3.array(z3.string()));
771
+ var NpmAdvisorySchema = z3.object({
772
+ id: z3.union([z3.string(), z3.number()]),
773
+ title: z3.string(),
774
+ name: z3.string().optional(),
775
+ module_name: z3.string().optional(),
776
+ severity: z3.enum(["critical", "high", "moderate", "low", "info"]),
777
+ vulnerable_versions: z3.string(),
778
+ patched_versions: z3.string().optional(),
779
+ url: z3.string(),
780
+ overview: z3.string().optional(),
781
+ recommendation: z3.string().optional(),
782
+ references: z3.string().optional(),
783
+ access: z3.string().optional(),
784
+ cwe: z3.union([z3.string(), z3.array(z3.string())]).optional(),
785
+ cves: z3.array(z3.string()).optional(),
786
+ cvss: z3.object({
787
+ score: z3.number(),
788
+ vectorString: z3.string().nullable().optional()
789
+ }).optional(),
790
+ findings: z3.array(z3.object({
791
+ version: z3.string(),
792
+ paths: z3.array(z3.string())
793
+ })).optional(),
794
+ created: z3.string().optional(),
795
+ updated: z3.string().optional(),
796
+ deleted: z3.boolean().optional(),
797
+ github_advisory_id: z3.string().optional()
798
+ });
799
+ var NpmAuditResponseSchema = z3.record(z3.string(), z3.array(NpmAdvisorySchema));
800
+
801
+ // ../source-npm/src/constants.ts
802
+ var NPM_AUDIT_API = {
803
+ REGISTRY_URL: "https://registry.npmjs.org",
804
+ BULK_ADVISORY_PATH: "/-/npm/v1/security/advisories/bulk",
805
+ TIMEOUT_MS: 30000,
806
+ MAX_RETRY_ATTEMPTS: 2,
807
+ RETRY_DELAY_MS: 1000,
808
+ MAX_PACKAGES_PER_REQUEST: 1000
809
+ };
810
+ var HTTP2 = {
811
+ CONTENT_TYPE: "application/json",
812
+ CONTENT_ENCODING: "gzip",
813
+ USER_AGENT: "@bun-security-scanner/npm/1.0.0"
814
+ };
815
+ var SECURITY2 = {
816
+ CVSS_FATAL_THRESHOLD: 7,
817
+ FATAL_SEVERITIES: ["critical", "high"],
818
+ MAX_VULNERABILITIES_PER_PACKAGE: 100,
819
+ MAX_DESCRIPTION_LENGTH: 200
820
+ };
821
+ var ENV2 = {
822
+ REGISTRY_URL: "NPM_SCANNER_REGISTRY_URL",
823
+ TIMEOUT_MS: "NPM_SCANNER_TIMEOUT_MS"
824
+ };
825
+ function getConfig2(envVar, defaultValue, parser) {
826
+ const envValue = Bun.env[envVar];
827
+ if (!envValue)
828
+ return defaultValue;
829
+ if (parser) {
830
+ try {
831
+ return parser(envValue);
832
+ } catch {
833
+ return defaultValue;
834
+ }
835
+ }
836
+ if (typeof defaultValue === "number") {
837
+ const parsed = Number(envValue);
838
+ return Number.isNaN(parsed) ? defaultValue : parsed;
839
+ }
840
+ if (typeof defaultValue === "boolean") {
841
+ return envValue.toLowerCase() === "true";
842
+ }
843
+ return envValue;
844
+ }
845
+
846
+ // ../source-npm/src/client.ts
847
+ function createNpmAuditClient() {
848
+ const registryUrl = getConfig2(ENV2.REGISTRY_URL, NPM_AUDIT_API.REGISTRY_URL);
849
+ const timeout = getConfig2(ENV2.TIMEOUT_MS, NPM_AUDIT_API.TIMEOUT_MS);
850
+ function deduplicatePackages(packages) {
851
+ const packageMap = new Map;
852
+ for (const pkg of packages) {
853
+ const key = `${pkg.name}@${pkg.version}`;
854
+ if (!packageMap.has(key)) {
855
+ packageMap.set(key, pkg);
856
+ }
857
+ }
858
+ const uniquePackages = Array.from(packageMap.values());
859
+ if (uniquePackages.length < packages.length) {
860
+ logger.debug(`Deduplicated ${packages.length} packages to ${uniquePackages.length} unique packages`);
861
+ }
862
+ return uniquePackages;
863
+ }
864
+ function buildRequestPayload(packages) {
865
+ const payload = {};
866
+ for (const pkg of packages) {
867
+ if (!payload[pkg.name]) {
868
+ payload[pkg.name] = [];
869
+ }
870
+ const versions = payload[pkg.name];
871
+ if (versions && !versions.includes(pkg.version)) {
872
+ versions.push(pkg.version);
873
+ }
874
+ }
875
+ return payload;
876
+ }
877
+ async function executeBulkQuery(payload) {
878
+ const packageCount = Object.keys(payload).length;
879
+ const versionCount = Object.values(payload).reduce((sum, versions) => sum + versions.length, 0);
880
+ logger.debug(`Querying ${packageCount} packages with ${versionCount} versions`);
881
+ const jsonPayload = JSON.stringify(payload);
882
+ const compressedPayload = Bun.gzipSync(jsonPayload);
883
+ const response = await withRetry(async () => {
884
+ const url = `${registryUrl}${NPM_AUDIT_API.BULK_ADVISORY_PATH}`;
885
+ const res = await fetch(url, {
886
+ method: "POST",
887
+ headers: {
888
+ "Content-Type": HTTP2.CONTENT_TYPE,
889
+ "Content-Encoding": HTTP2.CONTENT_ENCODING,
890
+ "User-Agent": HTTP2.USER_AGENT,
891
+ Accept: HTTP2.CONTENT_TYPE
892
+ },
893
+ body: compressedPayload,
894
+ signal: AbortSignal.timeout(timeout)
895
+ });
896
+ if (!res.ok) {
897
+ throw new Error(`npm registry returned ${res.status}: ${res.statusText}`);
898
+ }
899
+ return res;
900
+ }, `npm audit query (${packageCount} packages)`, {
901
+ maxAttempts: NPM_AUDIT_API.MAX_RETRY_ATTEMPTS + 1,
902
+ delayMs: NPM_AUDIT_API.RETRY_DELAY_MS
903
+ });
904
+ const data = await response.json();
905
+ const parsedResponse = NpmAuditResponseSchema.parse(data);
906
+ const advisories = [];
907
+ for (const [packageName, packageAdvisories] of Object.entries(parsedResponse)) {
908
+ for (const advisory of packageAdvisories) {
909
+ advisories.push({
910
+ ...advisory,
911
+ name: advisory.name || packageName
912
+ });
913
+ }
914
+ }
915
+ logger.info(`Found ${advisories.length} advisories for ${packageCount} packages`);
916
+ return advisories;
917
+ }
918
+ async function queryInBatches(packages) {
919
+ const advisories = [];
920
+ const batchSize = NPM_AUDIT_API.MAX_PACKAGES_PER_REQUEST;
921
+ for (let i = 0;i < packages.length; i += batchSize) {
922
+ const batch = packages.slice(i, i + batchSize);
923
+ const payload = buildRequestPayload(batch);
924
+ try {
925
+ const batchAdvisories = await executeBulkQuery(payload);
926
+ advisories.push(...batchAdvisories);
927
+ } catch (error) {
928
+ logger.error(`Batch query failed for ${batch.length} packages`, {
929
+ error: error instanceof Error ? error.message : String(error),
930
+ startIndex: i
931
+ });
932
+ }
933
+ }
934
+ return advisories;
935
+ }
936
+ async function queryVulnerabilities(packages) {
937
+ if (packages.length === 0) {
938
+ return [];
939
+ }
940
+ const uniquePackages = deduplicatePackages(packages);
941
+ logger.info(`Scanning ${uniquePackages.length} unique packages (${packages.length} total)`);
942
+ const requestPayload = buildRequestPayload(uniquePackages);
943
+ if (uniquePackages.length > NPM_AUDIT_API.MAX_PACKAGES_PER_REQUEST) {
944
+ return await queryInBatches(uniquePackages);
945
+ }
946
+ return await executeBulkQuery(requestPayload);
947
+ }
948
+ return {
949
+ queryVulnerabilities
950
+ };
951
+ }
952
+
953
+ // ../source-npm/src/severity.ts
954
+ function mapSeverityToLevel2(severity) {
955
+ if (isFatalSeverity2(severity)) {
956
+ logger.debug(`Advisory marked fatal due to npm severity: ${severity}`);
957
+ return "fatal";
958
+ }
959
+ logger.debug(`Advisory marked as warning (severity: ${severity})`);
960
+ return "warn";
961
+ }
962
+ function isFatalSeverity2(severity) {
963
+ return SECURITY2.FATAL_SEVERITIES.includes(severity);
964
+ }
965
+
966
+ // ../source-npm/src/processor.ts
967
+ function createAdvisoryProcessor(ignoreConfig = {}) {
968
+ const compiledIgnoreConfig = compileIgnoreConfig(ignoreConfig);
969
+ let ignoredCount = 0;
970
+ function buildAliases(advisory) {
971
+ const aliases = new Set([
972
+ ...advisory.cves ?? [],
973
+ ...advisory.github_advisory_id ? [advisory.github_advisory_id.toUpperCase()] : []
974
+ ]);
975
+ const ghsaFromUrl = extractGhsaFromUrl(advisory.url);
976
+ if (ghsaFromUrl) {
977
+ aliases.add(ghsaFromUrl);
978
+ }
979
+ return Array.from(aliases);
980
+ }
981
+ function extractGhsaFromUrl(url) {
982
+ const match = url.match(/GHSA-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}/i);
983
+ return match ? match[0].toUpperCase() : null;
984
+ }
985
+ function isVersionAffected(version, vulnerableVersions) {
986
+ try {
987
+ return Bun.semver.satisfies(version, vulnerableVersions);
988
+ } catch (error) {
989
+ logger.warn(`Failed to parse version range "${vulnerableVersions}" for version ${version}`, {
990
+ error: error instanceof Error ? error.message : String(error)
991
+ });
992
+ return false;
993
+ }
994
+ }
995
+ function getAdvisoryDescription(advisory) {
996
+ if (advisory.overview?.trim()) {
997
+ const overview = advisory.overview.trim();
998
+ if (overview.length <= SECURITY2.MAX_DESCRIPTION_LENGTH) {
999
+ return overview;
1000
+ }
1001
+ const firstSentence = overview.match(/^[^.!?]*[.!?]/)?.[0];
1002
+ if (firstSentence && firstSentence.length <= SECURITY2.MAX_DESCRIPTION_LENGTH) {
1003
+ return firstSentence;
1004
+ }
1005
+ return `${overview.substring(0, SECURITY2.MAX_DESCRIPTION_LENGTH - 3)}...`;
1006
+ }
1007
+ if (advisory.recommendation?.trim()) {
1008
+ const recommendation = advisory.recommendation.trim();
1009
+ if (recommendation.length <= SECURITY2.MAX_DESCRIPTION_LENGTH) {
1010
+ return recommendation;
1011
+ }
1012
+ return `${recommendation.substring(0, SECURITY2.MAX_DESCRIPTION_LENGTH - 3)}...`;
1013
+ }
1014
+ return null;
1015
+ }
1016
+ function createBunAdvisory(advisory, pkg, aliases) {
1017
+ const level = mapSeverityToLevel2(advisory.severity);
1018
+ const description = getAdvisoryDescription(advisory);
1019
+ const message = advisory.title || `Security advisory ${advisory.id}`;
1020
+ return {
1021
+ id: String(advisory.id),
1022
+ message,
1023
+ level,
1024
+ package: pkg.name,
1025
+ url: advisory.url,
1026
+ description,
1027
+ aliases
1028
+ };
1029
+ }
1030
+ function processAdvisory(advisory, packages, processedPairs, compiledConfig) {
1031
+ const bunAdvisories = [];
1032
+ const advisoryPackageName = advisory.name || advisory.module_name;
1033
+ if (!advisoryPackageName) {
1034
+ logger.debug(`Advisory ${advisory.id} has no package name`);
1035
+ return bunAdvisories;
1036
+ }
1037
+ const aliases = buildAliases(advisory);
1038
+ for (const pkg of packages) {
1039
+ if (pkg.name !== advisoryPackageName) {
1040
+ continue;
1041
+ }
1042
+ const pairKey = `${advisory.id}:${pkg.name}@${pkg.version}`;
1043
+ if (processedPairs.has(pairKey)) {
1044
+ continue;
1045
+ }
1046
+ if (isVersionAffected(pkg.version, advisory.vulnerable_versions)) {
1047
+ const ignoreResult = shouldIgnoreVulnerability(String(advisory.id), aliases, pkg.name, compiledConfig);
1048
+ if (ignoreResult.ignored) {
1049
+ logger.debug(`Ignoring ${advisory.id} for ${pkg.name}: ${ignoreResult.reason}`);
1050
+ ignoredCount++;
1051
+ processedPairs.add(pairKey);
1052
+ continue;
1053
+ }
1054
+ const bunAdvisory = createBunAdvisory(advisory, pkg, aliases);
1055
+ bunAdvisories.push(bunAdvisory);
1056
+ processedPairs.add(pairKey);
1057
+ logger.debug(`Created advisory for ${pkg.name}@${pkg.version}`, {
1058
+ advisory: advisory.id,
1059
+ level: bunAdvisory.level
1060
+ });
1061
+ }
1062
+ }
1063
+ return bunAdvisories;
1064
+ }
1065
+ function processAdvisories(advisories, packages) {
1066
+ if (advisories.length === 0 || packages.length === 0) {
1067
+ return [];
1068
+ }
1069
+ logger.info(`Processing ${advisories.length} advisories against ${packages.length} packages`);
1070
+ ignoredCount = 0;
1071
+ const bunAdvisories = [];
1072
+ const processedPairs = new Set;
1073
+ for (const advisory of advisories) {
1074
+ const matched = processAdvisory(advisory, packages, processedPairs, compiledIgnoreConfig);
1075
+ bunAdvisories.push(...matched);
1076
+ }
1077
+ if (ignoredCount > 0) {
1078
+ logger.info(`Ignored ${ignoredCount} advisories based on configuration`);
1079
+ }
1080
+ logger.info(`Generated ${bunAdvisories.length} security advisories`);
1081
+ return bunAdvisories;
1082
+ }
1083
+ return {
1084
+ processAdvisories
1085
+ };
1086
+ }
1087
+
1088
+ // ../source-npm/src/index.ts
1089
+ function createNpmSource(ignoreConfig = {}) {
1090
+ const client = createNpmAuditClient();
1091
+ const processor = createAdvisoryProcessor(ignoreConfig);
1092
+ return {
1093
+ name: "npm",
1094
+ async scan(packages) {
1095
+ if (packages.length === 0)
1096
+ return [];
1097
+ logger.info(`[npm] Starting scan for ${packages.length} packages`);
1098
+ const advisories = await client.queryVulnerabilities(packages);
1099
+ const bunAdvisories = processor.processAdvisories(advisories, packages);
1100
+ logger.info(`[npm] Scan complete: ${bunAdvisories.length} advisories found`);
1101
+ return bunAdvisories;
1102
+ }
1103
+ };
1104
+ }
1105
+
1106
+ // src/sources/factory.ts
1107
+ function createSources(type, ignoreConfig) {
1108
+ switch (type) {
1109
+ case "osv":
1110
+ return [createOSVSource(ignoreConfig)];
1111
+ case "npm":
1112
+ return [createNpmSource(ignoreConfig)];
1113
+ case "both":
1114
+ return [createOSVSource(ignoreConfig), createNpmSource(ignoreConfig)];
1115
+ default:
1116
+ throw new Error(`Unknown source type: ${type}`);
1117
+ }
1118
+ }
1119
+
1120
+ // src/sources/multi.ts
1121
+ function createMultiSourceScanner(sources) {
1122
+ if (sources.length === 0) {
1123
+ throw new Error("MultiSourceScanner requires at least one source");
1124
+ }
1125
+ function isHigherSeverity(a, b) {
1126
+ const priority = { fatal: 2, warn: 1 };
1127
+ return (priority[a] ?? 0) > (priority[b] ?? 0);
1128
+ }
1129
+ function deduplicateAdvisories(advisories) {
1130
+ const map = new Map;
1131
+ for (const advisory of advisories) {
1132
+ const currentIds = new Set([advisory.id, ...advisory.aliases ?? []]);
1133
+ const packageKey = advisory.package;
1134
+ let existingKey = null;
1135
+ for (const id of currentIds) {
1136
+ const key = `${packageKey}:${id}`;
1137
+ if (map.has(key)) {
1138
+ existingKey = key;
1139
+ break;
1140
+ }
1141
+ }
1142
+ if (!existingKey) {
1143
+ for (const id of currentIds) {
1144
+ map.set(`${packageKey}:${id}`, advisory);
1145
+ }
1146
+ continue;
1147
+ }
1148
+ const existing = map.get(existingKey);
1149
+ const winner = isHigherSeverity(advisory.level, existing.level) ? advisory : existing;
1150
+ const existingIds = new Set([existing.id, ...existing.aliases ?? []]);
1151
+ const mergedIds = new Set([...currentIds, ...existingIds]);
1152
+ for (const id of mergedIds) {
1153
+ map.set(`${packageKey}:${id}`, winner);
1154
+ }
1155
+ }
1156
+ return Array.from(new Set(map.values()));
1157
+ }
1158
+ async function scan(packages) {
1159
+ const sourceNames = sources.map((s) => s.name).join(", ");
1160
+ logger.info(`Scanning with sources: ${sourceNames}`);
1161
+ const results = await Promise.allSettled(sources.map((source) => source.scan(packages)));
1162
+ const allAdvisories = [];
1163
+ for (const [i, result] of results.entries()) {
1164
+ const source = sources[i];
1165
+ if (!source)
1166
+ continue;
1167
+ if (result.status === "fulfilled") {
1168
+ logger.info(`[${source.name}] Found ${result.value.length} advisories`);
1169
+ allAdvisories.push(...result.value);
1170
+ } else {
1171
+ logger.error(`[${source.name}] Scan failed`, {
1172
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
1173
+ });
1174
+ }
1175
+ }
1176
+ return deduplicateAdvisories(allAdvisories);
1177
+ }
1178
+ return {
1179
+ scan
1180
+ };
1181
+ }
1182
+
1183
+ // src/index.ts
1184
+ var scanner = {
1185
+ version: "1",
1186
+ async scan({ packages }) {
1187
+ try {
1188
+ logger.info(`Starting vulnerability scan for ${packages.length} packages`);
1189
+ const config = await loadConfig();
1190
+ const sources = createSources(config.source ?? "osv", config);
1191
+ const multiScanner = createMultiSourceScanner(sources);
1192
+ const advisories = await multiScanner.scan(packages);
1193
+ logger.info(`Scan completed: ${advisories.length} advisories found for ${packages.length} packages`);
1194
+ return advisories;
1195
+ } catch (error) {
1196
+ const message = error instanceof Error ? error.message : String(error);
1197
+ logger.error("Scanner encountered an unexpected error", {
1198
+ error: message
1199
+ });
1200
+ return [];
1201
+ }
1202
+ }
1203
+ };
1204
+ if (false) {}
1205
+
1206
+ // src/cli.ts
1207
+ function printUsage() {
1208
+ console.log(`
1209
+ Bun Vulnerability Scanner CLI
1210
+
1211
+ Usage:
1212
+ bun run src/cli.ts test <package@version> [package@version...]
1213
+ bun run src/cli.ts scan <package.json>
1214
+ bun run src/cli.ts --help
1215
+
1216
+ Commands:
1217
+ test Test scanning specific packages
1218
+ scan Scan packages from a package.json file
1219
+ --help Show this help message
1220
+
1221
+ Examples:
1222
+ bun run src/cli.ts test lodash@4.17.20 express@4.18.0
1223
+ bun run src/cli.ts test event-stream@3.3.6
1224
+ bun run src/cli.ts scan ./package.json
1225
+
1226
+ Environment Variables:
1227
+ BUN_SCAN_LOG_LEVEL Set logging level (debug, info, warn, error)
1228
+ BUN_SCAN_SOURCE Set vulnerability source (osv, npm, both)
1229
+ `);
1230
+ }
1231
+ function exitWithError(message, code = 1) {
1232
+ logger.error(message);
1233
+ process.exit(code);
1234
+ }
1235
+ async function testPackages(packageSpecs) {
1236
+ if (packageSpecs.length === 0) {
1237
+ exitWithError("No packages specified for testing");
1238
+ }
1239
+ const packages = [];
1240
+ for (const spec of packageSpecs) {
1241
+ const match = spec.match(/^(.+)@(.+)$/);
1242
+ if (!match) {
1243
+ exitWithError(`Invalid package specification: ${spec}. Use format: package@version`);
1244
+ }
1245
+ const [, name, version] = match;
1246
+ if (!name || !version) {
1247
+ exitWithError(`Invalid package specification: ${spec}`);
1248
+ }
1249
+ packages.push({
1250
+ name,
1251
+ version,
1252
+ requestedRange: version,
1253
+ tarball: `https://registry.npmjs.org/${name}/-/${name}-${version}.tgz`
1254
+ });
1255
+ }
1256
+ logger.info(`Testing ${packages.length} packages:`);
1257
+ for (const pkg of packages) {
1258
+ logger.info(` - ${pkg.name}@${pkg.version}`);
1259
+ }
1260
+ const startTime = Date.now();
1261
+ const advisories = await scanner.scan({ packages });
1262
+ const duration = Date.now() - startTime;
1263
+ console.log(`
1264
+ \uD83D\uDCCA Scan Results (completed in ${duration}ms):`);
1265
+ console.log(`Packages scanned: ${packages.length}`);
1266
+ console.log(`Advisories found: ${advisories.length}
1267
+ `);
1268
+ if (advisories.length === 0) {
1269
+ console.log("\u2705 No security advisories found - all packages appear safe!");
1270
+ } else {
1271
+ console.log("\uD83D\uDEA8 Security advisories:");
1272
+ for (const advisory of advisories) {
1273
+ const levelIcon = advisory.level === "fatal" ? "\uD83D\uDD34" : "\u26A0\uFE0F";
1274
+ const levelText = advisory.level.toUpperCase();
1275
+ console.log(`
1276
+ ${levelIcon} ${levelText}: ${advisory.package}`);
1277
+ if (advisory.description) {
1278
+ console.log(` Description: ${advisory.description}`);
1279
+ }
1280
+ if (advisory.url) {
1281
+ console.log(` URL: ${advisory.url}`);
1282
+ }
1283
+ }
1284
+ }
1285
+ }
1286
+ async function scanPackageJson(packageJsonPath) {
1287
+ try {
1288
+ const file = Bun.file(packageJsonPath);
1289
+ if (!await file.exists()) {
1290
+ exitWithError(`Package.json file not found: ${packageJsonPath}`);
1291
+ }
1292
+ const packageJson = await file.json();
1293
+ const dependencies = {
1294
+ ...packageJson.dependencies,
1295
+ ...packageJson.devDependencies,
1296
+ ...packageJson.peerDependencies
1297
+ };
1298
+ const packages = [];
1299
+ for (const [name, versionRange] of Object.entries(dependencies)) {
1300
+ let version = versionRange;
1301
+ version = version.replace(/^[~^]/, "");
1302
+ packages.push({
1303
+ name,
1304
+ version,
1305
+ requestedRange: versionRange,
1306
+ tarball: `https://registry.npmjs.org/${name}/-/${name}-${version}.tgz`
1307
+ });
1308
+ }
1309
+ logger.info(`Found ${packages.length} dependencies in ${packageJsonPath}`);
1310
+ await testPackages(packages.map((p) => `${p.name}@${p.version}`));
1311
+ } catch (error) {
1312
+ exitWithError(`Failed to read package.json: ${error instanceof Error ? error.message : String(error)}`);
1313
+ }
1314
+ }
1315
+ async function runCli() {
1316
+ const args = process.argv.slice(2);
1317
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
1318
+ printUsage();
1319
+ return;
1320
+ }
1321
+ const command = args[0];
1322
+ const commandArgs = args.slice(1);
1323
+ switch (command) {
1324
+ case "test": {
1325
+ await testPackages(commandArgs);
1326
+ break;
1327
+ }
1328
+ case "scan": {
1329
+ if (commandArgs.length === 0) {
1330
+ exitWithError("No package.json file specified");
1331
+ }
1332
+ const file = commandArgs[0];
1333
+ if (!file) {
1334
+ exitWithError("Package.json file path required");
1335
+ }
1336
+ await scanPackageJson(file);
1337
+ break;
1338
+ }
1339
+ default: {
1340
+ exitWithError(`Unknown command: ${command}`);
1341
+ }
1342
+ }
1343
+ }
1344
+ if (import.meta.main) {
1345
+ await runCli();
1346
+ }
1347
+ export {
1348
+ runCli
1349
+ };