Skip to content

Commit 2d45a23

Browse files
authored
fix: trace filter policy (#57)
Before the built-in trace filter would override the `default_value_action` which the user would supply. Also if the user supplies `default_value_action=drop`, the built-in filter would change it to `redact` for detected secret variables. We've updated the policy to not have such issues codetracer-python-recorder/resources/trace_filters/builtin_default.toml: codetracer-python-recorder/src/runtime/tracer/runtime_tracer.rs: codetracer-python-recorder/src/trace_filter/engine.rs: codetracer-python-recorder/src/trace_filter/loader.rs: codetracer-python-recorder/src/trace_filter/model.rs:
2 parents 8570ee0 + ea5c6d0 commit 2d45a23

File tree

5 files changed

+130
-4
lines changed

5 files changed

+130
-4
lines changed

codetracer-python-recorder/resources/trace_filters/builtin_default.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ reason = "Skip builtins module instrumentation"
3535

3636
[[scope.rules]]
3737
selector = 'pkg:glob:*'
38-
value_default = "allow"
3938

4039
[[scope.rules.value_patterns]]
4140
selector = 'local:regex:(?i).*(pass(word)?|passwd|pwd|secret|token|session|cookie|auth|credential|creds|bearer|ssn|credit|card|iban|cvv|cvc|pan|api[_-]?key|private[_-]?key|secret[_-]?key|ssh[_-]?key|jwt|refresh[_-]?token|access[_-]?token).*'

codetracer-python-recorder/src/runtime/tracer/runtime_tracer.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ mod tests {
151151
static LAST_OUTCOME: Cell<Option<CallbackOutcome>> = Cell::new(None);
152152
}
153153

154+
const BUILTIN_TRACE_FILTER: &str =
155+
include_str!("../../../resources/trace_filters/builtin_default.toml");
156+
154157
struct ScopedTracer;
155158

156159
impl ScopedTracer {
@@ -1036,6 +1039,94 @@ sensitive("s3cr3t")
10361039
});
10371040
}
10381041

1042+
#[test]
1043+
fn user_drop_default_overrides_builtin_allowance() {
1044+
Python::with_gil(|py| {
1045+
ensure_test_module(py);
1046+
1047+
let project = tempfile::tempdir().expect("project dir");
1048+
let project_root = project.path();
1049+
let filters_dir = project_root.join(".codetracer");
1050+
fs::create_dir(&filters_dir).expect("create .codetracer");
1051+
let drop_filter_path = filters_dir.join("drop-filter.toml");
1052+
write_filter(
1053+
&drop_filter_path,
1054+
r#"
1055+
[meta]
1056+
name = "drop-all"
1057+
version = 1
1058+
1059+
[scope]
1060+
default_exec = "trace"
1061+
default_value_action = "drop"
1062+
"#,
1063+
);
1064+
1065+
let config = TraceFilterConfig::from_inline_and_paths(
1066+
&[("builtin-default", BUILTIN_TRACE_FILTER)],
1067+
&[drop_filter_path.clone()],
1068+
)
1069+
.expect("load filter chain");
1070+
let engine = Arc::new(TraceFilterEngine::new(config));
1071+
1072+
let app_dir = project_root.join("app");
1073+
fs::create_dir_all(&app_dir).expect("create app dir");
1074+
let script_path = app_dir.join("dropper.py");
1075+
let body = r#"
1076+
def dropper():
1077+
secret = "token"
1078+
public = 42
1079+
snapshot()
1080+
emit_return(secret)
1081+
return secret
1082+
1083+
dropper()
1084+
"#;
1085+
let script = format!("{PRELUDE}\n{body}", PRELUDE = PRELUDE, body = body);
1086+
fs::write(&script_path, script).expect("write script");
1087+
1088+
let mut tracer = RuntimeTracer::new(
1089+
script_path.to_string_lossy().as_ref(),
1090+
&[],
1091+
TraceEventsFileFormat::Json,
1092+
None,
1093+
Some(engine),
1094+
);
1095+
1096+
{
1097+
let _guard = ScopedTracer::new(&mut tracer);
1098+
LAST_OUTCOME.with(|cell| cell.set(None));
1099+
let run_code = format!(
1100+
"import runpy, sys\nsys.path.insert(0, r\"{}\")\nrunpy.run_path(r\"{}\")",
1101+
project_root.display(),
1102+
script_path.display()
1103+
);
1104+
let run_code_c = CString::new(run_code).expect("script contains nul byte");
1105+
py.run(run_code_c.as_c_str(), None, None)
1106+
.expect("execute dropper script");
1107+
}
1108+
1109+
let mut variable_names: Vec<String> = Vec::new();
1110+
let mut return_events = 0usize;
1111+
for event in &tracer.writer.events {
1112+
match event {
1113+
TraceLowLevelEvent::VariableName(name) => variable_names.push(name.clone()),
1114+
TraceLowLevelEvent::Return(_) => return_events += 1,
1115+
_ => {}
1116+
}
1117+
}
1118+
assert!(
1119+
variable_names.is_empty(),
1120+
"expected no variables captured, found {:?}",
1121+
variable_names
1122+
);
1123+
assert_eq!(
1124+
return_events, 0,
1125+
"return value should be dropped instead of recorded"
1126+
);
1127+
});
1128+
}
1129+
10391130
#[test]
10401131
fn trace_filter_metadata_includes_summary() {
10411132
Python::with_gil(|py| {

codetracer-python-recorder/src/trace_filter/engine.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ pub struct TraceFilterEngine {
185185
config: Arc<TraceFilterConfig>,
186186
default_exec: ExecDecision,
187187
default_value_action: ValueAction,
188+
default_value_source: usize,
188189
rules: Arc<[CompiledScopeRule]>,
189190
cache: DashMap<usize, Arc<ScopeResolution>>,
190191
}
@@ -194,12 +195,14 @@ impl TraceFilterEngine {
194195
pub fn new(config: TraceFilterConfig) -> Self {
195196
let default_exec = config.default_exec().into();
196197
let default_value_action = config.default_value_action();
198+
let default_value_source = config.default_value_source();
197199
let rules = compile_rules(config.rules());
198200

199201
TraceFilterEngine {
200202
config: Arc::new(config),
201203
default_exec,
202204
default_value_action,
205+
default_value_source,
203206
rules,
204207
cache: DashMap::new(),
205208
}
@@ -239,6 +242,7 @@ impl TraceFilterEngine {
239242

240243
let mut exec = self.default_exec;
241244
let mut value_default = self.default_value_action;
245+
let mut value_default_source = self.default_value_source;
242246
let mut patterns: Arc<[CompiledValuePattern]> = Arc::from(Vec::new());
243247
let mut matched_rule_index = None;
244248
let mut matched_rule_source = context.source_id;
@@ -251,16 +255,33 @@ impl TraceFilterEngine {
251255
}
252256
if let Some(rule_value) = rule.value_default {
253257
value_default = rule_value;
258+
value_default_source = rule.source_id;
254259
}
255-
if !rule.value_patterns.is_empty() {
256-
patterns = rule.value_patterns.clone();
257-
}
260+
patterns = rule.value_patterns.clone();
258261
matched_rule_index = Some(rule.index);
259262
matched_rule_source = Some(rule.source_id);
260263
matched_rule_reason = rule.reason.clone();
261264
}
262265
}
263266

267+
let patterns = if value_default == ValueAction::Drop {
268+
if patterns
269+
.iter()
270+
.all(|pattern| pattern.source_id >= value_default_source)
271+
{
272+
patterns
273+
} else {
274+
let filtered: Vec<CompiledValuePattern> = patterns
275+
.iter()
276+
.filter(|pattern| pattern.source_id >= value_default_source)
277+
.cloned()
278+
.collect();
279+
filtered.into()
280+
}
281+
} else {
282+
patterns
283+
};
284+
264285
let value_policy = Arc::new(ValuePolicy::new(value_default, patterns));
265286

266287
Ok(ScopeResolution {

codetracer-python-recorder/src/trace_filter/loader.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use std::path::{Component, Path, PathBuf};
1717
pub struct ConfigAggregator {
1818
default_exec: Option<ExecDirective>,
1919
default_value_action: Option<ValueAction>,
20+
default_value_source: Option<usize>,
2021
io: Option<IoConfig>,
2122
rules: Vec<ScopeRule>,
2223
sources: Vec<FilterSource>,
@@ -57,12 +58,19 @@ impl ConfigAggregator {
5758
"composed filters never set 'scope.default_value_action'"
5859
)
5960
})?;
61+
let default_value_source = self.default_value_source.ok_or_else(|| {
62+
usage!(
63+
ErrorCode::InvalidPolicyValue,
64+
"failed to record source for 'scope.default_value_action'"
65+
)
66+
})?;
6067

6168
let io = self.io.unwrap_or_default();
6269

6370
Ok(TraceFilterConfig {
6471
default_exec,
6572
default_value_action,
73+
default_value_source,
6674
io,
6775
rules: self.rules,
6876
sources: self.sources,
@@ -100,6 +108,7 @@ impl ConfigAggregator {
100108
}
101109
if let Some(value_action) = defaults.value_action {
102110
self.default_value_action = Some(value_action);
111+
self.default_value_source = Some(source_index);
103112
}
104113

105114
if let Some(io) = parse_io(raw.io.as_ref(), path)? {

codetracer-python-recorder/src/trace_filter/model.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ pub struct FilterSummaryEntry {
137137
pub struct TraceFilterConfig {
138138
pub(crate) default_exec: ExecDirective,
139139
pub(crate) default_value_action: ValueAction,
140+
pub(crate) default_value_source: usize,
140141
pub(crate) io: IoConfig,
141142
pub(crate) rules: Vec<ScopeRule>,
142143
pub(crate) sources: Vec<FilterSource>,
@@ -153,6 +154,11 @@ impl TraceFilterConfig {
153154
self.default_value_action
154155
}
155156

157+
/// Source index of the definition that last set the default value action.
158+
pub fn default_value_source(&self) -> usize {
159+
self.default_value_source
160+
}
161+
156162
/// IO capture configuration associated with the composed filter chain.
157163
pub fn io(&self) -> &IoConfig {
158164
&self.io

0 commit comments

Comments
 (0)