From b38b07006b0bf45d45d1c9e6c0c7921f40066e46 Mon Sep 17 00:00:00 2001 From: claude-bot-go Date: Mon, 20 Jul 2026 23:26:46 +0800 Subject: [PATCH 1/2] docs(aep-core): expand classify_mcp_headers() doc comment and add case-insensitive test Enhance the function-level documentation to describe all three detection layers (credential prefix, high-entropy value, PII) and their priority order, satisfying the 'Documentation explains the classification logic' acceptance criterion for #187. Add a test verifying case-insensitive credential prefix matching, a documented behavior not previously covered by an explicit test. Closes #187 Co-Authored-By: Claude --- crates/aep-core/src/mcp_headers.rs | 32 ++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/crates/aep-core/src/mcp_headers.rs b/crates/aep-core/src/mcp_headers.rs index 8347cd8..552e936 100644 --- a/crates/aep-core/src/mcp_headers.rs +++ b/crates/aep-core/src/mcp_headers.rs @@ -1,8 +1,24 @@ pub use crate::evidence::McpHeaderRisk; -/// Check MCP-Method and MCP-Name header values for sensitive-data leakage patterns. +/// Check MCP-Method and MCP-Name header values for sensitive-data leakage patterns +/// as defined by the MCP 2026-07-28 specification. /// -/// Returns the highest-severity risk detected, or None if no risk is found. +/// Three detection layers are applied in priority order (first match wins): +/// +/// 1. **Credential prefix detection** — both `mcp_method` and `mcp_name` are +/// checked (case-insensitive) against known credential prefixes +/// (`ghp_`, `ghb_`, `sk-`, `Bearer `, `token `, `api_`). A match yields +/// [`McpHeaderRisk::CredentialLeak`]. +/// +/// 2. **High-entropy value detection** — a contiguous alphanumeric run of +/// ≥ 32 characters in either header value suggests a leaked API key or +/// opaque token, yielding [`McpHeaderRisk::HighEntropyValue`]. +/// +/// 3. **PII detection** — the `mcp_name` value is checked for an email-like +/// pattern (contains both `@` and `.`), yielding [`McpHeaderRisk::PiiLeak`]. +/// +/// Returns the highest-severity [`McpHeaderRisk`] detected, or `None` if no +/// leakage pattern is found and both headers are absent or benign. pub fn classify_mcp_headers( mcp_method: Option<&str>, mcp_name: Option<&str>, @@ -76,6 +92,18 @@ mod tests { ); } + #[test] + fn classify_mcp_headers_credential_prefix_case_insensitive() { + assert_eq!( + classify_mcp_headers(Some("GHP_ABC123"), None), + Some(McpHeaderRisk::CredentialLeak) + ); + assert_eq!( + classify_mcp_headers(Some("SK-abcdefghij"), None), + Some(McpHeaderRisk::CredentialLeak) + ); + } + #[test] fn classify_mcp_headers_clean_values_return_none() { assert_eq!( From 6caf6f0a2aa89b95e2a85899969dad3c4a456cf2 Mon Sep 17 00:00:00 2001 From: claude-bot-go Date: Mon, 20 Jul 2026 23:33:51 +0800 Subject: [PATCH 2/2] Fix #187: [milestone Milestone 2] Implement classify_mcp_headers() function in crates/proxy-wasm-evidence or aep-core --- crates/aep-core/src/mcp_headers.rs | 79 ++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/crates/aep-core/src/mcp_headers.rs b/crates/aep-core/src/mcp_headers.rs index 552e936..4951d6a 100644 --- a/crates/aep-core/src/mcp_headers.rs +++ b/crates/aep-core/src/mcp_headers.rs @@ -19,6 +19,22 @@ pub use crate::evidence::McpHeaderRisk; /// /// Returns the highest-severity [`McpHeaderRisk`] detected, or `None` if no /// leakage pattern is found and both headers are absent or benign. +/// +/// # Integration with `build_evidence` +/// +/// The returned `Option` is designed to pass directly as the +/// `mcp_header_risk` parameter of +/// [`proxy_wasm_evidence::recorder::build_evidence`], which converts the enum +/// to a `snake_case` string via [`McpHeaderRisk::as_str`] and stores it on +/// [`ActionEvidence::mcp_header_risk`]. This is the wiring used by +/// `EvidenceFilter::on_http_response_headers` in the Proxy-Wasm HTTP filter: +/// +/// ```text +/// let risk = classify_mcp_headers(mcp_method, mcp_name); +/// let evidence = build_evidence(id, tool, &risk_ctx, ts, None, risk); +/// // ^^^ +/// // Option flows through +/// ``` pub fn classify_mcp_headers( mcp_method: Option<&str>, mcp_name: Option<&str>, @@ -113,4 +129,67 @@ mod tests { assert_eq!(classify_mcp_headers(None, None), None); assert_eq!(classify_mcp_headers(Some("tools/list"), None), None); } + + #[test] + fn classify_mcp_headers_no_mcp_headers_returns_none() { + // Neither MCP-Method nor MCP-Name present — common for non-MCP traffic. + assert_eq!(classify_mcp_headers(None, None), None); + } + + #[test] + fn classify_mcp_headers_partial_mcp_headers_benign_returns_none() { + // Only MCP-Method present, MCP-Name absent — partial MCP headers, clean values. + assert_eq!(classify_mcp_headers(Some("tools/call"), None), None); + assert_eq!(classify_mcp_headers(Some("resources/read"), None), None); + + // Only MCP-Name present, MCP-Method absent — partial MCP headers, clean value. + assert_eq!(classify_mcp_headers(None, Some("my_tool")), None); + assert_eq!(classify_mcp_headers(None, Some("read_config")), None); + } + + #[test] + fn classify_mcp_headers_partial_mcp_headers_leakage_detected() { + // Only MCP-Method present with credential prefix — leakage still detected. + assert_eq!( + classify_mcp_headers(Some("ghp_abc123"), None), + Some(McpHeaderRisk::CredentialLeak) + ); + + // Only MCP-Name present with PII — leakage still detected. + assert_eq!( + classify_mcp_headers(None, Some("user@example.com")), + Some(McpHeaderRisk::PiiLeak) + ); + } + + #[test] + fn classify_mcp_headers_complete_mcp_headers_benign_returns_none() { + // Both MCP-Method and MCP-Name present with clean values — no leakage. + assert_eq!( + classify_mcp_headers(Some("tools/call"), Some("read_file")), + None + ); + assert_eq!( + classify_mcp_headers(Some("resources/read"), Some("my_resource")), + None + ); + } + + #[test] + fn classify_mcp_headers_complete_mcp_headers_leakage_in_method_detected() { + // Both headers present; MCP-Method carries a credential — credential wins. + assert_eq!( + classify_mcp_headers(Some("sk-secretkey"), Some("read_file")), + Some(McpHeaderRisk::CredentialLeak) + ); + } + + #[test] + fn classify_mcp_headers_complete_mcp_headers_leakage_in_name_detected() { + // Both headers present; MCP-Name carries PII — PII detected. + assert_eq!( + classify_mcp_headers(Some("tools/call"), Some("admin@corp.io")), + Some(McpHeaderRisk::PiiLeak) + ); + } }