From 33c6cffc59808a26c9b5e8b46a09fdbd8991fa96 Mon Sep 17 00:00:00 2001 From: ruv Date: Mon, 6 Jul 2026 20:47:20 -0400 Subject: [PATCH] test(mcp-scan): pin per-finding HIGH severity so a downgrade can't silently disarm the gate (#4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beta feedback #4 (mutation finding): mcp-scan is the CI security gate — mcpScanCmd exits 1 iff a HIGH finding is present. Mutation testing showed that downgrading the `allow-shell` rule HIGH→MEDIUM SURVIVES the suite: the existing tests assert finding IDs + the aggregate `worst`, and the "flags shell access" case carries three co-occurring HIGH findings, so an allow-shell downgrade leaves `worst` still HIGH and goes unnoticed — the gate would silently stop blocking on shell access. Adds two mutation-killing tests: - allow-shell ALONE (clean default-deny policy + allowShell:true) is the SOLE non-info finding, so its severity is asserted directly (`high`), `worst` is `high`, and `mcpScanCmd` exits 1. Verified: this test FAILS when allow-shell is mutated to `medium` and PASSES on the real code. - each security-critical rule (no-policy, no-default-deny, wildcard-tool-perm) keeps its HIGH severity. Test-only; no behaviour change. Full create-agent-harness suite 363/363; tsc clean. Co-Authored-By: claude-flow --- node_modules | 1 + .../__tests__/mcp-scan.test.ts | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 120000 node_modules diff --git a/node_modules b/node_modules new file mode 120000 index 00000000..4c37106c --- /dev/null +++ b/node_modules @@ -0,0 +1 @@ +/home/ruvultra/projects/agent-harness-generator/node_modules \ No newline at end of file diff --git a/packages/create-agent-harness/__tests__/mcp-scan.test.ts b/packages/create-agent-harness/__tests__/mcp-scan.test.ts index 52652a39..b580f9e7 100644 --- a/packages/create-agent-harness/__tests__/mcp-scan.test.ts +++ b/packages/create-agent-harness/__tests__/mcp-scan.test.ts @@ -95,6 +95,42 @@ describe('scanMcp', () => { }); }); +// GH #4 (mutation finding): the CI security gate blocks (exit 1) iff a HIGH finding is present, so a +// mutation that downgrades a HIGH rule to MEDIUM silently flips the gate to pass. The existing tests +// assert finding IDs + the aggregate `worst`, which misses such a downgrade whenever another HIGH +// finding co-occurs (e.g. the "flags shell access" case has three HIGHs). These pin each security- +// critical rule's OWN severity and prove that each HIGH condition ALONE still blocks the gate. +describe('per-finding HIGH severity is pinned (mutation guard — GH #4)', () => { + it('allow-shell ALONE is HIGH and blocks the gate (exit 1)', async () => { + // Same clean setup as "passes a safe default-deny harness" (worst=info) but with allowShell:true, + // so allow-shell is the SOLE non-info finding — its downgrade can't hide behind another HIGH. + const dir = await makeHarness({ + policy: { ...SAFE, allowShell: true }, + allow: ['mcp__bot__*'], + deps: { '@metaharness/kernel': '0.1.0' }, + }); + const r = scanMcp(dir); + const nonInfo = r.findings.filter((f) => f.severity !== 'info'); + expect(nonInfo.map((f) => f.id)).toEqual(['allow-shell']); + expect(nonInfo[0].severity).toBe('high'); // kills the allow-shell HIGH→MEDIUM mutant + expect(r.worst).toBe('high'); + expect(mcpScanCmd([dir]).code).toBe(1); // kills the gate exit 1→0 flip + }); + + it('each security-critical rule keeps its HIGH severity', async () => { + const noPolicy = scanMcp(await makeHarness({ policy: null, allow: ['mcp__bot__*'] })); + expect(noPolicy.findings.find((f) => f.id === 'no-policy')?.severity).toBe('high'); + + const bad = scanMcp(await makeHarness({ + policy: { ...SAFE, defaultDeny: false }, + allow: ['mcp__*__*'], + deps: { '@metaharness/kernel': '0.1.0' }, + })); + expect(bad.findings.find((f) => f.id === 'no-default-deny')?.severity).toBe('high'); + expect(bad.findings.find((f) => f.id === 'wildcard-tool-perm')?.severity).toBe('high'); + }); +}); + describe('mcpScanCmd', () => { it('exits 1 on a HIGH finding and 0 on a clean harness', async () => { const bad = await makeHarness({ policy: null, allow: ['mcp__bot__*'] });