Skip to content

Commit 58d37e9

Browse files
committed
feat: port secrets detection plugin
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent 6ca511b commit 58d37e9

25 files changed

Lines changed: 1426 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
members = [
33
"plugins/rust/python-package/pii_filter",
44
"plugins/rust/python-package/rate_limiter",
5+
"plugins/rust/python-package/secrets_detection",
56
]
67
resolver = "3"
78

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[package]
2+
name = "secrets_detection"
3+
version = "0.1.0"
4+
edition.workspace = true
5+
authors.workspace = true
6+
license.workspace = true
7+
repository.workspace = true
8+
description = "Rust-backed secrets detection plugin for MCP Gateway"
9+
10+
[lib]
11+
name = "secrets_detection_rust"
12+
crate-type = ["cdylib", "rlib"]
13+
14+
[[bin]]
15+
name = "stub_gen"
16+
path = "src/bin/stub_gen.rs"
17+
18+
[dependencies]
19+
cpex_framework_bridge = { path = "../../../../crates/framework_bridge" }
20+
log = { workspace = true }
21+
pyo3 = { workspace = true }
22+
pyo3-log = { workspace = true }
23+
pyo3-stub-gen = { workspace = true }
24+
regex = "1.12"
25+
26+
[dev-dependencies]
27+
criterion = { version = "0.8", features = ["html_reports"] }
28+
29+
[[bench]]
30+
name = "secrets_detection"
31+
harness = false
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
.PHONY: help
2+
help:
3+
@grep '^# help\:' $(firstword $(MAKEFILE_LIST)) | sed 's/^# help\: //'
4+
5+
PACKAGE_NAME := cpex-secrets-detection
6+
WHEEL_PREFIX := cpex_secrets_detection
7+
CARGO := cargo
8+
STUB_FILES := cpex_secrets_detection/__init__.pyi cpex_secrets_detection/secrets_detection_rust/__init__.pyi
9+
WHEEL_DIR := ../../../../target/wheels
10+
11+
GREEN := \033[0;32m
12+
YELLOW := \033[0;33m
13+
NC := \033[0m
14+
15+
# help: fmt - Format Rust code with rustfmt
16+
# help: fmt-check - Check Rust code formatting (CI)
17+
# help: clippy - Run clippy lints
18+
.PHONY: fmt fmt-check clippy
19+
20+
fmt:
21+
$(CARGO) fmt
22+
23+
fmt-check:
24+
$(CARGO) fmt -- --check
25+
26+
clippy:
27+
$(CARGO) clippy -- -D warnings
28+
29+
# help: sync - Install plugin development dependencies
30+
# help: test - Run Rust unit tests
31+
# help: test-verbose - Run Rust tests with verbose output
32+
# help: test-python - Run Python plugin tests
33+
# help: test-all - Run both Rust and Python tests
34+
.PHONY: sync test test-verbose test-python test-all verify-stubs
35+
36+
sync:
37+
uv sync --dev
38+
39+
test:
40+
@echo "$(GREEN)Running secrets_detection Rust tests...$(NC)"
41+
$(CARGO) test
42+
43+
test-verbose:
44+
@echo "$(GREEN)Running secrets_detection Rust tests (verbose)...$(NC)"
45+
$(CARGO) test -- --nocapture
46+
47+
test-python:
48+
@echo "$(GREEN)Running Python tests...$(NC)"
49+
uv run pytest tests/ -v
50+
51+
test-all: test test-python
52+
53+
verify-stubs: stub-gen
54+
@git diff --exit-code -- $(STUB_FILES)
55+
56+
# help: stub-gen - Generate Python type stubs (.pyi files)
57+
# help: build - Build release wheel (no install)
58+
# help: install - Build and install editable extension into project venv
59+
# help: install-wheel - Install the previously built wheel into project venv
60+
.PHONY: stub-gen build install install-wheel uninstall
61+
62+
stub-gen:
63+
@echo "$(GREEN)Generating Python type stubs...$(NC)"
64+
$(CARGO) run --bin stub_gen
65+
@echo "$(GREEN)Stubs generated$(NC)"
66+
67+
build: stub-gen
68+
@echo "$(GREEN)Building $(PACKAGE_NAME)...$(NC)"
69+
uv run maturin build --release
70+
@echo "$(GREEN)Build complete$(NC)"
71+
72+
install: stub-gen
73+
@echo "$(GREEN)Installing $(PACKAGE_NAME)...$(NC)"
74+
uv run maturin develop --release
75+
@echo "$(GREEN)Installation complete$(NC)"
76+
77+
install-wheel: build
78+
@echo "$(GREEN)Installing built wheel for $(PACKAGE_NAME)...$(NC)"
79+
python3 ../../../../tools/install_built_wheel.py --wheel-dir "$(WHEEL_DIR)" --wheel-prefix "$(WHEEL_PREFIX)" --package-name "$(PACKAGE_NAME)" --venv-dir .venv
80+
@echo "$(GREEN)Wheel installation complete$(NC)"
81+
82+
uninstall:
83+
@echo "$(YELLOW)Uninstalling $(PACKAGE_NAME)...$(NC)"
84+
@uv pip uninstall -y $(PACKAGE_NAME) 2>/dev/null || true
85+
86+
# help: bench - Run Criterion benchmarks
87+
# help: bench-no-run - Compile Criterion benchmarks without running them
88+
.PHONY: bench bench-no-run
89+
90+
bench:
91+
@echo "$(GREEN)Running benchmarks...$(NC)"
92+
$(CARGO) bench
93+
94+
bench-no-run:
95+
@echo "$(GREEN)Compiling benchmarks without running them...$(NC)"
96+
$(CARGO) bench --no-run
97+
98+
.PHONY: clean clean-all
99+
100+
clean:
101+
$(CARGO) clean
102+
rm -rf target/ coverage/
103+
find . -name "*.whl" -delete
104+
105+
clean-all: clean
106+
107+
# help: verify - Verify plugin installation
108+
# help: check-all - Run fmt-check + clippy + Rust tests
109+
# help: ci - Run the full CI-equivalent plugin verification flow
110+
.PHONY: verify check-all ci pre-commit
111+
112+
verify:
113+
@uv run python -c "from cpex_secrets_detection import secrets_detection_rust; print('secrets_detection_rust available')" || echo "secrets_detection_rust not installed — run: make install"
114+
115+
check-all: fmt-check clippy test
116+
@echo "$(GREEN)All checks passed$(NC)"
117+
118+
ci: check-all verify-stubs build bench-no-run install-wheel test-python
119+
@echo "$(GREEN)CI verification passed$(NC)"
120+
121+
pre-commit: check-all
122+
123+
.DEFAULT_GOAL := help
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# cpex-secrets-detection
2+
3+
Rust-backed secrets detection plugin for MCP Gateway / CPEX.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use criterion::{Criterion, criterion_group, criterion_main};
2+
use secrets_detection_rust::config::SecretsDetectionConfig;
3+
use secrets_detection_rust::scanner::detect_and_redact;
4+
5+
fn bench_detect_and_redact(c: &mut Criterion) {
6+
let config = SecretsDetectionConfig {
7+
redact: true,
8+
..Default::default()
9+
};
10+
let text = "prefix AWS_ACCESS_KEY_ID=AKIAFAKE12345EXAMPLE suffix";
11+
c.bench_function("detect_and_redact/aws", |b| {
12+
b.iter(|| detect_and_redact(text, &config))
13+
});
14+
}
15+
16+
criterion_group!(benches, bench_detect_and_redact);
17+
criterion_main!(benches);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding: utf-8 -*-
2+
"""Secrets detection plugin package."""
3+
4+
from __future__ import annotations
5+
6+
7+
def __getattr__(name: str):
8+
if name == "SecretsDetectionPlugin":
9+
from cpex_secrets_detection.secrets_detection import SecretsDetectionPlugin
10+
11+
return SecretsDetectionPlugin
12+
if name == "py_scan_container":
13+
from cpex_secrets_detection.secrets_detection_rust import py_scan_container
14+
15+
return py_scan_container
16+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
17+
18+
19+
__all__ = ["SecretsDetectionPlugin", "py_scan_container"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# This file is automatically generated by pyo3_stub_gen
2+
# ruff: noqa: E501, F401, F403, F405
3+
4+
from .secrets_detection import SecretsDetectionPlugin
5+
from .secrets_detection_rust import py_scan_container
6+
7+
__all__ = [
8+
"SecretsDetectionPlugin",
9+
"py_scan_container",
10+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
description: "Detect likely credentials and secrets in prompt input, tool output, and resource content"
2+
author: "ContextForge Contributors"
3+
version: "0.1.0"
4+
available_hooks:
5+
- "prompt_pre_fetch"
6+
- "tool_post_invoke"
7+
- "resource_post_fetch"
8+
default_configs:
9+
enabled:
10+
aws_access_key_id: true
11+
aws_secret_access_key: true
12+
google_api_key: true
13+
github_token: true
14+
stripe_secret_key: true
15+
generic_api_key_assignment: false
16+
slack_token: true
17+
private_key_block: true
18+
jwt_like: false
19+
hex_secret_32: false
20+
base64_24: false
21+
redact: false
22+
redaction_text: "***REDACTED***"
23+
block_on_detection: true
24+
min_findings_to_block: 1
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# -*- coding: utf-8 -*-
2+
"""Thin compatibility shim for the Rust-owned secrets detection plugin."""
3+
4+
from __future__ import annotations
5+
6+
try:
7+
from mcpgateway.plugins.framework import Plugin
8+
except ModuleNotFoundError:
9+
class Plugin: # type: ignore[no-redef]
10+
def __init__(self, config) -> None:
11+
self._config = config
12+
13+
from cpex_secrets_detection.secrets_detection_rust import (
14+
SecretsDetectionPluginCore,
15+
py_scan_container,
16+
)
17+
18+
19+
class SecretsDetectionPlugin(Plugin):
20+
"""Gateway-facing Plugin subclass that delegates behavior to Rust."""
21+
22+
def __init__(self, config) -> None:
23+
super().__init__(config)
24+
self._core = SecretsDetectionPluginCore(config.config or {})
25+
26+
async def prompt_pre_fetch(self, payload, context):
27+
return self._core.prompt_pre_fetch(payload, context)
28+
29+
async def tool_post_invoke(self, payload, context):
30+
return self._core.tool_post_invoke(payload, context)
31+
32+
async def resource_post_fetch(self, payload, context):
33+
return self._core.resource_post_fetch(payload, context)
34+
35+
36+
__all__ = ["SecretsDetectionPlugin", "py_scan_container"]

0 commit comments

Comments
 (0)