Skip to content

Commit 38ccb4f

Browse files
committed
fix: harden secrets detection benchmark behavior
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent 00605e6 commit 38ccb4f

6 files changed

Lines changed: 82 additions & 13 deletions

File tree

plugins/secrets_detection/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ Configuration (example)
2424
generic_api_key_assignment: false # Broad heuristic; enable only if you want generic header/assignment coverage
2525
slack_token: true
2626
private_key_block: true
27-
jwt_like: true
28-
hex_secret_32: true
29-
base64_24: true
27+
jwt_like: false # Broad heuristic; keep disabled by default unless you explicitly want warning/block coverage for JWT-shaped tokens
28+
hex_secret_32: false # Broad heuristic; keep disabled by default unless you explicitly want coverage for generic hex strings
29+
base64_24: false # Broad heuristic; keep disabled by default unless you explicitly want coverage for generic base64-like strings
3030
redact: false # replace matches with redaction_text
3131
redaction_text: "***REDACTED***"
3232
block_on_detection: true

plugins_rust/secrets_detection/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ secrets_detection:
5252
generic_api_key_assignment: false # Broad heuristic; useful for X-API-Key/api_key=... style coverage, but can increase false positives
5353
slack_token: true
5454
private_key_block: true
55-
jwt_like: true
56-
hex_secret_32: true
57-
base64_24: true
55+
jwt_like: false
56+
hex_secret_32: false
57+
base64_24: false
5858
redact: false # Replace secrets with redaction_text
5959
redaction_text: "***REDACTED***"
6060
block_on_detection: true # Block requests containing secrets

tests/loadtest/locustfile_secret_detection.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
from locust import between, task
2424
from locust.contrib.fasthttp import FastHttpUser
2525

26+
from tests.loadtest.secret_detection_benchmark_utils import is_secret_detection_blocked
27+
2628

2729
class SecretsDetectionUser(FastHttpUser):
2830
"""User that sends prompts with and without secrets to test detection performance."""
@@ -96,15 +98,13 @@ def get_prompt_with_secret_expect_block(self):
9698
name="/rpc prompts/get [secret-blocked]",
9799
catch_response=True,
98100
) as response:
99-
# Expect 403 when secrets are detected and blocked
100-
if response.status_code == 403:
101+
if is_secret_detection_blocked(response):
101102
self.secrets_blocked_count += 1
102103
response.success()
103104
elif response.status_code == 200:
104105
self.secrets_not_blocked_count += 1
105-
response.failure("Secret-bearing prompt unexpectedly succeeded with HTTP 200")
106+
response.failure("Secret-bearing payload was accepted without a secrets-detection violation")
107+
if self.secrets_not_blocked_count % 10 == 0:
108+
print(f"⚠️ Warning: {self.secrets_not_blocked_count} secrets not blocked (may indicate detection disabled)")
106109
else:
107110
response.failure(f"Unexpected status {response.status_code}")
108-
109-
110-
# Made with Bob
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Helpers for the secrets-detection benchmark."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
8+
def is_secret_detection_blocked(response: Any) -> bool:
9+
"""Return True when the response indicates the secrets plugin blocked the request."""
10+
if getattr(response, "status_code", None) == 403:
11+
return True
12+
13+
try:
14+
payload = response.json()
15+
except Exception:
16+
return False
17+
18+
if not isinstance(payload, dict):
19+
return False
20+
21+
error = payload.get("error")
22+
if not isinstance(error, dict):
23+
return False
24+
25+
data = error.get("data")
26+
if isinstance(data, dict) and data.get("plugin_error_code") == "SECRETS_DETECTED":
27+
return True
28+
29+
return False

tests/unit/plugins/test_secrets_detection.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from mcpgateway.plugins.framework import PluginConfig, ResourceHookType
1313
from mcpgateway.services.resource_service import ResourceService
1414
from plugins.secrets_detection.secrets_detection import SecretsDetectionPlugin
15+
from tests.loadtest.secret_detection_benchmark_utils import is_secret_detection_blocked
1516

1617
# Try to import Rust implementation
1718
try:
@@ -568,3 +569,40 @@ def test_plugin_warns_when_broad_patterns_enabled(caplog):
568569

569570
assert "Broad secrets heuristics enabled" in caplog.text
570571
assert "generic_api_key_assignment" in caplog.text
572+
573+
574+
def test_benchmark_helper_treats_json_rpc_secret_violation_as_blocked():
575+
"""Load-test helper should recognize JSON-RPC secret violations even on HTTP 200."""
576+
577+
class DummyResponse:
578+
status_code = 200
579+
580+
@staticmethod
581+
def json():
582+
return {
583+
"error": {
584+
"message": "Plugin Violation: secrets detected",
585+
"data": {
586+
"plugin_error_code": "SECRETS_DETECTED",
587+
},
588+
}
589+
}
590+
591+
assert is_secret_detection_blocked(DummyResponse()) is True
592+
593+
594+
def test_benchmark_helper_does_not_treat_success_payload_as_blocked():
595+
"""Load-test helper should not flag ordinary successful responses as blocked."""
596+
597+
class DummyResponse:
598+
status_code = 200
599+
600+
@staticmethod
601+
def json():
602+
return {
603+
"result": {
604+
"messages": [],
605+
}
606+
}
607+
608+
assert is_secret_detection_blocked(DummyResponse()) is False

tests/unit/test_locustfile_secret_detection.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ def test_secret_request_marks_http_200_as_failure():
2626
for node in ast.walk(target_function)
2727
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == "response" and node.func.attr == "success"
2828
]
29+
helper_calls = [node for node in ast.walk(target_function) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "is_secret_detection_blocked"]
2930

30-
assert any(call.args and isinstance(call.args[0], ast.Constant) and call.args[0].value == "Secret-bearing prompt unexpectedly succeeded with HTTP 200" for call in failure_calls)
31+
assert helper_calls
32+
assert any(call.args and isinstance(call.args[0], ast.Constant) and call.args[0].value == "Secret-bearing payload was accepted without a secrets-detection violation" for call in failure_calls)
3133
assert len(success_calls) == 1

0 commit comments

Comments
 (0)