From 0f7ef0b62ae5634c7da5961a59df69868dc97276 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 21:07:31 +0000 Subject: [PATCH] fix(cli): reject unknown --rule values instead of silently passing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An unknown or typo'd --rule ID was cast straight to RuleId and used to filter the rule set. A value matching no real rule filtered the set down to nothing, producing zero findings, a passing report, and exit code 0 — a false "PASSED" security gate from a single typo. Validate each --rule value against the RuleId enum and exit 2 with a clear error (mirroring --format and --fail-on). Also list valid rule IDs in --help for discoverability, and add a regression test. Fixes #11 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_017ffK3ewW2pyTMsmmNSQK2R --- src/__tests__/args.test.ts | 8 ++++++++ src/args.ts | 16 +++++++++++++++- src/cli.ts | 5 ++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/__tests__/args.test.ts b/src/__tests__/args.test.ts index 711df3a..0d5104f 100644 --- a/src/__tests__/args.test.ts +++ b/src/__tests__/args.test.ts @@ -78,6 +78,14 @@ describe('parseArgs()', () => { }); }); + // Regression for #11: a typo'd rule ID must fail loudly, not silently filter + // the rule set to nothing and report a passing (false-negative) scan. + it('rejects an unknown --rule value with exit code 2', () => { + const result = parseArgs(['--rule=NOAUTH', './mcp.json']); + expect(result).toMatchObject({ kind: 'error', exitCode: 2 }); + expect((result as { message: string }).message).toContain('NOAUTH'); + }); + it('detects URL targets and routes them to serverUrl', () => { for (const url of [ 'http://x.example.com', diff --git a/src/args.ts b/src/args.ts index 7388c92..e2ce54b 100644 --- a/src/args.ts +++ b/src/args.ts @@ -36,6 +36,10 @@ const SEVERITY_BY_NAME: Record = { info: Severity.INFO, }; +/** Valid rule IDs, used to reject typo'd `--rule` values rather than silently + * filtering the rule set down to nothing (a false "PASSED" security gate). */ +const VALID_RULE_IDS = new Set(Object.values(RuleId)); + /** * Parse raw CLI arguments into a structured, side-effect-free result. */ @@ -80,7 +84,17 @@ export function parseArgs(args: string[]): CliParseResult { }; } } else if (arg.startsWith('--rule=')) { - rules.push(arg.slice('--rule='.length) as RuleId); + const val = arg.slice('--rule='.length); + if (!VALID_RULE_IDS.has(val)) { + return { + kind: 'error', + message: + `Error: Unknown rule "${val}". Valid rule IDs: ` + + `${Object.values(RuleId).join(', ')}.`, + exitCode: 2, + }; + } + rules.push(val as RuleId); } else if (!arg.startsWith('--')) { target = arg; } else { diff --git a/src/cli.ts b/src/cli.ts index 9f0033f..43cbe69 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,7 +16,7 @@ import { scan } from './scanner.js'; import { toSarif } from './sarif.js'; -import { Severity, Finding } from './types.js'; +import { Severity, Finding, RuleId } from './types.js'; import { parseArgs } from './args.js'; const SEVERITY_ORDER: Record = { @@ -39,6 +39,9 @@ function printHelp(): void { console.log(' --rule=RULE_ID Run only the specified rule (repeatable)'); console.log(' --help, -h Show this help message'); console.log(''); + console.log('Valid rule IDs:'); + console.log(` ${Object.values(RuleId).join(', ')}`); + console.log(''); console.log('Examples:'); console.log(' mcp-security-scanner ./mcp-server.json'); console.log(' mcp-security-scanner --format=table ./mcp-server.json');