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.
- package/dist/index.d.ts +21 -0
- package/dist/index.js +331 -0
- package/dist/index.js.map +1 -0
- package/package.json +21 -0
- package/src/index.ts +407 -0
- package/test/cli.test.ts +381 -0
- package/tsconfig.json +8 -0
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|
package/test/cli.test.ts
ADDED
|
@@ -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
|
+
});
|