diff --git a/src/cli/args.ts b/src/cli/args.ts index a11cc962..b5ae4da0 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -72,6 +72,13 @@ function parseFailOn(value: string | undefined): string { return value; } +function requireFlagValue(flag: string, value: string | undefined, noun: string): string { + if (value === undefined || value === "" || value.startsWith("-")) { + throw new Error(`${flag} requires a ${noun} argument`); + } + return value; +} + function parseSbomFormat(value: string | undefined): SbomFormat { const valid = Object.keys(SBOM_FORMAT_ALIASES).join(", "); if (value === undefined || value.startsWith("-")) { @@ -243,19 +250,19 @@ export function parseArgs(argv: string[]): { continue; } if (arg === FLAG_AUDIT_LOG) { - options.auditLog = argv[++i]; + options.auditLog = requireFlagValue(FLAG_AUDIT_LOG, argv[++i], "path"); continue; } if (arg.startsWith(`${FLAG_AUDIT_LOG}=`)) { - options.auditLog = arg.slice(FLAG_AUDIT_LOG.length + 1); + options.auditLog = requireFlagValue(FLAG_AUDIT_LOG, arg.slice(FLAG_AUDIT_LOG.length + 1), "path"); continue; } if (arg === FLAG_RULE) { - options.rule = argv[++i]; + options.rule = requireFlagValue(FLAG_RULE, argv[++i], "rule id"); continue; } if (arg.startsWith(`${FLAG_RULE}=`)) { - options.rule = arg.slice(FLAG_RULE.length + 1); + options.rule = requireFlagValue(FLAG_RULE, arg.slice(FLAG_RULE.length + 1), "rule id"); continue; } if (arg === FLAG_FAIL_ON) { @@ -338,11 +345,11 @@ export function parseArgs(argv: string[]): { continue; } if (arg === FLAG_OFFLINE_DB) { - options.offlineDb = argv[++i]; + options.offlineDb = requireFlagValue(FLAG_OFFLINE_DB, argv[++i], "path"); continue; } if (arg.startsWith(`${FLAG_OFFLINE_DB}=`)) { - options.offlineDb = arg.slice(FLAG_OFFLINE_DB.length + 1); + options.offlineDb = requireFlagValue(FLAG_OFFLINE_DB, arg.slice(FLAG_OFFLINE_DB.length + 1), "path"); continue; } if (arg === FLAG_ALL) { @@ -358,27 +365,27 @@ export function parseArgs(argv: string[]): { continue; } if (arg === FLAG_BATCH_SIZE) { - options.batchSize = argv[++i] ?? options.batchSize; + options.batchSize = requireFlagValue(FLAG_BATCH_SIZE, argv[++i], "number"); continue; } if (arg.startsWith(`${FLAG_BATCH_SIZE}=`)) { - options.batchSize = arg.slice(FLAG_BATCH_SIZE.length + 1); + options.batchSize = requireFlagValue(FLAG_BATCH_SIZE, arg.slice(FLAG_BATCH_SIZE.length + 1), "number"); continue; } if (arg === FLAG_CACHE_DIR) { - options.cacheDir = argv[++i]; + options.cacheDir = requireFlagValue(FLAG_CACHE_DIR, argv[++i], "path"); continue; } if (arg.startsWith(`${FLAG_CACHE_DIR}=`)) { - options.cacheDir = arg.slice(FLAG_CACHE_DIR.length + 1); + options.cacheDir = requireFlagValue(FLAG_CACHE_DIR, arg.slice(FLAG_CACHE_DIR.length + 1), "path"); continue; } if (arg === FLAG_OSV_URL) { - options.osvUrl = argv[++i]; + options.osvUrl = requireFlagValue(FLAG_OSV_URL, argv[++i], "url"); continue; } if (arg.startsWith(`${FLAG_OSV_URL}=`)) { - options.osvUrl = arg.slice(FLAG_OSV_URL.length + 1); + options.osvUrl = requireFlagValue(FLAG_OSV_URL, arg.slice(FLAG_OSV_URL.length + 1), "url"); continue; } if (arg === FLAG_ALLOW_PRIVATE_OSV_URL) { @@ -402,19 +409,19 @@ export function parseArgs(argv: string[]): { continue; } if (arg === FLAG_SEARCH_DEPTH) { - options.searchDepth = argv[++i] ?? options.searchDepth; + options.searchDepth = requireFlagValue(FLAG_SEARCH_DEPTH, argv[++i], "number"); continue; } if (arg.startsWith(`${FLAG_SEARCH_DEPTH}=`)) { - options.searchDepth = arg.slice(FLAG_SEARCH_DEPTH.length + 1); + options.searchDepth = requireFlagValue(FLAG_SEARCH_DEPTH, arg.slice(FLAG_SEARCH_DEPTH.length + 1), "number"); continue; } if (arg === FLAG_MIN_SEVERITY) { - options.minSeverity = argv[++i] ?? options.minSeverity; + options.minSeverity = requireFlagValue(FLAG_MIN_SEVERITY, argv[++i], "level"); continue; } if (arg.startsWith(`${FLAG_MIN_SEVERITY}=`)) { - options.minSeverity = arg.slice(FLAG_MIN_SEVERITY.length + 1); + options.minSeverity = requireFlagValue(FLAG_MIN_SEVERITY, arg.slice(FLAG_MIN_SEVERITY.length + 1), "level"); continue; } if (arg === FLAG_USAGE || arg === FLAG_USAGE_HINTS) { @@ -449,15 +456,11 @@ export function parseArgs(argv: string[]): { continue; } if (arg === FLAG_CA_CERT) { - const val = argv[++i]; - if (!val) { - throw new Error("--ca-cert requires a path argument"); - } - options.caCert = val; + options.caCert = requireFlagValue(FLAG_CA_CERT, argv[++i], "path"); continue; } if (arg.startsWith(`${FLAG_CA_CERT}=`)) { - options.caCert = arg.slice(FLAG_CA_CERT.length + 1); + options.caCert = requireFlagValue(FLAG_CA_CERT, arg.slice(FLAG_CA_CERT.length + 1), "path"); continue; } if (arg === FLAG_SARIF) { @@ -483,11 +486,11 @@ export function parseArgs(argv: string[]): { continue; } if (arg === FLAG_AUDIT_LOG) { - options.auditLog = argv[++i]; + options.auditLog = requireFlagValue(FLAG_AUDIT_LOG, argv[++i], "path"); continue; } if (arg.startsWith(`${FLAG_AUDIT_LOG}=`)) { - options.auditLog = arg.slice(FLAG_AUDIT_LOG.length + 1); + options.auditLog = requireFlagValue(FLAG_AUDIT_LOG, arg.slice(FLAG_AUDIT_LOG.length + 1), "path"); continue; } if (arg === FLAG_CHECK_OVERRIDES) { diff --git a/tests/cli/scan-value-threshold-args.test.ts b/tests/cli/scan-value-threshold-args.test.ts new file mode 100644 index 00000000..7adbc76e --- /dev/null +++ b/tests/cli/scan-value-threshold-args.test.ts @@ -0,0 +1,38 @@ +import { parseArgs } from "../../src/cli/args.js"; + +const SCAN_THRESHOLD_FLAGS: Array<{ + flag: string; + noun: string; + option: "minSeverity" | "searchDepth" | "batchSize"; + value: string; +}> = [ + { flag: "--min-severity", noun: "level", option: "minSeverity", value: "high" }, + { flag: "--search-depth", noun: "number", option: "searchDepth", value: "6" }, + { flag: "--batch-size", noun: "number", option: "batchSize", value: "50" }, +]; + +describe("parseArgs - threshold and size flags (scan)", () => { + it.each(SCAN_THRESHOLD_FLAGS)( + "rejects $flag when the next token is another flag", + ({ flag, noun }) => { + expect(() => parseArgs([".", flag, "--all"])).toThrow(`${flag} requires a ${noun} argument`); + }, + ); + + it.each(SCAN_THRESHOLD_FLAGS)("rejects $flag when it is missing at the end", ({ flag, noun }) => { + expect(() => parseArgs([".", flag])).toThrow(`${flag} requires a ${noun} argument`); + }); + + it.each(SCAN_THRESHOLD_FLAGS)("rejects $flag with an empty = value", ({ flag, noun }) => { + expect(() => parseArgs([".", `${flag}=`])).toThrow(`${flag} requires a ${noun} argument`); + }); + + it.each(SCAN_THRESHOLD_FLAGS)("rejects $flag when the = value is another flag", ({ flag, noun }) => { + expect(() => parseArgs([".", `${flag}=--all`])).toThrow(`${flag} requires a ${noun} argument`); + }); + + it.each(SCAN_THRESHOLD_FLAGS)("parses $flag with a value in both forms", ({ flag, option, value }) => { + expect(parseArgs([".", flag, value]).options[option]).toBe(value); + expect(parseArgs([".", `${flag}=${value}`]).options[option]).toBe(value); + }); +}); diff --git a/tests/cli/value-flag-args.test.ts b/tests/cli/value-flag-args.test.ts new file mode 100644 index 00000000..fbc4a1d3 --- /dev/null +++ b/tests/cli/value-flag-args.test.ts @@ -0,0 +1,75 @@ +import { parseArgs } from "../../src/cli/args.js"; + +const SCAN_VALUE_FLAGS: Array<{ + flag: string; + noun: string; + option: "offlineDb" | "cacheDir" | "osvUrl" | "auditLog" | "caCert"; + value: string; +}> = [ + { flag: "--offline-db", noun: "path", option: "offlineDb", value: "/tmp/advisories.db" }, + { flag: "--cache-dir", noun: "path", option: "cacheDir", value: "/tmp/cache" }, + { flag: "--osv-url", noun: "url", option: "osvUrl", value: "https://example.com/osv" }, + { flag: "--audit-log", noun: "path", option: "auditLog", value: "/tmp/audit.ndjson" }, + { flag: "--ca-cert", noun: "path", option: "caCert", value: "/tmp/ca.pem" }, +]; + +const OVERRIDES_VALUE_FLAGS: Array<{ + flag: string; + noun: string; + option: "auditLog" | "rule"; + value: string; +}> = [ + { flag: "--audit-log", noun: "path", option: "auditLog", value: "/tmp/audit.ndjson" }, + { flag: "--rule", noun: "rule id", option: "rule", value: "OA001" }, +]; + +describe("parseArgs - required value flags (scan)", () => { + it.each(SCAN_VALUE_FLAGS)( + "rejects $flag when the next token is another flag", + ({ flag, noun }) => { + expect(() => parseArgs([".", flag, "--json"])).toThrow(`${flag} requires a ${noun} argument`); + }, + ); + + it.each(SCAN_VALUE_FLAGS)("rejects $flag when it is missing at the end", ({ flag, noun }) => { + expect(() => parseArgs([".", flag])).toThrow(`${flag} requires a ${noun} argument`); + }); + + it.each(SCAN_VALUE_FLAGS)("rejects $flag with an empty = value", ({ flag, noun }) => { + expect(() => parseArgs([".", `${flag}=`])).toThrow(`${flag} requires a ${noun} argument`); + }); + + it.each(SCAN_VALUE_FLAGS)("parses $flag with a value in both forms", ({ flag, option, value }) => { + expect(parseArgs([".", flag, value]).options[option]).toBe(value); + expect(parseArgs([".", `${flag}=${value}`]).options[option]).toBe(value); + }); +}); + +describe("parseArgs - required value flags (overrides)", () => { + it.each(OVERRIDES_VALUE_FLAGS)( + "rejects $flag when the next token is another flag", + ({ flag, noun }) => { + expect(() => parseArgs(["overrides", ".", flag, "--json"])).toThrow( + `${flag} requires a ${noun} argument`, + ); + }, + ); + + it.each(OVERRIDES_VALUE_FLAGS)("rejects $flag when it is missing at the end", ({ flag, noun }) => { + expect(() => parseArgs(["overrides", ".", flag])).toThrow(`${flag} requires a ${noun} argument`); + }); + + it.each(OVERRIDES_VALUE_FLAGS)("rejects $flag with an empty = value", ({ flag, noun }) => { + expect(() => parseArgs(["overrides", ".", `${flag}=`])).toThrow( + `${flag} requires a ${noun} argument`, + ); + }); + + it.each(OVERRIDES_VALUE_FLAGS)( + "parses $flag with a value in both forms", + ({ flag, option, value }) => { + expect(parseArgs(["overrides", ".", flag, value]).options[option]).toBe(value); + expect(parseArgs(["overrides", ".", `${flag}=${value}`]).options[option]).toBe(value); + }, + ); +});