diff --git a/src/audit.ts b/src/audit.ts index 46f8d70..c1587a5 100644 --- a/src/audit.ts +++ b/src/audit.ts @@ -41,9 +41,11 @@ export function runAudit( rules: Rule[] = ALL_RULES, ): AuditResult { const engine = new Engine(rules); + const useAllowlist = + config.enforceEnabledRules || config.enabledRules.length > 0; const raw = engine.run(target, { disabledRules: config.disabledRules, - enabledRules: config.enabledRules, + enabledRules: useAllowlist ? config.enabledRules : undefined, severityOverrides: config.severityOverrides, }); const findings = applyIgnores(raw.findings, config.ignore); diff --git a/src/cli.ts b/src/cli.ts index 09ea7f3..2d47ed7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -109,7 +109,7 @@ function csv(value: CliFlag | undefined): string[] { .filter(Boolean); } -function overlayFlags( +export function overlayFlags( base: McpAuditConfig, flags: Record, ): McpAuditConfig { @@ -124,7 +124,10 @@ function overlayFlags( overlay.failOn = fo; } if (flags["disable"]) overlay.disabledRules = csv(flags["disable"]); - if (flags["only"]) overlay.enabledRules = csv(flags["only"]); + if (flags["only"] !== undefined) { + overlay.enabledRules = csv(flags["only"]); + overlay.enforceEnabledRules = true; + } return normalizeConfig(overlay, base); } diff --git a/src/config.ts b/src/config.ts index 3afe5d2..0974a75 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,6 +10,8 @@ export interface McpAuditConfig { disabledRules: string[]; /** If non-empty, only these rule ids run. */ enabledRules: string[]; + /** When true, enabledRules is an allowlist (including empty → no rules). */ + enforceEnabledRules: boolean; /** Per-rule severity overrides. */ severityOverrides: Record; /** Findings at or above this severity cause a non-zero exit. */ @@ -21,6 +23,7 @@ export interface McpAuditConfig { export const DEFAULT_CONFIG: McpAuditConfig = { disabledRules: [], enabledRules: [], + enforceEnabledRules: false, severityOverrides: {}, failOn: "high", ignore: [], @@ -29,6 +32,7 @@ export const DEFAULT_CONFIG: McpAuditConfig = { const KNOWN_CONFIG_KEYS = new Set([ "disabledRules", "enabledRules", + "enforceEnabledRules", "severityOverrides", "failOn", "ignore", @@ -82,6 +86,7 @@ export function normalizeConfig( const config: McpAuditConfig = { disabledRules: [...base.disabledRules], enabledRules: [...base.enabledRules], + enforceEnabledRules: base.enforceEnabledRules, severityOverrides: { ...base.severityOverrides }, failOn: base.failOn, ignore: [...base.ignore], @@ -93,6 +98,9 @@ export function normalizeConfig( if (Array.isArray(obj.enabledRules)) { config.enabledRules = obj.enabledRules.map(String); } + if (obj.enforceEnabledRules === true) { + config.enforceEnabledRules = true; + } if (Array.isArray(obj.ignore)) { config.ignore = obj.ignore.map(String); } diff --git a/src/engine/engine.ts b/src/engine/engine.ts index 29047e5..f48ca04 100644 --- a/src/engine/engine.ts +++ b/src/engine/engine.ts @@ -41,7 +41,7 @@ export class Engine { activeRules(options: EngineOptions = {}): Rule[] { const { disabledRules = [], enabledRules } = options; return this.rules.filter((rule) => { - if (enabledRules && enabledRules.length > 0) { + if (enabledRules !== undefined) { return enabledRules.includes(rule.id); } return !disabledRules.includes(rule.id); diff --git a/test/cli.test.ts b/test/cli.test.ts index 90cad98..9d5e28a 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,5 +1,8 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { collectHeaders, main, parseArgs } from "../src/cli.js"; +import { collectHeaders, main, overlayFlags, parseArgs } from "../src/cli.js"; +import { DEFAULT_CONFIG } from "../src/config.js"; +import { runAudit } from "../src/audit.js"; +import { makeTarget } from "./helpers.js"; afterEach(() => { vi.restoreAllMocks(); @@ -65,7 +68,15 @@ describe("HTTP headers", () => { expect(collectHeaders(flags)).toEqual({ Authorization: "Bearer a:b" }); }); - it("rejects a header value without a colon", () => { + + it("treats --only \"\" as an empty allowlist (zero rules run)", () => { + const { flags } = parseArgs(["static", "manifest.json", "--only", ""]); + const config = overlayFlags(DEFAULT_CONFIG, flags); + const result = runAudit(makeTarget(), config); + expect(result.rulesRun).toEqual([]); +}); + +it("rejects a header value without a colon", () => { const { flags } = parseArgs([ "http", "https://example.com/mcp", diff --git a/test/engine.test.ts b/test/engine.test.ts index 1f69ee5..06d542d 100644 --- a/test/engine.test.ts +++ b/test/engine.test.ts @@ -40,6 +40,13 @@ describe("Engine", () => { expect(result.findings.map((f) => f.ruleId)).toEqual(["A"]); }); + it("runs no rules when enabledRules is an empty allowlist", () => { + const engine = new Engine([rule("A", "low"), rule("B", "high")]); + const result = engine.run(makeTarget(), { enabledRules: [] }); + expect(result.rulesRun).toEqual([]); + expect(result.findings).toEqual([]); + }); + it("applies severity overrides", () => { const engine = new Engine([rule("A", "low")]); const result = engine.run(makeTarget(), {