Skip to content

Commit ba6ff62

Browse files
committed
Add secrets detection tool pre-invoke hook
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent 5077ff9 commit ba6ff62

13 files changed

Lines changed: 410 additions & 87 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/rust/python-package/secrets_detection/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "secrets_detection"
3-
version = "0.3.1"
3+
version = "0.3.2"
44
edition.workspace = true
55
authors.workspace = true
66
license.workspace = true

plugins/rust/python-package/secrets_detection/cpex_secrets_detection/plugin-manifest.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
description: "Detect likely credentials and secrets in prompt input, tool output, and resource content"
22
author: "ContextForge Contributors"
3-
version: "0.3.1"
3+
version: "0.3.2"
44
kind: "cpex_secrets_detection.secrets_detection.SecretsDetectionPlugin"
55
available_hooks:
66
- "prompt_pre_fetch"
7+
- "tool_pre_invoke"
78
- "tool_post_invoke"
89
- "resource_post_fetch"
910
default_configs:

plugins/rust/python-package/secrets_detection/cpex_secrets_detection/secrets_detection.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ def __init__(self, config) -> None:
2020
async def prompt_pre_fetch(self, payload, context):
2121
return self._core.prompt_pre_fetch(payload, context)
2222

23+
async def tool_pre_invoke(self, payload, context):
24+
return self._core.tool_pre_invoke(payload, context)
25+
2326
async def tool_post_invoke(self, payload, context):
2427
return self._core.tool_post_invoke(payload, context)
2528

plugins/rust/python-package/secrets_detection/cpex_secrets_detection/secrets_detection_rust/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ __all__ = [
1212
class SecretsDetectionPluginCore:
1313
def __new__(cls, config: typing.Any) -> SecretsDetectionPluginCore: ...
1414
def prompt_pre_fetch(self, payload: typing.Any, _context: typing.Any) -> typing.Any: ...
15+
def tool_pre_invoke(self, payload: typing.Any, _context: typing.Any) -> typing.Any: ...
1516
def tool_post_invoke(self, payload: typing.Any, _context: typing.Any) -> typing.Any: ...
1617
def resource_post_fetch(self, payload: typing.Any, _context: typing.Any) -> typing.Any: ...
1718

plugins/rust/python-package/secrets_detection/src/plugin.rs

Lines changed: 83 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use pyo3_stub_gen::derive::*;
88

99
use crate::config::SecretsDetectionConfig;
1010
use crate::object_model::copy_object_with_updates;
11-
use crate::scanner::scan_container;
11+
use crate::scanner::{scan_container, scan_container_findings};
1212

1313
#[gen_stub_pyclass]
1414
#[pyclass]
@@ -32,54 +32,28 @@ impl SecretsDetectionPluginCore {
3232
payload: &Bound<'_, PyAny>,
3333
_context: &Bound<'_, PyAny>,
3434
) -> PyResult<Py<PyAny>> {
35-
let args = payload.getattr("args")?;
36-
let (count, redacted_args, findings) = scan_container(py, &args, &self.config)?;
37-
if self.should_block(count) {
38-
let modified_payload = if self.config.redact && count > 0 {
39-
copy_with_update(py, payload, [("args", redacted_args.clone().unbind())])?
40-
} else {
41-
payload.clone().unbind()
42-
};
43-
return blocked_result(
44-
py,
45-
"PromptPrehookResult",
46-
"Potential secrets detected in prompt arguments",
47-
count,
48-
findings.as_any(),
49-
modified_payload,
50-
);
51-
}
52-
53-
if self.config.redact && count > 0 {
54-
let modified_payload =
55-
copy_with_update(py, payload, [("args", redacted_args.unbind())])?;
56-
return build_framework_object(
57-
py,
58-
"PromptPrehookResult",
59-
[
60-
("modified_payload", modified_payload),
61-
(
62-
"metadata",
63-
redaction_metadata(py, count)?.into_any().unbind(),
64-
),
65-
],
66-
);
67-
}
68-
69-
if count > 0 {
70-
return build_framework_object(
71-
py,
72-
"PromptPrehookResult",
73-
[(
74-
"metadata",
75-
findings_metadata(py, count, findings.as_any())?
76-
.into_any()
77-
.unbind(),
78-
)],
79-
);
80-
}
35+
self.scan_payload_attr(
36+
py,
37+
payload,
38+
"args",
39+
"PromptPrehookResult",
40+
"Potential secrets detected in prompt arguments",
41+
)
42+
}
8143

82-
default_result(py, "PromptPrehookResult")
44+
pub fn tool_pre_invoke(
45+
&self,
46+
py: Python<'_>,
47+
payload: &Bound<'_, PyAny>,
48+
_context: &Bound<'_, PyAny>,
49+
) -> PyResult<Py<PyAny>> {
50+
self.scan_payload_attr(
51+
py,
52+
payload,
53+
"args",
54+
"ToolPreInvokeResult",
55+
"Potential secrets detected in tool arguments",
56+
)
8357
}
8458

8559
pub fn tool_post_invoke(
@@ -88,30 +62,51 @@ impl SecretsDetectionPluginCore {
8862
payload: &Bound<'_, PyAny>,
8963
_context: &Bound<'_, PyAny>,
9064
) -> PyResult<Py<PyAny>> {
91-
let value = payload.getattr("result")?;
92-
let (count, redacted_result, findings) = scan_container(py, &value, &self.config)?;
65+
self.scan_payload_attr(
66+
py,
67+
payload,
68+
"result",
69+
"ToolPostInvokeResult",
70+
"Potential secrets detected in tool result",
71+
)
72+
}
73+
74+
pub fn resource_post_fetch(
75+
&self,
76+
py: Python<'_>,
77+
payload: &Bound<'_, PyAny>,
78+
_context: &Bound<'_, PyAny>,
79+
) -> PyResult<Py<PyAny>> {
80+
let content = payload.getattr("content")?;
81+
let Ok(text) = content.getattr("text") else {
82+
return default_result(py, "ResourcePostFetchResult");
83+
};
84+
let (count, redacted_text, findings) = scan_container(py, &text, &self.config)?;
9385
if self.should_block(count) {
9486
let modified_payload = if self.config.redact && count > 0 {
95-
copy_with_update(py, payload, [("result", redacted_result.clone().unbind())])?
87+
let modified_content =
88+
copy_with_update(py, &content, [("text", redacted_text.clone().unbind())])?;
89+
copy_with_update(py, payload, [("content", modified_content)])?
9690
} else {
9791
payload.clone().unbind()
9892
};
9993
return blocked_result(
10094
py,
101-
"ToolPostInvokeResult",
102-
"Potential secrets detected in tool result",
95+
"ResourcePostFetchResult",
96+
"Potential secrets detected in resource content",
10397
count,
10498
findings.as_any(),
10599
modified_payload,
106100
);
107101
}
108102

109103
if self.config.redact && count > 0 {
110-
let modified_payload =
111-
copy_with_update(py, payload, [("result", redacted_result.unbind())])?;
104+
let modified_content =
105+
copy_with_update(py, &content, [("text", redacted_text.unbind())])?;
106+
let modified_payload = copy_with_update(py, payload, [("content", modified_content)])?;
112107
return build_framework_object(
113108
py,
114-
"ToolPostInvokeResult",
109+
"ResourcePostFetchResult",
115110
[
116111
("modified_payload", modified_payload),
117112
(
@@ -125,7 +120,7 @@ impl SecretsDetectionPluginCore {
125120
if count > 0 {
126121
return build_framework_object(
127122
py,
128-
"ToolPostInvokeResult",
123+
"ResourcePostFetchResult",
129124
[(
130125
"metadata",
131126
findings_metadata(py, count, findings.as_any())?
@@ -135,45 +130,58 @@ impl SecretsDetectionPluginCore {
135130
);
136131
}
137132

138-
default_result(py, "ToolPostInvokeResult")
133+
default_result(py, "ResourcePostFetchResult")
139134
}
135+
}
140136

141-
pub fn resource_post_fetch(
137+
impl SecretsDetectionPluginCore {
138+
fn should_block(&self, count: usize) -> bool {
139+
self.config.block_on_detection && count >= self.config.min_findings_to_block
140+
}
141+
142+
fn scan_payload_attr(
142143
&self,
143144
py: Python<'_>,
144145
payload: &Bound<'_, PyAny>,
145-
_context: &Bound<'_, PyAny>,
146+
attr: &str,
147+
result_class: &str,
148+
block_description: &str,
146149
) -> PyResult<Py<PyAny>> {
147-
let content = payload.getattr("content")?;
148-
let Ok(text) = content.getattr("text") else {
149-
return default_result(py, "ResourcePostFetchResult");
150+
let value = payload.getattr(attr)?;
151+
let (mut count, mut findings) = scan_container_findings(py, &value, &self.config)?;
152+
let redacted_value = if self.config.redact && count > 0 {
153+
let (redacted_count, redacted, redacted_findings) =
154+
scan_container(py, &value, &self.config)?;
155+
count = redacted_count;
156+
findings = redacted_findings;
157+
Some(redacted)
158+
} else {
159+
None
150160
};
151-
let (count, redacted_text, findings) = scan_container(py, &text, &self.config)?;
161+
152162
if self.should_block(count) {
153163
let modified_payload = if self.config.redact && count > 0 {
154-
let modified_content =
155-
copy_with_update(py, &content, [("text", redacted_text.clone().unbind())])?;
156-
copy_with_update(py, payload, [("content", modified_content)])?
164+
let redacted = redacted_value.expect("redacted value exists");
165+
copy_with_update(py, payload, [(attr, redacted.unbind())])?
157166
} else {
158167
payload.clone().unbind()
159168
};
160169
return blocked_result(
161170
py,
162-
"ResourcePostFetchResult",
163-
"Potential secrets detected in resource content",
171+
result_class,
172+
block_description,
164173
count,
165174
findings.as_any(),
166175
modified_payload,
167176
);
168177
}
169178

170179
if self.config.redact && count > 0 {
171-
let modified_content =
172-
copy_with_update(py, &content, [("text", redacted_text.unbind())])?;
173-
let modified_payload = copy_with_update(py, payload, [("content", modified_content)])?;
180+
let redacted = redacted_value.expect("redacted value exists");
181+
let modified_payload = copy_with_update(py, payload, [(attr, redacted.unbind())])?;
174182
return build_framework_object(
175183
py,
176-
"ResourcePostFetchResult",
184+
result_class,
177185
[
178186
("modified_payload", modified_payload),
179187
(
@@ -187,7 +195,7 @@ impl SecretsDetectionPluginCore {
187195
if count > 0 {
188196
return build_framework_object(
189197
py,
190-
"ResourcePostFetchResult",
198+
result_class,
191199
[(
192200
"metadata",
193201
findings_metadata(py, count, findings.as_any())?
@@ -197,13 +205,7 @@ impl SecretsDetectionPluginCore {
197205
);
198206
}
199207

200-
default_result(py, "ResourcePostFetchResult")
201-
}
202-
}
203-
204-
impl SecretsDetectionPluginCore {
205-
fn should_block(&self, count: usize) -> bool {
206-
self.config.block_on_detection && count >= self.config.min_findings_to_block
208+
default_result(py, result_class)
207209
}
208210
}
209211

plugins/rust/python-package/secrets_detection/src/scanner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ mod cycle_rewrite;
1111
mod python_scan;
1212
mod text_scan;
1313

14-
pub use python_scan::scan_container;
14+
pub use python_scan::{scan_container, scan_container_findings};
1515
pub use text_scan::detect_and_redact;
1616

1717
#[cfg(test)]

0 commit comments

Comments
 (0)