Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function csv(value: CliFlag | undefined): string[] {
.filter(Boolean);
}

function overlayFlags(
export function overlayFlags(
base: McpAuditConfig,
flags: Record<string, CliFlag>,
): McpAuditConfig {
Expand All @@ -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);
}

Expand Down
8 changes: 8 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Severity>;
/** Findings at or above this severity cause a non-zero exit. */
Expand All @@ -21,6 +23,7 @@ export interface McpAuditConfig {
export const DEFAULT_CONFIG: McpAuditConfig = {
disabledRules: [],
enabledRules: [],
enforceEnabledRules: false,
severityOverrides: {},
failOn: "high",
ignore: [],
Expand All @@ -29,6 +32,7 @@ export const DEFAULT_CONFIG: McpAuditConfig = {
const KNOWN_CONFIG_KEYS = new Set<string>([
"disabledRules",
"enabledRules",
"enforceEnabledRules",
"severityOverrides",
"failOn",
"ignore",
Expand Down Expand Up @@ -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],
Expand All @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/engine/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 13 additions & 2 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions test/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(), {
Expand Down
Loading