bardscan 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import * as core from '@bardscan/core';
4
+ export interface CliDeps {
5
+ mkdir: typeof mkdir;
6
+ writeFile: typeof writeFile;
7
+ runScan: typeof core.runScan;
8
+ updateAdvisoryDb: typeof core.updateAdvisoryDb;
9
+ buildMarkdownReport: typeof core.buildMarkdownReport;
10
+ buildSarifReport: (report: Awaited<ReturnType<typeof core.runScan>>) => object;
11
+ shouldFail: typeof core.shouldFail;
12
+ redactReportPaths: typeof core.redactReportPaths;
13
+ stdout: {
14
+ write: (text: string) => void;
15
+ isTTY?: boolean;
16
+ };
17
+ stderr: {
18
+ write: (text: string) => void;
19
+ };
20
+ }
21
+ export declare function runCli(rawArgs: string[], deps?: CliDeps): Promise<number>;
package/dist/index.js ADDED
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import yargs from 'yargs';
7
+ import { hideBin } from 'yargs/helpers';
8
+ import * as core from '@bardscan/core';
9
+ const defaultDeps = {
10
+ mkdir,
11
+ writeFile,
12
+ runScan: core.runScan,
13
+ updateAdvisoryDb: core.updateAdvisoryDb,
14
+ buildMarkdownReport: core.buildMarkdownReport,
15
+ buildSarifReport: (report) => typeof core.buildSarifReport === 'function'
16
+ ? core.buildSarifReport(report)
17
+ : {
18
+ version: '2.1.0',
19
+ runs: []
20
+ },
21
+ shouldFail: core.shouldFail,
22
+ redactReportPaths: core.redactReportPaths,
23
+ stdout: process.stdout,
24
+ stderr: process.stderr
25
+ };
26
+ export async function runCli(rawArgs, deps = defaultDeps) {
27
+ let exitCode = 0;
28
+ const parser = yargs(rawArgs)
29
+ .scriptName('bardscan')
30
+ .command('scan [path]', 'Scan a TypeScript project for vulnerable npm dependencies', (cmd) => cmd
31
+ .positional('path', {
32
+ type: 'string',
33
+ default: '.',
34
+ describe: 'Project path to scan'
35
+ })
36
+ .option('format', {
37
+ choices: ['json', 'md', 'sarif', 'both'],
38
+ default: 'both'
39
+ })
40
+ .option('out-dir', {
41
+ type: 'string',
42
+ default: path.join(os.tmpdir(), 'bardscan')
43
+ })
44
+ .option('fail-on', {
45
+ choices: ['critical', 'high', 'medium', 'low', 'none'],
46
+ default: 'high'
47
+ })
48
+ .option('privacy', {
49
+ choices: ['strict', 'standard'],
50
+ default: 'strict',
51
+ describe: 'Privacy preset that controls network and output defaults'
52
+ })
53
+ .option('online', {
54
+ type: 'boolean',
55
+ default: false,
56
+ describe: 'Enable online advisory lookups during scan'
57
+ })
58
+ .option('offline', {
59
+ type: 'boolean',
60
+ describe: 'Force cache-only scan mode'
61
+ })
62
+ .option('unknown-as', {
63
+ choices: ['critical', 'high', 'medium', 'low', 'unknown'],
64
+ default: 'unknown'
65
+ })
66
+ .option('refresh-cache', {
67
+ type: 'boolean',
68
+ default: false
69
+ })
70
+ .option('osv-url', {
71
+ type: 'string',
72
+ describe: 'Custom OSV API base URL (for mirrors/proxies)'
73
+ })
74
+ .option('fallback-calls', {
75
+ type: 'boolean',
76
+ describe: 'Allow secondary network fallbacks for unresolved severities'
77
+ })
78
+ .option('redact-paths', {
79
+ type: 'boolean',
80
+ describe: 'Redact target path and evidence paths in outputs'
81
+ })
82
+ .option('evidence', {
83
+ choices: ['none', 'imports'],
84
+ describe: 'Evidence collection mode'
85
+ })
86
+ .option('telemetry', {
87
+ choices: ['off', 'on'],
88
+ describe: 'Reserved telemetry mode (default off)'
89
+ })
90
+ .option('list-findings', {
91
+ choices: ['none', 'critical-high', 'medium-up', 'all'],
92
+ default: 'none',
93
+ describe: 'Print finding details in CLI output'
94
+ })
95
+ .option('findings-json', {
96
+ type: 'string',
97
+ describe: 'Write filtered finding details as JSON'
98
+ }), async (argv) => {
99
+ try {
100
+ const projectPath = path.resolve(String(argv.path));
101
+ const outDir = path.resolve(String(argv.outDir));
102
+ const settings = resolveScanSettings({
103
+ privacy: argv.privacy,
104
+ online: Boolean(argv.online),
105
+ offline: argv.offline,
106
+ fallbackCalls: argv.fallbackCalls,
107
+ redactPaths: argv.redactPaths,
108
+ evidence: argv.evidence,
109
+ telemetry: argv.telemetry
110
+ });
111
+ await deps.mkdir(outDir, { recursive: true });
112
+ const report = await deps.runScan({
113
+ projectPath,
114
+ outDir,
115
+ failOn: argv.failOn,
116
+ offline: settings.offline,
117
+ unknownAs: argv.unknownAs,
118
+ refreshCache: Boolean(argv.refreshCache),
119
+ osvUrl: argv.osvUrl ? String(argv.osvUrl) : undefined,
120
+ enableNetworkFallbacks: settings.enableNetworkFallbacks,
121
+ evidenceMode: settings.evidenceMode
122
+ });
123
+ const redact = typeof deps.redactReportPaths === 'function' ? deps.redactReportPaths : (r) => r;
124
+ const displayReport = settings.redactPaths ? redact(report) : report;
125
+ const jsonPath = path.join(outDir, 'report.json');
126
+ const mdPath = path.join(outDir, 'report.md');
127
+ const sarifPath = path.join(outDir, 'report.sarif');
128
+ if (argv.format === 'json' || argv.format === 'both') {
129
+ await deps.writeFile(jsonPath, JSON.stringify(displayReport, null, 2));
130
+ deps.stdout.write(`${jsonPath}\n`);
131
+ }
132
+ if (argv.format === 'md' || argv.format === 'both') {
133
+ await deps.writeFile(mdPath, deps.buildMarkdownReport(displayReport));
134
+ deps.stdout.write(`${mdPath}\n`);
135
+ }
136
+ if (argv.format === 'sarif') {
137
+ await deps.writeFile(sarifPath, JSON.stringify(deps.buildSarifReport(displayReport), null, 2));
138
+ deps.stdout.write(`${sarifPath}\n`);
139
+ }
140
+ const thresholdHit = argv.failOn !== 'none' &&
141
+ report.findings.some((f) => deps.shouldFail(argv.failOn, f.severity));
142
+ deps.stdout.write(buildCliSummary(displayReport, String(argv.failOn), thresholdHit, useColor(deps.stdout)));
143
+ deps.stdout.write(buildFindingsList(displayReport, argv.listFindings, useColor(deps.stdout)));
144
+ if (argv.findingsJson) {
145
+ const findingsJsonPath = path.resolve(String(argv.findingsJson));
146
+ const filteredFindings = filterFindings(displayReport, argv.listFindings);
147
+ await deps.writeFile(findingsJsonPath, JSON.stringify(filteredFindings, null, 2));
148
+ deps.stdout.write(`${findingsJsonPath}\n`);
149
+ }
150
+ if (thresholdHit) {
151
+ exitCode = 1;
152
+ return;
153
+ }
154
+ exitCode = 0;
155
+ }
156
+ catch (error) {
157
+ deps.stderr.write(`${error.message}\n`);
158
+ exitCode = 2;
159
+ }
160
+ })
161
+ .command('db update [path]', 'Refresh advisory cache for dependencies in the project lockfile', (cmd) => cmd
162
+ .positional('path', {
163
+ type: 'string',
164
+ default: '.',
165
+ describe: 'Project path to index dependencies from'
166
+ })
167
+ .option('out-dir', {
168
+ type: 'string',
169
+ default: path.join(os.tmpdir(), 'bardscan')
170
+ })
171
+ .option('refresh-cache', {
172
+ type: 'boolean',
173
+ default: false
174
+ })
175
+ .option('osv-url', {
176
+ type: 'string',
177
+ describe: 'Custom OSV API base URL (for mirrors/proxies)'
178
+ })
179
+ .option('fallback-calls', {
180
+ type: 'boolean',
181
+ default: true,
182
+ describe: 'Allow secondary network fallbacks for unresolved severities'
183
+ }), async (argv) => {
184
+ try {
185
+ const projectPath = path.resolve(String(argv.path));
186
+ const outDir = path.resolve(String(argv.outDir));
187
+ await deps.mkdir(outDir, { recursive: true });
188
+ const update = await deps.updateAdvisoryDb({
189
+ projectPath,
190
+ outDir,
191
+ refreshCache: Boolean(argv.refreshCache),
192
+ osvUrl: argv.osvUrl ? String(argv.osvUrl) : undefined,
193
+ enableNetworkFallbacks: Boolean(argv.fallbackCalls)
194
+ });
195
+ deps.stdout.write(buildDbUpdateSummary(update, useColor(deps.stdout)));
196
+ exitCode = 0;
197
+ }
198
+ catch (error) {
199
+ deps.stderr.write(`${error.message}\n`);
200
+ exitCode = 2;
201
+ }
202
+ })
203
+ .demandCommand(1)
204
+ .strict()
205
+ .help();
206
+ await parser.parseAsync();
207
+ return exitCode;
208
+ }
209
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
210
+ void runCli(hideBin(process.argv)).then((code) => {
211
+ process.exitCode = code;
212
+ });
213
+ }
214
+ function resolveScanSettings(input) {
215
+ const preset = input.privacy === 'strict'
216
+ ? {
217
+ offline: true,
218
+ enableNetworkFallbacks: false,
219
+ redactPaths: true,
220
+ evidenceMode: 'none',
221
+ telemetry: 'off'
222
+ }
223
+ : {
224
+ offline: true,
225
+ enableNetworkFallbacks: true,
226
+ redactPaths: false,
227
+ evidenceMode: 'imports',
228
+ telemetry: 'off'
229
+ };
230
+ let offline = preset.offline;
231
+ if (input.online)
232
+ offline = false;
233
+ if (typeof input.offline === 'boolean')
234
+ offline = input.offline;
235
+ if (input.privacy === 'strict' && !offline) {
236
+ throw new Error('privacy strict disallows online scanning. Remove --online or use --privacy standard.');
237
+ }
238
+ if ((input.telemetry ?? preset.telemetry) === 'on' && input.privacy === 'strict') {
239
+ throw new Error('privacy strict disallows telemetry.');
240
+ }
241
+ return {
242
+ offline,
243
+ enableNetworkFallbacks: input.fallbackCalls ?? preset.enableNetworkFallbacks,
244
+ redactPaths: input.redactPaths ?? preset.redactPaths,
245
+ evidenceMode: input.evidence ?? preset.evidenceMode
246
+ };
247
+ }
248
+ function buildCliSummary(report, failOn, thresholdHit, color) {
249
+ const sev = report.summary.bySeverity;
250
+ const conf = report.summary.byConfidence;
251
+ const lines = [
252
+ '',
253
+ colorize('bardscan summary', 'cyan', color),
254
+ `target: ${report.targetPath}`,
255
+ `dependencies: ${report.summary.dependencyCount}`,
256
+ `findings: ${report.summary.findingsCount}`,
257
+ `severity: critical=${colorize(String(sev.critical), 'magenta', color)} high=${colorize(String(sev.high), 'red', color)} medium=${colorize(String(sev.medium), 'yellow', color)} low=${colorize(String(sev.low), 'green', color)} unknown=${colorize(String(sev.unknown), 'gray', color)}`,
258
+ `confidence: high=${colorize(String(conf.high), 'green', color)} medium=${colorize(String(conf.medium), 'yellow', color)} low=${colorize(String(conf.low), 'red', color)} unknown=${colorize(String(conf.unknown), 'gray', color)}`,
259
+ `fail-on: ${failOn}`,
260
+ `threshold hit: ${thresholdHit ? colorize('yes', 'red', color) : colorize('no', 'green', color)}`
261
+ ];
262
+ return `${lines.join('\n')}\n`;
263
+ }
264
+ function buildDbUpdateSummary(update, color) {
265
+ const lines = [
266
+ '',
267
+ colorize('bardscan db update', 'cyan', color),
268
+ `target: ${update.projectPath}`,
269
+ `dependencies: ${update.dependencyCount}`,
270
+ `queried: ${update.queriedCount}`,
271
+ `sources: osv=${colorize(String(update.bySource.osv), 'green', color)} cache=${colorize(String(update.bySource.cache), 'yellow', color)} unknown=${colorize(String(update.bySource.unknown), 'red', color)}`
272
+ ];
273
+ return `${lines.join('\n')}\n`;
274
+ }
275
+ function buildFindingsList(report, mode, color) {
276
+ if (mode === 'none')
277
+ return '';
278
+ const filtered = filterFindings(report, mode);
279
+ if (filtered.length === 0) {
280
+ return `\n${colorize('finding details', 'cyan', color)}\n(no matching findings)\n`;
281
+ }
282
+ const lines = ['', colorize('finding details', 'cyan', color)];
283
+ for (const finding of filtered) {
284
+ const vulnIds = finding.vulnerabilities.slice(0, 3).map((v) => v.id).join(', ');
285
+ const evidenceCount = finding.evidence.length;
286
+ const unknownReason = finding.unknownReason ? ` unknown-reason=${finding.unknownReason}` : '';
287
+ lines.push(`- ${colorize(finding.severity, severityColor(finding.severity), color)} ${finding.packageName}@${finding.version}` +
288
+ ` confidence=${finding.confidence} direct=${finding.direct ? 'yes' : 'no'} evidence=${evidenceCount}` +
289
+ ` source=${finding.source}${unknownReason} ids=${vulnIds || 'n/a'}`);
290
+ }
291
+ return `${lines.join('\n')}\n`;
292
+ }
293
+ function filterFindings(report, mode) {
294
+ if (mode === 'all')
295
+ return report.findings;
296
+ if (mode === 'critical-high') {
297
+ return report.findings.filter((f) => f.severity === 'critical' || f.severity === 'high');
298
+ }
299
+ if (mode === 'medium-up') {
300
+ return report.findings.filter((f) => f.severity === 'critical' || f.severity === 'high' || f.severity === 'medium');
301
+ }
302
+ return [];
303
+ }
304
+ function severityColor(severity) {
305
+ if (severity === 'critical')
306
+ return 'magenta';
307
+ if (severity === 'high')
308
+ return 'red';
309
+ if (severity === 'medium')
310
+ return 'yellow';
311
+ if (severity === 'low')
312
+ return 'green';
313
+ return 'gray';
314
+ }
315
+ function useColor(stdout) {
316
+ return Boolean(stdout.isTTY && !process.env.NO_COLOR);
317
+ }
318
+ function colorize(text, color, enabled) {
319
+ if (!enabled)
320
+ return text;
321
+ const code = {
322
+ red: 31,
323
+ yellow: 33,
324
+ green: 32,
325
+ cyan: 36,
326
+ magenta: 35,
327
+ gray: 90
328
+ };
329
+ return `\u001b[${code[color]}m${text}\u001b[0m`;
330
+ }
331
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "version": "0.1.4",
3
+ "name": "bardscan",
4
+ "type": "module",
5
+ "bin": {
6
+ "bardscan": "dist/index.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc -p tsconfig.json",
10
+ "lint": "eslint src test --ext .ts",
11
+ "test": "corepack pnpm --filter @bardscan/core build && node --test --import tsx test/*.test.ts"
12
+ },
13
+ "dependencies": {
14
+ "@bardscan/core": "workspace:*",
15
+ "yargs": "^17.7.2"
16
+ },
17
+ "devDependencies": {
18
+ "@types/yargs": "^17.0.33",
19
+ "tsx": "^4.19.2"
20
+ }
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import yargs from 'yargs';
7
+ import { hideBin } from 'yargs/helpers';
8
+ import * as core from '@bardscan/core';
9
+ import { Severity } from '@bardscan/core';
10
+
11
+ export interface CliDeps {
12
+ mkdir: typeof mkdir;
13
+ writeFile: typeof writeFile;
14
+ runScan: typeof core.runScan;
15
+ updateAdvisoryDb: typeof core.updateAdvisoryDb;
16
+ buildMarkdownReport: typeof core.buildMarkdownReport;
17
+ buildSarifReport: (report: Awaited<ReturnType<typeof core.runScan>>) => object;
18
+ shouldFail: typeof core.shouldFail;
19
+ redactReportPaths: typeof core.redactReportPaths;
20
+ stdout: { write: (text: string) => void; isTTY?: boolean };
21
+ stderr: { write: (text: string) => void };
22
+ }
23
+
24
+ type FailOn = Severity | 'none';
25
+ type ListFindingsMode = 'none' | 'critical-high' | 'medium-up' | 'all';
26
+ type PrivacyMode = 'strict' | 'standard';
27
+ type EvidenceMode = 'none' | 'imports';
28
+
29
+ const defaultDeps: CliDeps = {
30
+ mkdir,
31
+ writeFile,
32
+ runScan: core.runScan,
33
+ updateAdvisoryDb: core.updateAdvisoryDb,
34
+ buildMarkdownReport: core.buildMarkdownReport,
35
+ buildSarifReport: (report) =>
36
+ typeof (core as { buildSarifReport?: (r: typeof report) => object }).buildSarifReport === 'function'
37
+ ? (core as { buildSarifReport: (r: typeof report) => object }).buildSarifReport(report)
38
+ : {
39
+ version: '2.1.0',
40
+ runs: []
41
+ },
42
+ shouldFail: core.shouldFail,
43
+ redactReportPaths: core.redactReportPaths,
44
+ stdout: process.stdout,
45
+ stderr: process.stderr
46
+ };
47
+
48
+ export async function runCli(rawArgs: string[], deps: CliDeps = defaultDeps): Promise<number> {
49
+ let exitCode = 0;
50
+
51
+ const parser = yargs(rawArgs)
52
+ .scriptName('bardscan')
53
+ .command(
54
+ 'scan [path]',
55
+ 'Scan a TypeScript project for vulnerable npm dependencies',
56
+ (cmd) =>
57
+ cmd
58
+ .positional('path', {
59
+ type: 'string',
60
+ default: '.',
61
+ describe: 'Project path to scan'
62
+ })
63
+ .option('format', {
64
+ choices: ['json', 'md', 'sarif', 'both'] as const,
65
+ default: 'both'
66
+ })
67
+ .option('out-dir', {
68
+ type: 'string',
69
+ default: path.join(os.tmpdir(), 'bardscan')
70
+ })
71
+ .option('fail-on', {
72
+ choices: ['critical', 'high', 'medium', 'low', 'none'] as const,
73
+ default: 'high'
74
+ })
75
+ .option('privacy', {
76
+ choices: ['strict', 'standard'] as const,
77
+ default: 'strict',
78
+ describe: 'Privacy preset that controls network and output defaults'
79
+ })
80
+ .option('online', {
81
+ type: 'boolean',
82
+ default: false,
83
+ describe: 'Enable online advisory lookups during scan'
84
+ })
85
+ .option('offline', {
86
+ type: 'boolean',
87
+ describe: 'Force cache-only scan mode'
88
+ })
89
+ .option('unknown-as', {
90
+ choices: ['critical', 'high', 'medium', 'low', 'unknown'] as const,
91
+ default: 'unknown'
92
+ })
93
+ .option('refresh-cache', {
94
+ type: 'boolean',
95
+ default: false
96
+ })
97
+ .option('osv-url', {
98
+ type: 'string',
99
+ describe: 'Custom OSV API base URL (for mirrors/proxies)'
100
+ })
101
+ .option('fallback-calls', {
102
+ type: 'boolean',
103
+ describe: 'Allow secondary network fallbacks for unresolved severities'
104
+ })
105
+ .option('redact-paths', {
106
+ type: 'boolean',
107
+ describe: 'Redact target path and evidence paths in outputs'
108
+ })
109
+ .option('evidence', {
110
+ choices: ['none', 'imports'] as const,
111
+ describe: 'Evidence collection mode'
112
+ })
113
+ .option('telemetry', {
114
+ choices: ['off', 'on'] as const,
115
+ describe: 'Reserved telemetry mode (default off)'
116
+ })
117
+ .option('list-findings', {
118
+ choices: ['none', 'critical-high', 'medium-up', 'all'] as const,
119
+ default: 'none',
120
+ describe: 'Print finding details in CLI output'
121
+ })
122
+ .option('findings-json', {
123
+ type: 'string',
124
+ describe: 'Write filtered finding details as JSON'
125
+ }),
126
+ async (argv) => {
127
+ try {
128
+ const projectPath = path.resolve(String(argv.path));
129
+ const outDir = path.resolve(String(argv.outDir));
130
+ const settings = resolveScanSettings({
131
+ privacy: argv.privacy as PrivacyMode,
132
+ online: Boolean(argv.online),
133
+ offline: argv.offline,
134
+ fallbackCalls: argv.fallbackCalls,
135
+ redactPaths: argv.redactPaths,
136
+ evidence: argv.evidence as EvidenceMode | undefined,
137
+ telemetry: argv.telemetry as 'off' | 'on' | undefined
138
+ });
139
+
140
+ await deps.mkdir(outDir, { recursive: true });
141
+
142
+ const report = await deps.runScan({
143
+ projectPath,
144
+ outDir,
145
+ failOn: argv.failOn as FailOn,
146
+ offline: settings.offline,
147
+ unknownAs: argv.unknownAs as Severity,
148
+ refreshCache: Boolean(argv.refreshCache),
149
+ osvUrl: argv.osvUrl ? String(argv.osvUrl) : undefined,
150
+ enableNetworkFallbacks: settings.enableNetworkFallbacks,
151
+ evidenceMode: settings.evidenceMode
152
+ });
153
+
154
+ const redact = typeof deps.redactReportPaths === 'function' ? deps.redactReportPaths : (r: typeof report) => r;
155
+ const displayReport = settings.redactPaths ? redact(report) : report;
156
+ const jsonPath = path.join(outDir, 'report.json');
157
+ const mdPath = path.join(outDir, 'report.md');
158
+ const sarifPath = path.join(outDir, 'report.sarif');
159
+
160
+ if (argv.format === 'json' || argv.format === 'both') {
161
+ await deps.writeFile(jsonPath, JSON.stringify(displayReport, null, 2));
162
+ deps.stdout.write(`${jsonPath}\n`);
163
+ }
164
+ if (argv.format === 'md' || argv.format === 'both') {
165
+ await deps.writeFile(mdPath, deps.buildMarkdownReport(displayReport));
166
+ deps.stdout.write(`${mdPath}\n`);
167
+ }
168
+ if (argv.format === 'sarif') {
169
+ await deps.writeFile(sarifPath, JSON.stringify(deps.buildSarifReport(displayReport), null, 2));
170
+ deps.stdout.write(`${sarifPath}\n`);
171
+ }
172
+
173
+ const thresholdHit =
174
+ argv.failOn !== 'none' &&
175
+ report.findings.some((f) => deps.shouldFail(argv.failOn as FailOn, f.severity));
176
+ deps.stdout.write(buildCliSummary(displayReport, String(argv.failOn), thresholdHit, useColor(deps.stdout)));
177
+ deps.stdout.write(buildFindingsList(displayReport, argv.listFindings as ListFindingsMode, useColor(deps.stdout)));
178
+ if (argv.findingsJson) {
179
+ const findingsJsonPath = path.resolve(String(argv.findingsJson));
180
+ const filteredFindings = filterFindings(displayReport, argv.listFindings as ListFindingsMode);
181
+ await deps.writeFile(findingsJsonPath, JSON.stringify(filteredFindings, null, 2));
182
+ deps.stdout.write(`${findingsJsonPath}\n`);
183
+ }
184
+
185
+ if (thresholdHit) {
186
+ exitCode = 1;
187
+ return;
188
+ }
189
+ exitCode = 0;
190
+ } catch (error) {
191
+ deps.stderr.write(`${(error as Error).message}\n`);
192
+ exitCode = 2;
193
+ }
194
+ }
195
+ )
196
+ .command(
197
+ 'db update [path]',
198
+ 'Refresh advisory cache for dependencies in the project lockfile',
199
+ (cmd) =>
200
+ cmd
201
+ .positional('path', {
202
+ type: 'string',
203
+ default: '.',
204
+ describe: 'Project path to index dependencies from'
205
+ })
206
+ .option('out-dir', {
207
+ type: 'string',
208
+ default: path.join(os.tmpdir(), 'bardscan')
209
+ })
210
+ .option('refresh-cache', {
211
+ type: 'boolean',
212
+ default: false
213
+ })
214
+ .option('osv-url', {
215
+ type: 'string',
216
+ describe: 'Custom OSV API base URL (for mirrors/proxies)'
217
+ })
218
+ .option('fallback-calls', {
219
+ type: 'boolean',
220
+ default: true,
221
+ describe: 'Allow secondary network fallbacks for unresolved severities'
222
+ }),
223
+ async (argv) => {
224
+ try {
225
+ const projectPath = path.resolve(String(argv.path));
226
+ const outDir = path.resolve(String(argv.outDir));
227
+ await deps.mkdir(outDir, { recursive: true });
228
+
229
+ const update = await deps.updateAdvisoryDb({
230
+ projectPath,
231
+ outDir,
232
+ refreshCache: Boolean(argv.refreshCache),
233
+ osvUrl: argv.osvUrl ? String(argv.osvUrl) : undefined,
234
+ enableNetworkFallbacks: Boolean(argv.fallbackCalls)
235
+ });
236
+
237
+ deps.stdout.write(buildDbUpdateSummary(update, useColor(deps.stdout)));
238
+ exitCode = 0;
239
+ } catch (error) {
240
+ deps.stderr.write(`${(error as Error).message}\n`);
241
+ exitCode = 2;
242
+ }
243
+ }
244
+ )
245
+ .demandCommand(1)
246
+ .strict()
247
+ .help();
248
+
249
+ await parser.parseAsync();
250
+ return exitCode;
251
+ }
252
+
253
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
254
+ void runCli(hideBin(process.argv)).then((code) => {
255
+ process.exitCode = code;
256
+ });
257
+ }
258
+
259
+ function resolveScanSettings(input: {
260
+ privacy: PrivacyMode;
261
+ online: boolean;
262
+ offline: boolean | undefined;
263
+ fallbackCalls: boolean | undefined;
264
+ redactPaths: boolean | undefined;
265
+ evidence: EvidenceMode | undefined;
266
+ telemetry: 'off' | 'on' | undefined;
267
+ }): {
268
+ offline: boolean;
269
+ enableNetworkFallbacks: boolean;
270
+ redactPaths: boolean;
271
+ evidenceMode: EvidenceMode;
272
+ } {
273
+ const preset =
274
+ input.privacy === 'strict'
275
+ ? {
276
+ offline: true,
277
+ enableNetworkFallbacks: false,
278
+ redactPaths: true,
279
+ evidenceMode: 'none' as EvidenceMode,
280
+ telemetry: 'off' as const
281
+ }
282
+ : {
283
+ offline: true,
284
+ enableNetworkFallbacks: true,
285
+ redactPaths: false,
286
+ evidenceMode: 'imports' as EvidenceMode,
287
+ telemetry: 'off' as const
288
+ };
289
+
290
+ let offline = preset.offline;
291
+ if (input.online) offline = false;
292
+ if (typeof input.offline === 'boolean') offline = input.offline;
293
+
294
+ if (input.privacy === 'strict' && !offline) {
295
+ throw new Error('privacy strict disallows online scanning. Remove --online or use --privacy standard.');
296
+ }
297
+ if ((input.telemetry ?? preset.telemetry) === 'on' && input.privacy === 'strict') {
298
+ throw new Error('privacy strict disallows telemetry.');
299
+ }
300
+
301
+ return {
302
+ offline,
303
+ enableNetworkFallbacks: input.fallbackCalls ?? preset.enableNetworkFallbacks,
304
+ redactPaths: input.redactPaths ?? preset.redactPaths,
305
+ evidenceMode: input.evidence ?? preset.evidenceMode
306
+ };
307
+ }
308
+
309
+ function buildCliSummary(
310
+ report: Awaited<ReturnType<typeof core.runScan>>,
311
+ failOn: string,
312
+ thresholdHit: boolean,
313
+ color: boolean
314
+ ): string {
315
+ const sev = report.summary.bySeverity;
316
+ const conf = report.summary.byConfidence;
317
+ const lines = [
318
+ '',
319
+ colorize('bardscan summary', 'cyan', color),
320
+ `target: ${report.targetPath}`,
321
+ `dependencies: ${report.summary.dependencyCount}`,
322
+ `findings: ${report.summary.findingsCount}`,
323
+ `severity: critical=${colorize(String(sev.critical), 'magenta', color)} high=${colorize(String(sev.high), 'red', color)} medium=${colorize(String(sev.medium), 'yellow', color)} low=${colorize(String(sev.low), 'green', color)} unknown=${colorize(String(sev.unknown), 'gray', color)}`,
324
+ `confidence: high=${colorize(String(conf.high), 'green', color)} medium=${colorize(String(conf.medium), 'yellow', color)} low=${colorize(String(conf.low), 'red', color)} unknown=${colorize(String(conf.unknown), 'gray', color)}`,
325
+ `fail-on: ${failOn}`,
326
+ `threshold hit: ${thresholdHit ? colorize('yes', 'red', color) : colorize('no', 'green', color)}`
327
+ ];
328
+ return `${lines.join('\n')}\n`;
329
+ }
330
+
331
+ function buildDbUpdateSummary(update: Awaited<ReturnType<typeof core.updateAdvisoryDb>>, color: boolean): string {
332
+ const lines = [
333
+ '',
334
+ colorize('bardscan db update', 'cyan', color),
335
+ `target: ${update.projectPath}`,
336
+ `dependencies: ${update.dependencyCount}`,
337
+ `queried: ${update.queriedCount}`,
338
+ `sources: osv=${colorize(String(update.bySource.osv), 'green', color)} cache=${colorize(String(update.bySource.cache), 'yellow', color)} unknown=${colorize(String(update.bySource.unknown), 'red', color)}`
339
+ ];
340
+ return `${lines.join('\n')}\n`;
341
+ }
342
+
343
+ function buildFindingsList(
344
+ report: Awaited<ReturnType<typeof core.runScan>>,
345
+ mode: ListFindingsMode,
346
+ color: boolean
347
+ ): string {
348
+ if (mode === 'none') return '';
349
+
350
+ const filtered = filterFindings(report, mode);
351
+
352
+ if (filtered.length === 0) {
353
+ return `\n${colorize('finding details', 'cyan', color)}\n(no matching findings)\n`;
354
+ }
355
+
356
+ const lines = ['', colorize('finding details', 'cyan', color)];
357
+ for (const finding of filtered) {
358
+ const vulnIds = finding.vulnerabilities.slice(0, 3).map((v) => v.id).join(', ');
359
+ const evidenceCount = finding.evidence.length;
360
+ const unknownReason = finding.unknownReason ? ` unknown-reason=${finding.unknownReason}` : '';
361
+ lines.push(
362
+ `- ${colorize(finding.severity, severityColor(finding.severity), color)} ${finding.packageName}@${finding.version}` +
363
+ ` confidence=${finding.confidence} direct=${finding.direct ? 'yes' : 'no'} evidence=${evidenceCount}` +
364
+ ` source=${finding.source}${unknownReason} ids=${vulnIds || 'n/a'}`
365
+ );
366
+ }
367
+ return `${lines.join('\n')}\n`;
368
+ }
369
+
370
+ function filterFindings(
371
+ report: Awaited<ReturnType<typeof core.runScan>>,
372
+ mode: ListFindingsMode
373
+ ): Awaited<ReturnType<typeof core.runScan>>['findings'] {
374
+ if (mode === 'all') return report.findings;
375
+ if (mode === 'critical-high') {
376
+ return report.findings.filter((f) => f.severity === 'critical' || f.severity === 'high');
377
+ }
378
+ if (mode === 'medium-up') {
379
+ return report.findings.filter((f) => f.severity === 'critical' || f.severity === 'high' || f.severity === 'medium');
380
+ }
381
+ return [];
382
+ }
383
+
384
+ function severityColor(severity: Severity): 'magenta' | 'red' | 'yellow' | 'green' | 'gray' {
385
+ if (severity === 'critical') return 'magenta';
386
+ if (severity === 'high') return 'red';
387
+ if (severity === 'medium') return 'yellow';
388
+ if (severity === 'low') return 'green';
389
+ return 'gray';
390
+ }
391
+
392
+ function useColor(stdout: { isTTY?: boolean }): boolean {
393
+ return Boolean(stdout.isTTY && !process.env.NO_COLOR);
394
+ }
395
+
396
+ function colorize(text: string, color: 'red' | 'yellow' | 'green' | 'cyan' | 'magenta' | 'gray', enabled: boolean): string {
397
+ if (!enabled) return text;
398
+ const code: Record<typeof color, number> = {
399
+ red: 31,
400
+ yellow: 33,
401
+ green: 32,
402
+ cyan: 36,
403
+ magenta: 35,
404
+ gray: 90
405
+ };
406
+ return `\u001b[${code[color]}m${text}\u001b[0m`;
407
+ }
@@ -0,0 +1,381 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+ import { CliDeps, runCli } from '../src/index.js';
7
+
8
+ function makeReport(severity: 'critical' | 'high' | 'medium' | 'low' | 'unknown') {
9
+ return {
10
+ targetPath: '/tmp/project',
11
+ generatedAt: '2026-02-19T00:00:00.000Z',
12
+ failOn: 'high',
13
+ summary: {
14
+ dependencyCount: 1,
15
+ scannedFiles: 1,
16
+ findingsCount: 1,
17
+ bySeverity: {
18
+ critical: severity === 'critical' ? 1 : 0,
19
+ high: severity === 'high' ? 1 : 0,
20
+ medium: severity === 'medium' ? 1 : 0,
21
+ low: severity === 'low' ? 1 : 0,
22
+ unknown: severity === 'unknown' ? 1 : 0
23
+ },
24
+ byConfidence: { high: 1, medium: 0, low: 0, unknown: 0 }
25
+ },
26
+ findings: [
27
+ {
28
+ packageName: 'lodash',
29
+ version: '4.17.21',
30
+ direct: true,
31
+ severity,
32
+ severitySource: 'osv_label',
33
+ confidence: 'high',
34
+ evidence: ['src/index.ts'],
35
+ vulnerabilities: [{ id: 'OSV-1', severity, severitySource: 'osv_label' }],
36
+ source: 'osv'
37
+ }
38
+ ]
39
+ };
40
+ }
41
+
42
+ test('runCli writes only JSON report and returns exit code 1 on threshold match', async () => {
43
+ const outDir = await mkdtemp(path.join(os.tmpdir(), 'bardscan-cli-json-'));
44
+ const writes: string[] = [];
45
+ const stdout: string[] = [];
46
+ const stderr: string[] = [];
47
+ const deps: CliDeps = {
48
+ mkdir: async () => undefined,
49
+ writeFile: async (filePath) => {
50
+ writes.push(String(filePath));
51
+ },
52
+ runScan: async () => makeReport('high'),
53
+ buildMarkdownReport: () => '# report',
54
+ buildSarifReport: () => ({ version: '2.1.0', runs: [] }),
55
+ shouldFail: (failOn, findingSeverity) => failOn === findingSeverity,
56
+ stdout: { write: (text: string) => stdout.push(text) },
57
+ stderr: { write: (text: string) => stderr.push(text) }
58
+ };
59
+
60
+ const code = await runCli(['scan', '.', '--format', 'json', '--out-dir', outDir, '--fail-on', 'high'], deps);
61
+
62
+ assert.equal(code, 1);
63
+ assert.equal(stderr.length, 0);
64
+ assert.equal(writes.length, 1);
65
+ assert.equal(writes[0], path.resolve(outDir, 'report.json'));
66
+ assert.match(stdout.join(''), /report\.json/);
67
+ assert.doesNotMatch(stdout.join(''), /report\.md/);
68
+ assert.match(stdout.join(''), /bardscan summary/);
69
+ assert.match(stdout.join(''), /threshold hit: yes/);
70
+ });
71
+
72
+ test('runCli writes only Markdown report and returns exit code 0 for fail-on none', async () => {
73
+ const outDir = await mkdtemp(path.join(os.tmpdir(), 'bardscan-cli-md-'));
74
+ const writes: string[] = [];
75
+ const stdout: string[] = [];
76
+ const deps: CliDeps = {
77
+ mkdir: async () => undefined,
78
+ writeFile: async (filePath) => {
79
+ writes.push(String(filePath));
80
+ },
81
+ runScan: async () => makeReport('critical'),
82
+ buildMarkdownReport: () => '# markdown-report',
83
+ buildSarifReport: () => ({ version: '2.1.0', runs: [] }),
84
+ shouldFail: () => true,
85
+ stdout: { write: (text: string) => stdout.push(text) },
86
+ stderr: { write: () => undefined }
87
+ };
88
+
89
+ const code = await runCli(['scan', '.', '--format', 'md', '--out-dir', outDir, '--fail-on', 'none'], deps);
90
+
91
+ assert.equal(code, 0);
92
+ assert.equal(writes.length, 1);
93
+ assert.equal(writes[0], path.resolve(outDir, 'report.md'));
94
+ assert.match(stdout.join(''), /report\.md/);
95
+ assert.doesNotMatch(stdout.join(''), /report\.json/);
96
+ assert.match(stdout.join(''), /bardscan summary/);
97
+ assert.match(stdout.join(''), /threshold hit: no/);
98
+ });
99
+
100
+ test('runCli returns exit code 2 and writes to stderr on tool errors', async () => {
101
+ const outDir = await mkdtemp(path.join(os.tmpdir(), 'bardscan-cli-err-'));
102
+ const stderr: string[] = [];
103
+ const deps: CliDeps = {
104
+ mkdir: async () => undefined,
105
+ writeFile: async () => undefined,
106
+ runScan: async () => {
107
+ throw new Error('boom');
108
+ },
109
+ buildMarkdownReport: () => '# report',
110
+ buildSarifReport: () => ({ version: '2.1.0', runs: [] }),
111
+ shouldFail: () => false,
112
+ stdout: { write: () => undefined },
113
+ stderr: { write: (text: string) => stderr.push(text) }
114
+ };
115
+
116
+ const code = await runCli(['scan', '.', '--format', 'both', '--out-dir', outDir], deps);
117
+
118
+ assert.equal(code, 2);
119
+ assert.match(stderr.join(''), /boom/);
120
+ });
121
+
122
+ test('runCli writes SARIF report when format is sarif', async () => {
123
+ const outDir = await mkdtemp(path.join(os.tmpdir(), 'bardscan-cli-sarif-'));
124
+ const writes: string[] = [];
125
+ const deps: CliDeps = {
126
+ mkdir: async () => undefined,
127
+ writeFile: async (filePath) => {
128
+ writes.push(String(filePath));
129
+ },
130
+ runScan: async () => makeReport('medium'),
131
+ buildMarkdownReport: () => '# report',
132
+ buildSarifReport: () => ({ version: '2.1.0', runs: [{ tool: { driver: { name: 'bardscan' } }, results: [] }] }),
133
+ shouldFail: () => false,
134
+ stdout: { write: () => undefined },
135
+ stderr: { write: () => undefined }
136
+ };
137
+
138
+ const code = await runCli(['scan', '.', '--format', 'sarif', '--out-dir', outDir, '--fail-on', 'none'], deps);
139
+ assert.equal(code, 0);
140
+ assert.equal(writes.length, 1);
141
+ assert.equal(writes[0], path.resolve(outDir, 'report.sarif'));
142
+ });
143
+
144
+ test('runCli colorizes summary output when stdout is a TTY', async () => {
145
+ const outDir = await mkdtemp(path.join(os.tmpdir(), 'bardscan-cli-color-'));
146
+ const stdout: string[] = [];
147
+ const previousNoColor = process.env.NO_COLOR;
148
+ delete process.env.NO_COLOR;
149
+ const deps: CliDeps = {
150
+ mkdir: async () => undefined,
151
+ writeFile: async () => undefined,
152
+ runScan: async () => makeReport('high'),
153
+ buildMarkdownReport: () => '# report',
154
+ buildSarifReport: () => ({ version: '2.1.0', runs: [] }),
155
+ shouldFail: (failOn, findingSeverity) => failOn === findingSeverity,
156
+ stdout: { write: (text: string) => stdout.push(text), isTTY: true },
157
+ stderr: { write: () => undefined }
158
+ };
159
+
160
+ try {
161
+ const code = await runCli(['scan', '.', '--format', 'json', '--out-dir', outDir, '--fail-on', 'high'], deps);
162
+ assert.equal(code, 1);
163
+ const output = stdout.join('');
164
+ assert.equal(output.includes('\u001b[36mbardscan summary\u001b[0m'), true);
165
+ assert.equal(output.includes('\u001b[31myes\u001b[0m'), true);
166
+ } finally {
167
+ if (previousNoColor === undefined) {
168
+ delete process.env.NO_COLOR;
169
+ } else {
170
+ process.env.NO_COLOR = previousNoColor;
171
+ }
172
+ }
173
+ });
174
+
175
+ test('runCli lists critical/high findings when list mode is critical-high', async () => {
176
+ const outDir = await mkdtemp(path.join(os.tmpdir(), 'bardscan-cli-list-hi-'));
177
+ const stdout: string[] = [];
178
+ const deps: CliDeps = {
179
+ mkdir: async () => undefined,
180
+ writeFile: async () => undefined,
181
+ runScan: async () => ({
182
+ targetPath: '/tmp/project',
183
+ generatedAt: '2026-02-19T00:00:00.000Z',
184
+ failOn: 'none',
185
+ summary: {
186
+ dependencyCount: 2,
187
+ scannedFiles: 1,
188
+ findingsCount: 2,
189
+ bySeverity: { critical: 0, high: 1, medium: 0, low: 1, unknown: 0 },
190
+ byConfidence: { high: 1, medium: 1, low: 0, unknown: 0 }
191
+ },
192
+ findings: [
193
+ {
194
+ packageName: 'high-pkg',
195
+ version: '1.0.0',
196
+ direct: true,
197
+ severity: 'high',
198
+ severitySource: 'osv_label',
199
+ confidence: 'high',
200
+ evidence: ['src/a.ts'],
201
+ vulnerabilities: [{ id: 'OSV-HIGH', severity: 'high', severitySource: 'osv_label' }],
202
+ source: 'osv'
203
+ },
204
+ {
205
+ packageName: 'low-pkg',
206
+ version: '1.0.0',
207
+ direct: false,
208
+ severity: 'low',
209
+ severitySource: 'osv_label',
210
+ confidence: 'low',
211
+ evidence: ['src/b.ts'],
212
+ vulnerabilities: [{ id: 'OSV-LOW', severity: 'low', severitySource: 'osv_label' }],
213
+ source: 'osv'
214
+ }
215
+ ]
216
+ }),
217
+ buildMarkdownReport: () => '# report',
218
+ buildSarifReport: () => ({ version: '2.1.0', runs: [] }),
219
+ shouldFail: () => false,
220
+ stdout: { write: (text: string) => stdout.push(text) },
221
+ stderr: { write: () => undefined }
222
+ };
223
+
224
+ const code = await runCli(
225
+ ['scan', '.', '--format', 'json', '--out-dir', outDir, '--fail-on', 'none', '--list-findings', 'critical-high'],
226
+ deps
227
+ );
228
+ assert.equal(code, 0);
229
+ const output = stdout.join('');
230
+ assert.match(output, /finding details/);
231
+ assert.match(output, /high-pkg@1.0.0/);
232
+ assert.doesNotMatch(output, /low-pkg@1.0.0/);
233
+ });
234
+
235
+ test('runCli lists medium/high/critical findings when list mode is medium-up', async () => {
236
+ const outDir = await mkdtemp(path.join(os.tmpdir(), 'bardscan-cli-list-med-'));
237
+ const stdout: string[] = [];
238
+ const deps: CliDeps = {
239
+ mkdir: async () => undefined,
240
+ writeFile: async () => undefined,
241
+ runScan: async () => ({
242
+ targetPath: '/tmp/project',
243
+ generatedAt: '2026-02-19T00:00:00.000Z',
244
+ failOn: 'none',
245
+ summary: {
246
+ dependencyCount: 3,
247
+ scannedFiles: 1,
248
+ findingsCount: 3,
249
+ bySeverity: { critical: 0, high: 1, medium: 1, low: 1, unknown: 0 },
250
+ byConfidence: { high: 1, medium: 2, low: 0, unknown: 0 }
251
+ },
252
+ findings: [
253
+ {
254
+ packageName: 'high-pkg',
255
+ version: '1.0.0',
256
+ direct: true,
257
+ severity: 'high',
258
+ severitySource: 'osv_label',
259
+ confidence: 'high',
260
+ evidence: ['src/a.ts'],
261
+ vulnerabilities: [{ id: 'OSV-HIGH', severity: 'high', severitySource: 'osv_label' }],
262
+ source: 'osv'
263
+ },
264
+ {
265
+ packageName: 'medium-pkg',
266
+ version: '1.0.0',
267
+ direct: true,
268
+ severity: 'medium',
269
+ severitySource: 'osv_label',
270
+ confidence: 'medium',
271
+ evidence: [],
272
+ vulnerabilities: [{ id: 'OSV-MED', severity: 'medium', severitySource: 'osv_label' }],
273
+ source: 'osv'
274
+ },
275
+ {
276
+ packageName: 'low-pkg',
277
+ version: '1.0.0',
278
+ direct: false,
279
+ severity: 'low',
280
+ severitySource: 'osv_label',
281
+ confidence: 'low',
282
+ evidence: ['src/b.ts'],
283
+ vulnerabilities: [{ id: 'OSV-LOW', severity: 'low', severitySource: 'osv_label' }],
284
+ source: 'osv'
285
+ }
286
+ ]
287
+ }),
288
+ buildMarkdownReport: () => '# report',
289
+ buildSarifReport: () => ({ version: '2.1.0', runs: [] }),
290
+ shouldFail: () => false,
291
+ stdout: { write: (text: string) => stdout.push(text) },
292
+ stderr: { write: () => undefined }
293
+ };
294
+
295
+ const code = await runCli(
296
+ ['scan', '.', '--format', 'json', '--out-dir', outDir, '--fail-on', 'none', '--list-findings', 'medium-up'],
297
+ deps
298
+ );
299
+ assert.equal(code, 0);
300
+ const output = stdout.join('');
301
+ assert.match(output, /high-pkg@1.0.0/);
302
+ assert.match(output, /medium-pkg@1.0.0/);
303
+ assert.doesNotMatch(output, /low-pkg@1.0.0/);
304
+ });
305
+
306
+ test('runCli writes filtered findings JSON when findings-json is set', async () => {
307
+ const outDir = await mkdtemp(path.join(os.tmpdir(), 'bardscan-cli-findings-json-'));
308
+ const writes: Array<{ filePath: string; content: string }> = [];
309
+ const deps: CliDeps = {
310
+ mkdir: async () => undefined,
311
+ writeFile: async (filePath, content) => {
312
+ writes.push({ filePath: String(filePath), content: String(content) });
313
+ },
314
+ runScan: async () => ({
315
+ targetPath: '/tmp/project',
316
+ generatedAt: '2026-02-19T00:00:00.000Z',
317
+ failOn: 'none',
318
+ summary: {
319
+ dependencyCount: 2,
320
+ scannedFiles: 1,
321
+ findingsCount: 2,
322
+ bySeverity: { critical: 0, high: 1, medium: 0, low: 1, unknown: 0 },
323
+ byConfidence: { high: 1, medium: 1, low: 0, unknown: 0 }
324
+ },
325
+ findings: [
326
+ {
327
+ packageName: 'high-pkg',
328
+ version: '1.0.0',
329
+ direct: true,
330
+ severity: 'high',
331
+ severitySource: 'osv_label',
332
+ confidence: 'high',
333
+ evidence: ['src/a.ts'],
334
+ vulnerabilities: [{ id: 'OSV-HIGH', severity: 'high', severitySource: 'osv_label' }],
335
+ source: 'osv'
336
+ },
337
+ {
338
+ packageName: 'low-pkg',
339
+ version: '1.0.0',
340
+ direct: false,
341
+ severity: 'low',
342
+ severitySource: 'osv_label',
343
+ confidence: 'low',
344
+ evidence: ['src/b.ts'],
345
+ vulnerabilities: [{ id: 'OSV-LOW', severity: 'low', severitySource: 'osv_label' }],
346
+ source: 'osv'
347
+ }
348
+ ]
349
+ }),
350
+ buildMarkdownReport: () => '# report',
351
+ buildSarifReport: () => ({ version: '2.1.0', runs: [] }),
352
+ shouldFail: () => false,
353
+ stdout: { write: () => undefined },
354
+ stderr: { write: () => undefined }
355
+ };
356
+
357
+ const findingsJsonPath = path.join(outDir, 'filtered-findings.json');
358
+ const code = await runCli(
359
+ [
360
+ 'scan',
361
+ '.',
362
+ '--format',
363
+ 'json',
364
+ '--out-dir',
365
+ outDir,
366
+ '--fail-on',
367
+ 'none',
368
+ '--list-findings',
369
+ 'critical-high',
370
+ '--findings-json',
371
+ findingsJsonPath
372
+ ],
373
+ deps
374
+ );
375
+ assert.equal(code, 0);
376
+ const findingsWrite = writes.find((w) => w.filePath === path.resolve(findingsJsonPath));
377
+ assert.ok(findingsWrite);
378
+ const parsed = JSON.parse(findingsWrite.content) as Array<{ packageName: string }>;
379
+ assert.equal(parsed.length, 1);
380
+ assert.equal(parsed[0]?.packageName, 'high-pkg');
381
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }