bun-scan 1.1.0 → 1.1.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # Bun-Scan
2
+
3
+ A security scanner for [Bun](https://bun.sh/) that checks packages for known vulnerabilities during installation.
4
+
5
+ ## Features
6
+
7
+ - **Real-time Scanning**: Checks packages against configured sources (OSV, npm, or both) during installation
8
+ - **Whitelists**: Specific warnings can be ignored
9
+ - **Fail-safe**: Can configure non-critical advisories to not prevent installations
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ # Install as a dev dependency
15
+ bun add -d bun-scan
16
+ ```
17
+
18
+ Add to your `bunfig.toml`:
19
+
20
+ ```toml
21
+ [install.security]
22
+ scanner = "bun-scan"
23
+ ```
24
+
25
+ Select your source from `npm`, `osv` (default), or run checks against `both` by setting up your config in `.bun-scan.config.json`
26
+
27
+ Note to set the schema version in the URL to the correct version:
28
+
29
+ ```json
30
+ {
31
+ "$schema": "https://raw.githubusercontent.com/rawtoast/bun-scan/v1.1.0/schema/bun-scan.schema.json",
32
+ "source": "npm"
33
+ }
34
+ ```
35
+
36
+ ### Ignoring Vulnerabilities
37
+
38
+ A package may have a vulnerability, but your project is not affected. In this scenario, you would
39
+ not want installations to be prevented. To work around this, the vulnerability can be flagged as ignored in your `.bun-scan.config.json`
40
+
41
+ ```json
42
+ {
43
+ "$schema": "https://raw.githubusercontent.com/rawtoast/bun-scan/v1.1.0/schema/bun-scan.schema.json",
44
+ "source": "npm",
45
+ "packages": {
46
+ "hono": {
47
+ "vulnerabilities": ["CVE-2026-22818"],
48
+ "reason": "Project does not use JWT from hono, verify again in June",
49
+ "until": "2026-06-01"
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ Note that `bunReportWarnings` can be set `false` to print warning-level advisories without triggering Bun's install prompt:
56
+
57
+ ```json
58
+ {
59
+ "bunReportWarnings": false
60
+ }
61
+ ```
62
+
63
+ ### Advisory Levels
64
+
65
+ #### Fatal (Installation Blocked)
66
+
67
+ - **CVSS Score**: ≥ 7.0 (High/Critical)
68
+ - **Database Severity**: CRITICAL or HIGH
69
+ - **Action**: Installation is immediately blocked
70
+
71
+ #### Warning (User Prompted)
72
+
73
+ - **CVSS Score**: < 7.0 (Medium/Low)
74
+ - **Database Severity**: MEDIUM, LOW, or unspecified
75
+ - **Action**: User is prompted to continue or cancel
76
+
77
+ ## License
78
+
79
+ MIT License - see the [LICENSE](LICENSE) file for details.
80
+
81
+ ## Acknowledgments
82
+
83
+ - **maloma7**: For the original implementation of the Bun OSV Scanner
84
+
85
+ ## Related Projects
86
+
87
+ - [Bun Security Scanner API](https://bun.com/docs/install/security-scanner-api)
88
+ - [OSV.dev](https://osv.dev/)
89
+ - [GitHub advisories](https://github.com/advisories)
90
+ - [Bun OSV Scanner](https://github.com/bun-security-scanner/osv)
91
+ - [Bun NPM Scanner](https://github.com/bun-security-scanner/npm)
package/dist/cli.js CHANGED
@@ -29,7 +29,8 @@ var ENV = {
29
29
  LOG_LEVEL: "BUN_SCAN_LOG_LEVEL",
30
30
  API_BASE_URL: "OSV_API_BASE_URL",
31
31
  TIMEOUT_MS: "OSV_TIMEOUT_MS",
32
- DISABLE_BATCH: "OSV_DISABLE_BATCH"
32
+ DISABLE_BATCH: "OSV_DISABLE_BATCH",
33
+ FAIL_ON_SCANNER_ERROR: "BUN_SCAN_FAIL_ON_SCANNER_ERROR"
33
34
  };
34
35
  // ../core/src/logger.ts
35
36
  var LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
@@ -118,6 +119,7 @@ import { z } from "zod";
118
119
  var CONFIG_DEFAULTS = {
119
120
  logLevel: "info",
120
121
  bunReportWarnings: true,
122
+ failOnScannerError: false,
121
123
  osv: {
122
124
  apiBaseUrl: OSV_API.BASE_URL,
123
125
  timeoutMs: OSV_API.TIMEOUT_MS,
@@ -152,6 +154,7 @@ var ConfigSchema = z.object({
152
154
  packages: z.record(z.string(), IgnorePackageRuleSchema).optional(),
153
155
  logLevel: z.enum(["debug", "info", "warn", "error"]).optional(),
154
156
  bunReportWarnings: z.boolean().optional(),
157
+ failOnScannerError: z.boolean().optional(),
155
158
  osv: OsvConfigSchema.optional(),
156
159
  npm: NpmConfigSchema.optional()
157
160
  });
@@ -198,6 +201,7 @@ function parseEnvLogLevel(envVar) {
198
201
  function buildEnvConfig() {
199
202
  return {
200
203
  logLevel: parseEnvLogLevel(ENV.LOG_LEVEL),
204
+ failOnScannerError: parseEnvBoolean(ENV.FAIL_ON_SCANNER_ERROR),
201
205
  osv: {
202
206
  apiBaseUrl: Bun.env[ENV.API_BASE_URL] || undefined,
203
207
  timeoutMs: parseEnvNumber(ENV.TIMEOUT_MS),
@@ -215,11 +219,14 @@ function mergeConfig(fileConfig) {
215
219
  source: DEFAULT_SOURCE,
216
220
  logLevel: CONFIG_DEFAULTS.logLevel,
217
221
  bunReportWarnings: CONFIG_DEFAULTS.bunReportWarnings,
222
+ failOnScannerError: CONFIG_DEFAULTS.failOnScannerError,
218
223
  osv: { ...CONFIG_DEFAULTS.osv },
219
224
  npm: { ...CONFIG_DEFAULTS.npm }
220
225
  };
221
226
  if (envConfig.logLevel !== undefined)
222
227
  merged.logLevel = envConfig.logLevel;
228
+ if (envConfig.failOnScannerError !== undefined)
229
+ merged.failOnScannerError = envConfig.failOnScannerError;
223
230
  if (envConfig.osv?.apiBaseUrl !== undefined)
224
231
  merged.osv.apiBaseUrl = envConfig.osv.apiBaseUrl;
225
232
  if (envConfig.osv?.timeoutMs !== undefined)
@@ -241,6 +248,8 @@ function mergeConfig(fileConfig) {
241
248
  merged.logLevel = fileConfig.logLevel;
242
249
  if (fileConfig.bunReportWarnings !== undefined)
243
250
  merged.bunReportWarnings = fileConfig.bunReportWarnings;
251
+ if (fileConfig.failOnScannerError !== undefined)
252
+ merged.failOnScannerError = fileConfig.failOnScannerError;
244
253
  if (fileConfig.osv?.apiBaseUrl !== undefined)
245
254
  merged.osv.apiBaseUrl = fileConfig.osv.apiBaseUrl;
246
255
  if (fileConfig.osv?.timeoutMs !== undefined)
@@ -252,18 +261,22 @@ function mergeConfig(fileConfig) {
252
261
  if (fileConfig.npm?.timeoutMs !== undefined)
253
262
  merged.npm.timeoutMs = fileConfig.npm.timeoutMs;
254
263
  }
264
+ if (envConfig.failOnScannerError !== undefined) {
265
+ merged.failOnScannerError = envConfig.failOnScannerError;
266
+ }
255
267
  return merged;
256
268
  }
257
269
  async function loadConfig() {
270
+ const strictBootstrap = parseEnvBoolean(ENV.FAIL_ON_SCANNER_ERROR) === true;
258
271
  for (const filename of CONFIG_FILES) {
259
- const config = await tryLoadConfigFile(filename);
272
+ const config = await tryLoadConfigFile(filename, { fatalOnError: strictBootstrap });
260
273
  if (config) {
261
274
  return mergeConfig(config);
262
275
  }
263
276
  }
264
277
  return mergeConfig(null);
265
278
  }
266
- async function tryLoadConfigFile(filename) {
279
+ async function tryLoadConfigFile(filename, options) {
267
280
  try {
268
281
  const file = Bun.file(filename);
269
282
  const exists = await file.exists();
@@ -276,6 +289,18 @@ async function tryLoadConfigFile(filename) {
276
289
  logConfigStats(parsed);
277
290
  return parsed;
278
291
  } catch (error) {
292
+ const isENOENT = error instanceof Error && ("code" in error ? error.code === "ENOENT" : error.message.includes("No such file"));
293
+ if (options?.fatalOnError) {
294
+ if (isENOENT) {
295
+ return null;
296
+ }
297
+ logger.error(`Failed to read config file ${filename}`, {
298
+ error: error instanceof Error ? error.message : String(error),
299
+ stack: error instanceof Error ? error.stack : undefined
300
+ });
301
+ const message = error instanceof Error ? error.message : String(error);
302
+ throw new Error(`bun-scan: failed to load config file ${filename}: ${message}. ` + `BUN_SCAN_FAIL_ON_SCANNER_ERROR=true makes config errors fatal.`);
303
+ }
279
304
  if (error instanceof z.ZodError) {
280
305
  logger.warn(`Invalid config in ${filename}`, {
281
306
  errors: error.issues.map((e) => `${e.path.join(".")}: ${e.message}`)
@@ -284,6 +309,13 @@ async function tryLoadConfigFile(filename) {
284
309
  logger.warn(`Failed to parse ${filename} as JSON`, {
285
310
  error: error.message
286
311
  });
312
+ } else if (isENOENT) {
313
+ logger.debug(`Config file ${filename} not found (race condition handled)`);
314
+ } else {
315
+ logger.warn(`Failed to read config file ${filename}`, {
316
+ error: error instanceof Error ? error.message : String(error),
317
+ stack: error instanceof Error ? error.stack : undefined
318
+ });
287
319
  }
288
320
  return null;
289
321
  }
@@ -408,6 +440,7 @@ function createOSVClient(options = {}) {
408
440
  const baseUrl = osvConfig.apiBaseUrl ?? OSV_API.BASE_URL;
409
441
  const timeout = osvConfig.timeoutMs ?? OSV_API.TIMEOUT_MS;
410
442
  const useBatch = !(osvConfig.disableBatch ?? false);
443
+ const failOnError = options.failOnScannerError ?? false;
411
444
  function deduplicatePackages(packages) {
412
445
  const packageMap = new Map;
413
446
  for (const pkg of packages) {
@@ -466,8 +499,15 @@ function createOSVClient(options = {}) {
466
499
  return OSVVulnerabilitySchema.parse(data);
467
500
  }, `Get vulnerability ${id}`);
468
501
  } catch (error) {
502
+ const message = error instanceof Error ? error.message : String(error);
503
+ if (failOnError) {
504
+ logger.error(`Failed to fetch vulnerability ${id} (strict mode)`, {
505
+ error: message
506
+ });
507
+ throw error;
508
+ }
469
509
  logger.warn(`Failed to fetch vulnerability ${id}`, {
470
- error: error instanceof Error ? error.message : String(error)
510
+ error: message
471
511
  });
472
512
  return null;
473
513
  }
@@ -482,6 +522,18 @@ function createOSVClient(options = {}) {
482
522
  for (let i = 0;i < uniqueIds.length; i += chunkSize) {
483
523
  const chunk = uniqueIds.slice(i, i + chunkSize);
484
524
  const chunkResults = await Promise.allSettled(chunk.map((id) => fetchSingleVulnerability(id)));
525
+ if (failOnError) {
526
+ const rejections = chunkResults.filter((r) => r.status === "rejected");
527
+ if (rejections.length > 0) {
528
+ const firstError = rejections[0].reason;
529
+ const message = firstError instanceof Error ? firstError.message : String(firstError);
530
+ logger.error(`Failed to fetch vulnerability details (strict mode)`, {
531
+ error: message,
532
+ failedCount: rejections.length
533
+ });
534
+ throw firstError;
535
+ }
536
+ }
485
537
  for (const result of chunkResults) {
486
538
  if (result.status === "fulfilled" && result.value) {
487
539
  vulnerabilities.push(result.value);
@@ -499,8 +551,16 @@ function createOSVClient(options = {}) {
499
551
  const batchIds = await executeBatchQuery(batchQueries);
500
552
  vulnerabilityIds.push(...batchIds);
501
553
  } catch (error) {
554
+ const message = error instanceof Error ? error.message : String(error);
555
+ if (failOnError) {
556
+ logger.error(`Batch query failed for ${batchQueries.length} packages (strict mode)`, {
557
+ error: message,
558
+ startIndex: i
559
+ });
560
+ throw error;
561
+ }
502
562
  logger.error(`Batch query failed for ${batchQueries.length} packages`, {
503
- error: error instanceof Error ? error.message : String(error),
563
+ error: message,
504
564
  startIndex: i
505
565
  });
506
566
  }
@@ -538,8 +598,15 @@ function createOSVClient(options = {}) {
538
598
  break;
539
599
  }
540
600
  } catch (error) {
601
+ const message = error instanceof Error ? error.message : String(error);
602
+ if (failOnError) {
603
+ logger.error(`Query failed for ${query.package?.name || "unknown"}@${query.version || "unknown"} (strict mode)`, {
604
+ error: message
605
+ });
606
+ throw error;
607
+ }
541
608
  logger.warn(`Query failed for ${query.package?.name || "unknown"}@${query.version || "unknown"}`, {
542
- error: error instanceof Error ? error.message : String(error)
609
+ error: message
543
610
  });
544
611
  break;
545
612
  }
@@ -548,6 +615,18 @@ function createOSVClient(options = {}) {
548
615
  }
549
616
  async function queryIndividually(queries) {
550
617
  const responses = await Promise.allSettled(queries.map((query) => querySinglePackage(query)));
618
+ if (failOnError) {
619
+ const rejections = responses.filter((r) => r.status === "rejected");
620
+ if (rejections.length > 0) {
621
+ const firstError = rejections[0].reason;
622
+ const message = firstError instanceof Error ? firstError.message : String(firstError);
623
+ logger.error(`Individual query failed (strict mode)`, {
624
+ error: message,
625
+ failedCount: rejections.length
626
+ });
627
+ throw firstError;
628
+ }
629
+ }
551
630
  const vulnerabilities = [];
552
631
  let successCount = 0;
553
632
  for (const response of responses) {
@@ -846,6 +925,8 @@ function createVulnerabilityProcessor(ignoreConfig = {}) {
846
925
  function isNewOptionsFormat(options) {
847
926
  if ("osv" in options)
848
927
  return true;
928
+ if ("failOnScannerError" in options)
929
+ return true;
849
930
  if ("ignore" in options && options.ignore && typeof options.ignore === "object" && !Array.isArray(options.ignore)) {
850
931
  return true;
851
932
  }
@@ -854,14 +935,32 @@ function isNewOptionsFormat(options) {
854
935
  function createOSVSource(options = {}) {
855
936
  let ignoreConfig;
856
937
  let osvConfig;
938
+ let failOnScannerError;
857
939
  if (isNewOptionsFormat(options)) {
858
- ignoreConfig = options.ignore ?? {};
940
+ const ignoreFromOptions = options.ignore;
941
+ const packagesFromOptions = options.packages;
942
+ if (ignoreFromOptions !== undefined) {
943
+ if (Array.isArray(ignoreFromOptions)) {
944
+ ignoreConfig = { ignore: ignoreFromOptions, packages: packagesFromOptions };
945
+ } else {
946
+ ignoreConfig = {
947
+ ignore: ignoreFromOptions.ignore,
948
+ packages: ignoreFromOptions.packages ?? packagesFromOptions
949
+ };
950
+ }
951
+ } else if (packagesFromOptions !== undefined) {
952
+ ignoreConfig = { ignore: undefined, packages: packagesFromOptions };
953
+ } else {
954
+ ignoreConfig = {};
955
+ }
859
956
  osvConfig = options.osv;
957
+ failOnScannerError = options.failOnScannerError;
860
958
  } else {
861
959
  ignoreConfig = options;
862
960
  osvConfig = undefined;
961
+ failOnScannerError = undefined;
863
962
  }
864
- const client = createOSVClient({ osv: osvConfig });
963
+ const client = createOSVClient({ osv: osvConfig, failOnScannerError });
865
964
  const processor = createVulnerabilityProcessor(ignoreConfig);
866
965
  return {
867
966
  name: "osv",
@@ -934,6 +1033,7 @@ function createNpmAuditClient(options = {}) {
934
1033
  const npmConfig = options.npm ?? CONFIG_DEFAULTS.npm;
935
1034
  const registryUrl = npmConfig.registryUrl ?? NPM_AUDIT_API.REGISTRY_URL;
936
1035
  const timeout = npmConfig.timeoutMs ?? NPM_AUDIT_API.TIMEOUT_MS;
1036
+ const failOnError = options.failOnScannerError ?? false;
937
1037
  function deduplicatePackages(packages) {
938
1038
  const packageMap = new Map;
939
1039
  for (const pkg of packages) {
@@ -1012,8 +1112,16 @@ function createNpmAuditClient(options = {}) {
1012
1112
  const batchAdvisories = await executeBulkQuery(payload);
1013
1113
  advisories.push(...batchAdvisories);
1014
1114
  } catch (error) {
1115
+ const message = error instanceof Error ? error.message : String(error);
1116
+ if (failOnError) {
1117
+ logger.error(`Batch query failed for ${batch.length} packages (strict mode)`, {
1118
+ error: message,
1119
+ startIndex: i
1120
+ });
1121
+ throw error;
1122
+ }
1015
1123
  logger.error(`Batch query failed for ${batch.length} packages`, {
1016
- error: error instanceof Error ? error.message : String(error),
1124
+ error: message,
1017
1125
  startIndex: i
1018
1126
  });
1019
1127
  }
@@ -1030,7 +1138,23 @@ function createNpmAuditClient(options = {}) {
1030
1138
  if (uniquePackages.length > NPM_AUDIT_API.MAX_PACKAGES_PER_REQUEST) {
1031
1139
  return await queryInBatches(uniquePackages);
1032
1140
  }
1033
- return await executeBulkQuery(requestPayload);
1141
+ try {
1142
+ return await executeBulkQuery(requestPayload);
1143
+ } catch (error) {
1144
+ const message = error instanceof Error ? error.message : String(error);
1145
+ if (failOnError) {
1146
+ logger.error(`Bulk query failed (strict mode)`, {
1147
+ error: message,
1148
+ packageCount: uniquePackages.length
1149
+ });
1150
+ throw error;
1151
+ }
1152
+ logger.error(`Bulk query failed`, {
1153
+ error: message,
1154
+ packageCount: uniquePackages.length
1155
+ });
1156
+ return [];
1157
+ }
1034
1158
  }
1035
1159
  return {
1036
1160
  queryVulnerabilities
@@ -1176,6 +1300,8 @@ function createAdvisoryProcessor(ignoreConfig = {}) {
1176
1300
  function isNewOptionsFormat2(options) {
1177
1301
  if ("npm" in options)
1178
1302
  return true;
1303
+ if ("failOnScannerError" in options)
1304
+ return true;
1179
1305
  if ("ignore" in options && options.ignore && typeof options.ignore === "object" && !Array.isArray(options.ignore)) {
1180
1306
  return true;
1181
1307
  }
@@ -1184,14 +1310,32 @@ function isNewOptionsFormat2(options) {
1184
1310
  function createNpmSource(options = {}) {
1185
1311
  let ignoreConfig;
1186
1312
  let npmConfig;
1313
+ let failOnScannerError;
1187
1314
  if (isNewOptionsFormat2(options)) {
1188
- ignoreConfig = options.ignore ?? {};
1315
+ const ignoreFromOptions = options.ignore;
1316
+ const packagesFromOptions = options.packages;
1317
+ if (ignoreFromOptions !== undefined) {
1318
+ if (Array.isArray(ignoreFromOptions)) {
1319
+ ignoreConfig = { ignore: ignoreFromOptions, packages: packagesFromOptions };
1320
+ } else {
1321
+ ignoreConfig = {
1322
+ ignore: ignoreFromOptions.ignore,
1323
+ packages: ignoreFromOptions.packages ?? packagesFromOptions
1324
+ };
1325
+ }
1326
+ } else if (packagesFromOptions !== undefined) {
1327
+ ignoreConfig = { ignore: undefined, packages: packagesFromOptions };
1328
+ } else {
1329
+ ignoreConfig = {};
1330
+ }
1189
1331
  npmConfig = options.npm;
1332
+ failOnScannerError = options.failOnScannerError;
1190
1333
  } else {
1191
1334
  ignoreConfig = options;
1192
1335
  npmConfig = undefined;
1336
+ failOnScannerError = undefined;
1193
1337
  }
1194
- const client = createNpmAuditClient({ npm: npmConfig });
1338
+ const client = createNpmAuditClient({ npm: npmConfig, failOnScannerError });
1195
1339
  const processor = createAdvisoryProcessor(ignoreConfig);
1196
1340
  return {
1197
1341
  name: "npm",
@@ -1211,17 +1355,17 @@ function createNpmSource(options = {}) {
1211
1355
  function extractIgnoreConfig(config) {
1212
1356
  return { ignore: config.ignore, packages: config.packages };
1213
1357
  }
1214
- function createSources(type, config) {
1358
+ function createSources(type, config, failOnScannerError) {
1215
1359
  const ignoreConfig = extractIgnoreConfig(config);
1216
1360
  switch (type) {
1217
1361
  case "osv":
1218
- return [createOSVSource({ ignore: ignoreConfig, osv: config.osv })];
1362
+ return [createOSVSource({ ignore: ignoreConfig, osv: config.osv, failOnScannerError })];
1219
1363
  case "npm":
1220
- return [createNpmSource({ ignore: ignoreConfig, npm: config.npm })];
1364
+ return [createNpmSource({ ignore: ignoreConfig, npm: config.npm, failOnScannerError })];
1221
1365
  case "both":
1222
1366
  return [
1223
- createOSVSource({ ignore: ignoreConfig, osv: config.osv }),
1224
- createNpmSource({ ignore: ignoreConfig, npm: config.npm })
1367
+ createOSVSource({ ignore: ignoreConfig, osv: config.osv, failOnScannerError }),
1368
+ createNpmSource({ ignore: ignoreConfig, npm: config.npm, failOnScannerError })
1225
1369
  ];
1226
1370
  default:
1227
1371
  throw new Error(`Unknown source type: ${type}`);
@@ -1229,10 +1373,11 @@ function createSources(type, config) {
1229
1373
  }
1230
1374
 
1231
1375
  // src/sources/multi.ts
1232
- function createMultiSourceScanner(sources) {
1376
+ function createMultiSourceScanner(sources, options) {
1233
1377
  if (sources.length === 0) {
1234
1378
  throw new Error("MultiSourceScanner requires at least one source");
1235
1379
  }
1380
+ const failOnError = options?.failOnScannerError ?? false;
1236
1381
  function isHigherSeverity(a, b) {
1237
1382
  const priority = { fatal: 2, warn: 1 };
1238
1383
  return (priority[a] ?? 0) > (priority[b] ?? 0);
@@ -1269,8 +1414,9 @@ function createMultiSourceScanner(sources) {
1269
1414
  async function scan(packages) {
1270
1415
  const sourceNames = sources.map((s) => s.name).join(", ");
1271
1416
  logger.info(`Scanning with sources: ${sourceNames}`);
1272
- const results = await Promise.allSettled(sources.map((source) => source.scan(packages)));
1417
+ const results = await Promise.allSettled(sources.map((source) => Promise.resolve().then(() => source.scan(packages))));
1273
1418
  const allAdvisories = [];
1419
+ const failures = [];
1274
1420
  for (const [i, result] of results.entries()) {
1275
1421
  const source = sources[i];
1276
1422
  if (!source)
@@ -1279,11 +1425,15 @@ function createMultiSourceScanner(sources) {
1279
1425
  logger.debug(`[${source.name}] Found ${result.value.length} advisories`);
1280
1426
  allAdvisories.push(...result.value);
1281
1427
  } else {
1282
- logger.error(`[${source.name}] Scan failed`, {
1283
- error: result.reason instanceof Error ? result.reason.message : String(result.reason)
1284
- });
1428
+ const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
1429
+ logger.error(`[${source.name}] Scan failed`, { error: errorMessage });
1430
+ failures.push({ name: source.name, error: errorMessage });
1285
1431
  }
1286
1432
  }
1433
+ if (failOnError && failures.length > 0) {
1434
+ const details = failures.map((f) => `${f.name}: ${f.error}`).join("; ");
1435
+ throw new Error(`bun-scan: scan failed for ${failures.length === 1 ? "source" : "sources"} ` + `${failures.map((f) => `"${f.name}"`).join(", ")}. ` + `Details: ${details}. ` + `failOnScannerError=true requires all configured sources to succeed.`);
1436
+ }
1287
1437
  return deduplicateAdvisories(allAdvisories);
1288
1438
  }
1289
1439
  return {
@@ -1295,12 +1445,16 @@ function createMultiSourceScanner(sources) {
1295
1445
  var scanner = {
1296
1446
  version: "1",
1297
1447
  async scan({ packages }) {
1448
+ let failOnScannerError = parseEnvBoolean(ENV.FAIL_ON_SCANNER_ERROR) === true;
1298
1449
  try {
1299
1450
  logger.debug(`Starting vulnerability scan for ${packages.length} packages`);
1300
1451
  const config = await loadConfig();
1301
1452
  const bunReportWarnings = config.bunReportWarnings ?? CONFIG_DEFAULTS.bunReportWarnings;
1302
- const sources = createSources(config.source ?? "osv", config);
1303
- const multiScanner = createMultiSourceScanner(sources);
1453
+ if (parseEnvBoolean(ENV.FAIL_ON_SCANNER_ERROR) === undefined) {
1454
+ failOnScannerError = config.failOnScannerError ?? failOnScannerError;
1455
+ }
1456
+ const sources = createSources(config.source ?? "osv", config, failOnScannerError);
1457
+ const multiScanner = createMultiSourceScanner(sources, { failOnScannerError });
1304
1458
  const advisories = await multiScanner.scan(packages);
1305
1459
  logger.info(`Scan completed: ${advisories.length} advisories found for ${packages.length} packages`);
1306
1460
  if (!bunReportWarnings) {
@@ -1318,6 +1472,12 @@ var scanner = {
1318
1472
  return advisories;
1319
1473
  } catch (error) {
1320
1474
  const message = error instanceof Error ? error.message : String(error);
1475
+ if (failOnScannerError) {
1476
+ logger.error("Scanner error in strict mode \u2014 failing scan", {
1477
+ error: message
1478
+ });
1479
+ throw error;
1480
+ }
1321
1481
  logger.error("Scanner encountered an unexpected error", {
1322
1482
  error: message
1323
1483
  });
package/dist/index.d.ts CHANGED
@@ -64,7 +64,7 @@ export declare const DEFAULT_RETRY_CONFIG: RetryConfig
64
64
  export declare function withRetry<T>(
65
65
  operation: () => Promise<T>,
66
66
  operationName: string,
67
- config?: Partial<RetryConfig>,
67
+ config?: RetryConfig,
68
68
  ): Promise<T>
69
69
 
70
70
  // ============================================================================
@@ -86,6 +86,12 @@ export interface Config {
86
86
  source?: SourceType
87
87
  ignore?: string[]
88
88
  packages?: Record<string, IgnorePackageRule>
89
+ logLevel?: "debug" | "info" | "warn" | "error"
90
+ bunReportWarnings?: boolean
91
+ /** Fail on scanner errors (block install). Env var overrides config file (escape hatch). */
92
+ failOnScannerError?: boolean
93
+ osv?: OsvConfig
94
+ npm?: NpmConfig
89
95
  }
90
96
 
91
97
  export interface OsvConfig {
@@ -94,9 +100,29 @@ export interface OsvConfig {
94
100
  disableBatch?: boolean
95
101
  }
96
102
 
103
+ export interface NpmConfig {
104
+ registryUrl?: string
105
+ timeoutMs?: number
106
+ }
107
+
97
108
  export interface CreateOSVSourceOptions {
98
- ignore?: IgnoreConfig
109
+ /** Legacy array form or new object form */
110
+ ignore?: IgnoreConfig | string[]
111
+ /** Legacy packages config (when ignore is not an object) */
112
+ packages?: Record<string, IgnorePackageRule>
99
113
  osv?: OsvConfig
114
+ /** When true, throw on internal errors (batch/query failures) instead of continuing with partial results */
115
+ failOnScannerError?: boolean
116
+ }
117
+
118
+ export interface CreateNpmSourceOptions {
119
+ /** Legacy array form or new object form */
120
+ ignore?: IgnoreConfig | string[]
121
+ /** Legacy packages config (when ignore is not an object) */
122
+ packages?: Record<string, IgnorePackageRule>
123
+ npm?: NpmConfig
124
+ /** When true, throw on internal errors (batch/query failures) instead of continuing with partial results */
125
+ failOnScannerError?: boolean
100
126
  }
101
127
 
102
128
  export interface CompiledPackageRule {
@@ -110,6 +136,24 @@ export interface CompiledIgnoreConfig {
110
136
  packages: Map<string, CompiledPackageRule>
111
137
  }
112
138
 
139
+ export interface ConfigDefaults {
140
+ readonly logLevel: "debug" | "info" | "warn" | "error"
141
+ readonly bunReportWarnings: boolean
142
+ readonly failOnScannerError: boolean
143
+ readonly osv: {
144
+ readonly apiBaseUrl: string
145
+ readonly timeoutMs: number
146
+ readonly disableBatch: boolean
147
+ }
148
+ readonly npm: {
149
+ readonly registryUrl: string
150
+ readonly timeoutMs: number
151
+ }
152
+ }
153
+
154
+ /** Default configuration values */
155
+ export declare const CONFIG_DEFAULTS: ConfigDefaults
156
+
113
157
  /** Zod schema for ignore config validation */
114
158
  export declare const IgnoreConfigSchema: z.ZodObject<{
115
159
  ignore: z.ZodOptional<z.ZodArray<z.ZodString>>
@@ -139,6 +183,22 @@ export declare const ConfigSchema: z.ZodObject<{
139
183
  }>
140
184
  >
141
185
  >
186
+ logLevel: z.ZodOptional<z.ZodEnum<["debug", "info", "warn", "error"]>>
187
+ bunReportWarnings: z.ZodOptional<z.ZodBoolean>
188
+ failOnScannerError: z.ZodOptional<z.ZodBoolean>
189
+ osv: z.ZodOptional<
190
+ z.ZodObject<{
191
+ apiBaseUrl: z.ZodOptional<z.ZodString>
192
+ timeoutMs: z.ZodOptional<z.ZodNumber>
193
+ disableBatch: z.ZodOptional<z.ZodBoolean>
194
+ }>
195
+ >
196
+ npm: z.ZodOptional<
197
+ z.ZodObject<{
198
+ registryUrl: z.ZodOptional<z.ZodString>
199
+ timeoutMs: z.ZodOptional<z.ZodNumber>
200
+ }>
201
+ >
142
202
  }>
143
203
 
144
204
  /** Load config from .bun-scan.json or .bun-scan.config.json */
@@ -149,11 +209,11 @@ export declare function compileIgnoreConfig(config: IgnoreConfig): CompiledIgnor
149
209
 
150
210
  /** Check if a vulnerability should be ignored */
151
211
  export declare function shouldIgnoreVulnerability(
152
- compiledConfig: CompiledIgnoreConfig,
153
212
  vulnId: string,
154
- packageName: string,
155
- aliases?: string[],
156
- ): boolean
213
+ vulnAliases: string[] | undefined,
214
+ packageName: string | undefined,
215
+ config: CompiledIgnoreConfig,
216
+ ): { ignored: boolean; reason?: string }
157
217
 
158
218
  // ============================================================================
159
219
  // Source Factories
@@ -165,15 +225,22 @@ export declare function createOSVSource(
165
225
  ): VulnerabilitySource
166
226
 
167
227
  /** Create an npm audit vulnerability source */
168
- export declare function createNpmSource(config?: IgnoreConfig): VulnerabilitySource
228
+ export declare function createNpmSource(
229
+ options?: CreateNpmSourceOptions | IgnoreConfig,
230
+ ): VulnerabilitySource
169
231
 
170
232
  /** Create a vulnerability source by type */
171
- export declare function createSource(type: SourceType, config?: IgnoreConfig): VulnerabilitySource
233
+ export declare function createSource(
234
+ type: "osv" | "npm",
235
+ config: Config,
236
+ failOnScannerError?: boolean,
237
+ ): VulnerabilitySource
172
238
 
173
239
  /** Create all sources for a given type (both = OSV + npm) */
174
240
  export declare function createSources(
175
241
  type: SourceType,
176
- config?: IgnoreConfig,
242
+ config: Config,
243
+ failOnScannerError?: boolean,
177
244
  ): VulnerabilitySource[]
178
245
 
179
246
  // ============================================================================
@@ -184,8 +251,17 @@ export interface MultiSourceScanner {
184
251
  scan(packages: Bun.Security.Package[]): Promise<Bun.Security.Advisory[]>
185
252
  }
186
253
 
254
+ /** Options for multi-source scanner */
255
+ export interface MultiSourceScannerOptions {
256
+ /** When true, throw if any configured source fails */
257
+ failOnScannerError?: boolean
258
+ }
259
+
187
260
  /** Create a scanner that queries multiple sources in parallel */
188
- export declare function createMultiSourceScanner(sources: VulnerabilitySource[]): MultiSourceScanner
261
+ export declare function createMultiSourceScanner(
262
+ sources: VulnerabilitySource[],
263
+ options?: MultiSourceScannerOptions,
264
+ ): MultiSourceScanner
189
265
 
190
266
  // ============================================================================
191
267
  // Main Scanner Export
package/dist/index.js CHANGED
@@ -1,12 +1,16 @@
1
1
  // @bun
2
2
  var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
3
7
  var __export = (target, all) => {
4
8
  for (var name in all)
5
9
  __defProp(target, name, {
6
10
  get: all[name],
7
11
  enumerable: true,
8
12
  configurable: true,
9
- set: (newValue) => all[name] = () => newValue
13
+ set: __exportSetter.bind(all, name)
10
14
  });
11
15
  };
12
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -44,7 +48,8 @@ var init_constants = __esm(() => {
44
48
  LOG_LEVEL: "BUN_SCAN_LOG_LEVEL",
45
49
  API_BASE_URL: "OSV_API_BASE_URL",
46
50
  TIMEOUT_MS: "OSV_TIMEOUT_MS",
47
- DISABLE_BATCH: "OSV_DISABLE_BATCH"
51
+ DISABLE_BATCH: "OSV_DISABLE_BATCH",
52
+ FAIL_ON_SCANNER_ERROR: "BUN_SCAN_FAIL_ON_SCANNER_ERROR"
48
53
  };
49
54
  });
50
55
 
@@ -179,6 +184,7 @@ function parseEnvLogLevel(envVar) {
179
184
  function buildEnvConfig() {
180
185
  return {
181
186
  logLevel: parseEnvLogLevel(ENV.LOG_LEVEL),
187
+ failOnScannerError: parseEnvBoolean(ENV.FAIL_ON_SCANNER_ERROR),
182
188
  osv: {
183
189
  apiBaseUrl: Bun.env[ENV.API_BASE_URL] || undefined,
184
190
  timeoutMs: parseEnvNumber(ENV.TIMEOUT_MS),
@@ -196,11 +202,14 @@ function mergeConfig(fileConfig) {
196
202
  source: DEFAULT_SOURCE,
197
203
  logLevel: CONFIG_DEFAULTS.logLevel,
198
204
  bunReportWarnings: CONFIG_DEFAULTS.bunReportWarnings,
205
+ failOnScannerError: CONFIG_DEFAULTS.failOnScannerError,
199
206
  osv: { ...CONFIG_DEFAULTS.osv },
200
207
  npm: { ...CONFIG_DEFAULTS.npm }
201
208
  };
202
209
  if (envConfig.logLevel !== undefined)
203
210
  merged.logLevel = envConfig.logLevel;
211
+ if (envConfig.failOnScannerError !== undefined)
212
+ merged.failOnScannerError = envConfig.failOnScannerError;
204
213
  if (envConfig.osv?.apiBaseUrl !== undefined)
205
214
  merged.osv.apiBaseUrl = envConfig.osv.apiBaseUrl;
206
215
  if (envConfig.osv?.timeoutMs !== undefined)
@@ -222,6 +231,8 @@ function mergeConfig(fileConfig) {
222
231
  merged.logLevel = fileConfig.logLevel;
223
232
  if (fileConfig.bunReportWarnings !== undefined)
224
233
  merged.bunReportWarnings = fileConfig.bunReportWarnings;
234
+ if (fileConfig.failOnScannerError !== undefined)
235
+ merged.failOnScannerError = fileConfig.failOnScannerError;
225
236
  if (fileConfig.osv?.apiBaseUrl !== undefined)
226
237
  merged.osv.apiBaseUrl = fileConfig.osv.apiBaseUrl;
227
238
  if (fileConfig.osv?.timeoutMs !== undefined)
@@ -233,18 +244,22 @@ function mergeConfig(fileConfig) {
233
244
  if (fileConfig.npm?.timeoutMs !== undefined)
234
245
  merged.npm.timeoutMs = fileConfig.npm.timeoutMs;
235
246
  }
247
+ if (envConfig.failOnScannerError !== undefined) {
248
+ merged.failOnScannerError = envConfig.failOnScannerError;
249
+ }
236
250
  return merged;
237
251
  }
238
252
  async function loadConfig() {
253
+ const strictBootstrap = parseEnvBoolean(ENV.FAIL_ON_SCANNER_ERROR) === true;
239
254
  for (const filename of CONFIG_FILES) {
240
- const config = await tryLoadConfigFile(filename);
255
+ const config = await tryLoadConfigFile(filename, { fatalOnError: strictBootstrap });
241
256
  if (config) {
242
257
  return mergeConfig(config);
243
258
  }
244
259
  }
245
260
  return mergeConfig(null);
246
261
  }
247
- async function tryLoadConfigFile(filename) {
262
+ async function tryLoadConfigFile(filename, options) {
248
263
  try {
249
264
  const file = Bun.file(filename);
250
265
  const exists = await file.exists();
@@ -257,6 +272,18 @@ async function tryLoadConfigFile(filename) {
257
272
  logConfigStats(parsed);
258
273
  return parsed;
259
274
  } catch (error) {
275
+ const isENOENT = error instanceof Error && ("code" in error ? error.code === "ENOENT" : error.message.includes("No such file"));
276
+ if (options?.fatalOnError) {
277
+ if (isENOENT) {
278
+ return null;
279
+ }
280
+ logger.error(`Failed to read config file ${filename}`, {
281
+ error: error instanceof Error ? error.message : String(error),
282
+ stack: error instanceof Error ? error.stack : undefined
283
+ });
284
+ const message = error instanceof Error ? error.message : String(error);
285
+ throw new Error(`bun-scan: failed to load config file ${filename}: ${message}. ` + `BUN_SCAN_FAIL_ON_SCANNER_ERROR=true makes config errors fatal.`);
286
+ }
260
287
  if (error instanceof z.ZodError) {
261
288
  logger.warn(`Invalid config in ${filename}`, {
262
289
  errors: error.issues.map((e) => `${e.path.join(".")}: ${e.message}`)
@@ -265,6 +292,13 @@ async function tryLoadConfigFile(filename) {
265
292
  logger.warn(`Failed to parse ${filename} as JSON`, {
266
293
  error: error.message
267
294
  });
295
+ } else if (isENOENT) {
296
+ logger.debug(`Config file ${filename} not found (race condition handled)`);
297
+ } else {
298
+ logger.warn(`Failed to read config file ${filename}`, {
299
+ error: error instanceof Error ? error.message : String(error),
300
+ stack: error instanceof Error ? error.stack : undefined
301
+ });
268
302
  }
269
303
  return null;
270
304
  }
@@ -315,6 +349,7 @@ var init_config = __esm(() => {
315
349
  CONFIG_DEFAULTS = {
316
350
  logLevel: "info",
317
351
  bunReportWarnings: true,
352
+ failOnScannerError: false,
318
353
  osv: {
319
354
  apiBaseUrl: OSV_API.BASE_URL,
320
355
  timeoutMs: OSV_API.TIMEOUT_MS,
@@ -349,6 +384,7 @@ var init_config = __esm(() => {
349
384
  packages: z.record(z.string(), IgnorePackageRuleSchema).optional(),
350
385
  logLevel: z.enum(["debug", "info", "warn", "error"]).optional(),
351
386
  bunReportWarnings: z.boolean().optional(),
387
+ failOnScannerError: z.boolean().optional(),
352
388
  osv: OsvConfigSchema.optional(),
353
389
  npm: NpmConfigSchema.optional()
354
390
  });
@@ -447,6 +483,7 @@ function createOSVClient(options = {}) {
447
483
  const baseUrl = osvConfig.apiBaseUrl ?? OSV_API.BASE_URL;
448
484
  const timeout = osvConfig.timeoutMs ?? OSV_API.TIMEOUT_MS;
449
485
  const useBatch = !(osvConfig.disableBatch ?? false);
486
+ const failOnError = options.failOnScannerError ?? false;
450
487
  function deduplicatePackages(packages) {
451
488
  const packageMap = new Map;
452
489
  for (const pkg of packages) {
@@ -505,8 +542,15 @@ function createOSVClient(options = {}) {
505
542
  return OSVVulnerabilitySchema.parse(data);
506
543
  }, `Get vulnerability ${id}`);
507
544
  } catch (error) {
545
+ const message = error instanceof Error ? error.message : String(error);
546
+ if (failOnError) {
547
+ logger.error(`Failed to fetch vulnerability ${id} (strict mode)`, {
548
+ error: message
549
+ });
550
+ throw error;
551
+ }
508
552
  logger.warn(`Failed to fetch vulnerability ${id}`, {
509
- error: error instanceof Error ? error.message : String(error)
553
+ error: message
510
554
  });
511
555
  return null;
512
556
  }
@@ -521,6 +565,18 @@ function createOSVClient(options = {}) {
521
565
  for (let i = 0;i < uniqueIds.length; i += chunkSize) {
522
566
  const chunk = uniqueIds.slice(i, i + chunkSize);
523
567
  const chunkResults = await Promise.allSettled(chunk.map((id) => fetchSingleVulnerability(id)));
568
+ if (failOnError) {
569
+ const rejections = chunkResults.filter((r) => r.status === "rejected");
570
+ if (rejections.length > 0) {
571
+ const firstError = rejections[0].reason;
572
+ const message = firstError instanceof Error ? firstError.message : String(firstError);
573
+ logger.error(`Failed to fetch vulnerability details (strict mode)`, {
574
+ error: message,
575
+ failedCount: rejections.length
576
+ });
577
+ throw firstError;
578
+ }
579
+ }
524
580
  for (const result of chunkResults) {
525
581
  if (result.status === "fulfilled" && result.value) {
526
582
  vulnerabilities.push(result.value);
@@ -538,8 +594,16 @@ function createOSVClient(options = {}) {
538
594
  const batchIds = await executeBatchQuery(batchQueries);
539
595
  vulnerabilityIds.push(...batchIds);
540
596
  } catch (error) {
597
+ const message = error instanceof Error ? error.message : String(error);
598
+ if (failOnError) {
599
+ logger.error(`Batch query failed for ${batchQueries.length} packages (strict mode)`, {
600
+ error: message,
601
+ startIndex: i
602
+ });
603
+ throw error;
604
+ }
541
605
  logger.error(`Batch query failed for ${batchQueries.length} packages`, {
542
- error: error instanceof Error ? error.message : String(error),
606
+ error: message,
543
607
  startIndex: i
544
608
  });
545
609
  }
@@ -577,8 +641,15 @@ function createOSVClient(options = {}) {
577
641
  break;
578
642
  }
579
643
  } catch (error) {
644
+ const message = error instanceof Error ? error.message : String(error);
645
+ if (failOnError) {
646
+ logger.error(`Query failed for ${query.package?.name || "unknown"}@${query.version || "unknown"} (strict mode)`, {
647
+ error: message
648
+ });
649
+ throw error;
650
+ }
580
651
  logger.warn(`Query failed for ${query.package?.name || "unknown"}@${query.version || "unknown"}`, {
581
- error: error instanceof Error ? error.message : String(error)
652
+ error: message
582
653
  });
583
654
  break;
584
655
  }
@@ -587,6 +658,18 @@ function createOSVClient(options = {}) {
587
658
  }
588
659
  async function queryIndividually(queries) {
589
660
  const responses = await Promise.allSettled(queries.map((query) => querySinglePackage(query)));
661
+ if (failOnError) {
662
+ const rejections = responses.filter((r) => r.status === "rejected");
663
+ if (rejections.length > 0) {
664
+ const firstError = rejections[0].reason;
665
+ const message = firstError instanceof Error ? firstError.message : String(firstError);
666
+ logger.error(`Individual query failed (strict mode)`, {
667
+ error: message,
668
+ failedCount: rejections.length
669
+ });
670
+ throw firstError;
671
+ }
672
+ }
590
673
  const vulnerabilities = [];
591
674
  let successCount = 0;
592
675
  for (const response of responses) {
@@ -901,6 +984,8 @@ var init_processor = __esm(() => {
901
984
  function isNewOptionsFormat(options) {
902
985
  if ("osv" in options)
903
986
  return true;
987
+ if ("failOnScannerError" in options)
988
+ return true;
904
989
  if ("ignore" in options && options.ignore && typeof options.ignore === "object" && !Array.isArray(options.ignore)) {
905
990
  return true;
906
991
  }
@@ -909,14 +994,32 @@ function isNewOptionsFormat(options) {
909
994
  function createOSVSource(options = {}) {
910
995
  let ignoreConfig;
911
996
  let osvConfig;
997
+ let failOnScannerError;
912
998
  if (isNewOptionsFormat(options)) {
913
- ignoreConfig = options.ignore ?? {};
999
+ const ignoreFromOptions = options.ignore;
1000
+ const packagesFromOptions = options.packages;
1001
+ if (ignoreFromOptions !== undefined) {
1002
+ if (Array.isArray(ignoreFromOptions)) {
1003
+ ignoreConfig = { ignore: ignoreFromOptions, packages: packagesFromOptions };
1004
+ } else {
1005
+ ignoreConfig = {
1006
+ ignore: ignoreFromOptions.ignore,
1007
+ packages: ignoreFromOptions.packages ?? packagesFromOptions
1008
+ };
1009
+ }
1010
+ } else if (packagesFromOptions !== undefined) {
1011
+ ignoreConfig = { ignore: undefined, packages: packagesFromOptions };
1012
+ } else {
1013
+ ignoreConfig = {};
1014
+ }
914
1015
  osvConfig = options.osv;
1016
+ failOnScannerError = options.failOnScannerError;
915
1017
  } else {
916
1018
  ignoreConfig = options;
917
1019
  osvConfig = undefined;
1020
+ failOnScannerError = undefined;
918
1021
  }
919
- const client = createOSVClient({ osv: osvConfig });
1022
+ const client = createOSVClient({ osv: osvConfig, failOnScannerError });
920
1023
  const processor = createVulnerabilityProcessor(ignoreConfig);
921
1024
  return {
922
1025
  name: "osv",
@@ -1005,6 +1108,7 @@ function createNpmAuditClient(options = {}) {
1005
1108
  const npmConfig = options.npm ?? CONFIG_DEFAULTS.npm;
1006
1109
  const registryUrl = npmConfig.registryUrl ?? NPM_AUDIT_API.REGISTRY_URL;
1007
1110
  const timeout = npmConfig.timeoutMs ?? NPM_AUDIT_API.TIMEOUT_MS;
1111
+ const failOnError = options.failOnScannerError ?? false;
1008
1112
  function deduplicatePackages(packages) {
1009
1113
  const packageMap = new Map;
1010
1114
  for (const pkg of packages) {
@@ -1083,8 +1187,16 @@ function createNpmAuditClient(options = {}) {
1083
1187
  const batchAdvisories = await executeBulkQuery(payload);
1084
1188
  advisories.push(...batchAdvisories);
1085
1189
  } catch (error) {
1190
+ const message = error instanceof Error ? error.message : String(error);
1191
+ if (failOnError) {
1192
+ logger.error(`Batch query failed for ${batch.length} packages (strict mode)`, {
1193
+ error: message,
1194
+ startIndex: i
1195
+ });
1196
+ throw error;
1197
+ }
1086
1198
  logger.error(`Batch query failed for ${batch.length} packages`, {
1087
- error: error instanceof Error ? error.message : String(error),
1199
+ error: message,
1088
1200
  startIndex: i
1089
1201
  });
1090
1202
  }
@@ -1101,7 +1213,23 @@ function createNpmAuditClient(options = {}) {
1101
1213
  if (uniquePackages.length > NPM_AUDIT_API.MAX_PACKAGES_PER_REQUEST) {
1102
1214
  return await queryInBatches(uniquePackages);
1103
1215
  }
1104
- return await executeBulkQuery(requestPayload);
1216
+ try {
1217
+ return await executeBulkQuery(requestPayload);
1218
+ } catch (error) {
1219
+ const message = error instanceof Error ? error.message : String(error);
1220
+ if (failOnError) {
1221
+ logger.error(`Bulk query failed (strict mode)`, {
1222
+ error: message,
1223
+ packageCount: uniquePackages.length
1224
+ });
1225
+ throw error;
1226
+ }
1227
+ logger.error(`Bulk query failed`, {
1228
+ error: message,
1229
+ packageCount: uniquePackages.length
1230
+ });
1231
+ return [];
1232
+ }
1105
1233
  }
1106
1234
  return {
1107
1235
  queryVulnerabilities
@@ -1261,6 +1389,8 @@ var init_processor2 = __esm(() => {
1261
1389
  function isNewOptionsFormat2(options) {
1262
1390
  if ("npm" in options)
1263
1391
  return true;
1392
+ if ("failOnScannerError" in options)
1393
+ return true;
1264
1394
  if ("ignore" in options && options.ignore && typeof options.ignore === "object" && !Array.isArray(options.ignore)) {
1265
1395
  return true;
1266
1396
  }
@@ -1269,14 +1399,32 @@ function isNewOptionsFormat2(options) {
1269
1399
  function createNpmSource(options = {}) {
1270
1400
  let ignoreConfig;
1271
1401
  let npmConfig;
1402
+ let failOnScannerError;
1272
1403
  if (isNewOptionsFormat2(options)) {
1273
- ignoreConfig = options.ignore ?? {};
1404
+ const ignoreFromOptions = options.ignore;
1405
+ const packagesFromOptions = options.packages;
1406
+ if (ignoreFromOptions !== undefined) {
1407
+ if (Array.isArray(ignoreFromOptions)) {
1408
+ ignoreConfig = { ignore: ignoreFromOptions, packages: packagesFromOptions };
1409
+ } else {
1410
+ ignoreConfig = {
1411
+ ignore: ignoreFromOptions.ignore,
1412
+ packages: ignoreFromOptions.packages ?? packagesFromOptions
1413
+ };
1414
+ }
1415
+ } else if (packagesFromOptions !== undefined) {
1416
+ ignoreConfig = { ignore: undefined, packages: packagesFromOptions };
1417
+ } else {
1418
+ ignoreConfig = {};
1419
+ }
1274
1420
  npmConfig = options.npm;
1421
+ failOnScannerError = options.failOnScannerError;
1275
1422
  } else {
1276
1423
  ignoreConfig = options;
1277
1424
  npmConfig = undefined;
1425
+ failOnScannerError = undefined;
1278
1426
  }
1279
- const client = createNpmAuditClient({ npm: npmConfig });
1427
+ const client = createNpmAuditClient({ npm: npmConfig, failOnScannerError });
1280
1428
  const processor = createAdvisoryProcessor(ignoreConfig);
1281
1429
  return {
1282
1430
  name: "npm",
@@ -1306,28 +1454,28 @@ var init_src3 = __esm(() => {
1306
1454
  function extractIgnoreConfig(config) {
1307
1455
  return { ignore: config.ignore, packages: config.packages };
1308
1456
  }
1309
- function createSource(type, config) {
1457
+ function createSource(type, config, failOnScannerError) {
1310
1458
  const ignoreConfig = extractIgnoreConfig(config);
1311
1459
  switch (type) {
1312
1460
  case "osv":
1313
- return createOSVSource({ ignore: ignoreConfig, osv: config.osv });
1461
+ return createOSVSource({ ignore: ignoreConfig, osv: config.osv, failOnScannerError });
1314
1462
  case "npm":
1315
- return createNpmSource({ ignore: ignoreConfig, npm: config.npm });
1463
+ return createNpmSource({ ignore: ignoreConfig, npm: config.npm, failOnScannerError });
1316
1464
  default:
1317
1465
  throw new Error(`Unknown source type: ${type}`);
1318
1466
  }
1319
1467
  }
1320
- function createSources(type, config) {
1468
+ function createSources(type, config, failOnScannerError) {
1321
1469
  const ignoreConfig = extractIgnoreConfig(config);
1322
1470
  switch (type) {
1323
1471
  case "osv":
1324
- return [createOSVSource({ ignore: ignoreConfig, osv: config.osv })];
1472
+ return [createOSVSource({ ignore: ignoreConfig, osv: config.osv, failOnScannerError })];
1325
1473
  case "npm":
1326
- return [createNpmSource({ ignore: ignoreConfig, npm: config.npm })];
1474
+ return [createNpmSource({ ignore: ignoreConfig, npm: config.npm, failOnScannerError })];
1327
1475
  case "both":
1328
1476
  return [
1329
- createOSVSource({ ignore: ignoreConfig, osv: config.osv }),
1330
- createNpmSource({ ignore: ignoreConfig, npm: config.npm })
1477
+ createOSVSource({ ignore: ignoreConfig, osv: config.osv, failOnScannerError }),
1478
+ createNpmSource({ ignore: ignoreConfig, npm: config.npm, failOnScannerError })
1331
1479
  ];
1332
1480
  default:
1333
1481
  throw new Error(`Unknown source type: ${type}`);
@@ -1339,10 +1487,11 @@ var init_factory = __esm(() => {
1339
1487
  });
1340
1488
 
1341
1489
  // src/sources/multi.ts
1342
- function createMultiSourceScanner(sources) {
1490
+ function createMultiSourceScanner(sources, options) {
1343
1491
  if (sources.length === 0) {
1344
1492
  throw new Error("MultiSourceScanner requires at least one source");
1345
1493
  }
1494
+ const failOnError = options?.failOnScannerError ?? false;
1346
1495
  function isHigherSeverity(a, b) {
1347
1496
  const priority = { fatal: 2, warn: 1 };
1348
1497
  return (priority[a] ?? 0) > (priority[b] ?? 0);
@@ -1379,8 +1528,9 @@ function createMultiSourceScanner(sources) {
1379
1528
  async function scan(packages) {
1380
1529
  const sourceNames = sources.map((s) => s.name).join(", ");
1381
1530
  logger.info(`Scanning with sources: ${sourceNames}`);
1382
- const results = await Promise.allSettled(sources.map((source) => source.scan(packages)));
1531
+ const results = await Promise.allSettled(sources.map((source) => Promise.resolve().then(() => source.scan(packages))));
1383
1532
  const allAdvisories = [];
1533
+ const failures = [];
1384
1534
  for (const [i, result] of results.entries()) {
1385
1535
  const source = sources[i];
1386
1536
  if (!source)
@@ -1389,11 +1539,15 @@ function createMultiSourceScanner(sources) {
1389
1539
  logger.debug(`[${source.name}] Found ${result.value.length} advisories`);
1390
1540
  allAdvisories.push(...result.value);
1391
1541
  } else {
1392
- logger.error(`[${source.name}] Scan failed`, {
1393
- error: result.reason instanceof Error ? result.reason.message : String(result.reason)
1394
- });
1542
+ const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
1543
+ logger.error(`[${source.name}] Scan failed`, { error: errorMessage });
1544
+ failures.push({ name: source.name, error: errorMessage });
1395
1545
  }
1396
1546
  }
1547
+ if (failOnError && failures.length > 0) {
1548
+ const details = failures.map((f) => `${f.name}: ${f.error}`).join("; ");
1549
+ throw new Error(`bun-scan: scan failed for ${failures.length === 1 ? "source" : "sources"} ` + `${failures.map((f) => `"${f.name}"`).join(", ")}. ` + `Details: ${details}. ` + `failOnScannerError=true requires all configured sources to succeed.`);
1550
+ }
1397
1551
  return deduplicateAdvisories(allAdvisories);
1398
1552
  }
1399
1553
  return {
@@ -1567,12 +1721,16 @@ var init_src4 = __esm(async () => {
1567
1721
  scanner = {
1568
1722
  version: "1",
1569
1723
  async scan({ packages }) {
1724
+ let failOnScannerError = parseEnvBoolean(ENV.FAIL_ON_SCANNER_ERROR) === true;
1570
1725
  try {
1571
1726
  logger.debug(`Starting vulnerability scan for ${packages.length} packages`);
1572
1727
  const config = await loadConfig();
1573
1728
  const bunReportWarnings = config.bunReportWarnings ?? CONFIG_DEFAULTS.bunReportWarnings;
1574
- const sources = createSources(config.source ?? "osv", config);
1575
- const multiScanner = createMultiSourceScanner(sources);
1729
+ if (parseEnvBoolean(ENV.FAIL_ON_SCANNER_ERROR) === undefined) {
1730
+ failOnScannerError = config.failOnScannerError ?? failOnScannerError;
1731
+ }
1732
+ const sources = createSources(config.source ?? "osv", config, failOnScannerError);
1733
+ const multiScanner = createMultiSourceScanner(sources, { failOnScannerError });
1576
1734
  const advisories = await multiScanner.scan(packages);
1577
1735
  logger.info(`Scan completed: ${advisories.length} advisories found for ${packages.length} packages`);
1578
1736
  if (!bunReportWarnings) {
@@ -1590,6 +1748,12 @@ var init_src4 = __esm(async () => {
1590
1748
  return advisories;
1591
1749
  } catch (error) {
1592
1750
  const message = error instanceof Error ? error.message : String(error);
1751
+ if (failOnScannerError) {
1752
+ logger.error("Scanner error in strict mode \u2014 failing scan", {
1753
+ error: message
1754
+ });
1755
+ throw error;
1756
+ }
1593
1757
  logger.error("Scanner encountered an unexpected error", {
1594
1758
  error: message
1595
1759
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-scan",
3
- "version": "1.1.0",
3
+ "version": "1.1.1-beta.1",
4
4
  "description": "Vulnerability scanner for Bun projects",
5
5
  "keywords": [
6
6
  "audit",
@@ -49,15 +49,15 @@
49
49
  "lint": "oxlint check"
50
50
  },
51
51
  "dependencies": {
52
- "zod": "4.3.5"
52
+ "zod": "4.3.6"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@repo/core": "workspace:*",
56
56
  "@repo/source-npm": "workspace:*",
57
57
  "@repo/source-osv": "workspace:*",
58
- "@types/bun": "1.3.6",
59
- "@typescript/native-preview": "7.0.0-dev.20260114.1",
60
- "bun-types": "1.3.6"
58
+ "@types/bun": "1.3.8",
59
+ "@typescript/native-preview": "7.0.0-dev.20260131.1",
60
+ "bun-types": "1.3.8"
61
61
  },
62
62
  "engines": {
63
63
  "bun": ">=1.0.0"