diff --git a/src/__tests__/scanner.test.ts b/src/__tests__/scanner.test.ts index dd8e44a..419d927 100644 --- a/src/__tests__/scanner.test.ts +++ b/src/__tests__/scanner.test.ts @@ -561,6 +561,33 @@ describe('INSECURE_TRANSPORT rule', () => { const findings = runRuntimeRule(RuleId.INSECURE_TRANSPORT, config); expect(findings).toHaveLength(0); }); + + it('fires for a ws:// serverUrl when transport.url is absent', () => { + // A config can declare its endpoint at the top level (`serverUrl`) with no + // nested `transport.url`. The rule must still catch cleartext WebSockets + // there — mirroring MISSING_TLS's serverUrl fallback. + const config: ParsedMcpConfig = { serverUrl: 'ws://example.com/mcp' }; + const findings = runRuntimeRule(RuleId.INSECURE_TRANSPORT, config); + expect(findings).toHaveLength(1); + expect(findings[0].ruleId).toBe(RuleId.INSECURE_TRANSPORT); + expect(findings[0].severity).toBe(Severity.HIGH); + }); + + it('does not fire for a wss:// serverUrl when transport.url is absent', () => { + const config: ParsedMcpConfig = { serverUrl: 'wss://example.com/mcp' }; + const findings = runRuntimeRule(RuleId.INSECURE_TRANSPORT, config); + expect(findings).toHaveLength(0); + }); + + it('prefers transport.url over serverUrl when both are present', () => { + const config: ParsedMcpConfig = { + serverUrl: 'wss://secure.example.com/mcp', + transport: { url: 'ws://insecure.example.com/mcp' }, + }; + const findings = runRuntimeRule(RuleId.INSECURE_TRANSPORT, config); + expect(findings).toHaveLength(1); + expect(findings[0].evidence).toContain('ws://insecure.example.com/mcp'); + }); }); // ─── failOn behaviour ───────────────────────────────────────────────────────── diff --git a/src/rules/runtime-rules.ts b/src/rules/runtime-rules.ts index 0d0c3fd..d2d64eb 100644 --- a/src/rules/runtime-rules.ts +++ b/src/rules/runtime-rules.ts @@ -15,7 +15,12 @@ export const runtimeRules: Rule[] = [ severity: Severity.HIGH, title: 'Insecure WebSocket Transport (ws://)', check(config: ParsedMcpConfig): Finding[] { - const url = config.transport?.url; + // Mirror MISSING_TLS: fall back to the top-level `serverUrl` when no + // `transport.url` is set. A config can declare its endpoint at either + // location (e.g. examples/minimal-config.json uses `serverUrl` alone), + // and an unencrypted `ws://` endpoint is equally insecure whichever + // field carries it. + const url = config.transport?.url ?? config.serverUrl; if (url && url.startsWith('ws://')) { return [ {