diff --git a/codetracer-python-recorder/resources/trace_filters/builtin_default.toml b/codetracer-python-recorder/resources/trace_filters/builtin_default.toml index 6749850..b47d566 100644 --- a/codetracer-python-recorder/resources/trace_filters/builtin_default.toml +++ b/codetracer-python-recorder/resources/trace_filters/builtin_default.toml @@ -35,7 +35,6 @@ reason = "Skip builtins module instrumentation" [[scope.rules]] selector = 'pkg:glob:*' -value_default = "allow" [[scope.rules.value_patterns]] 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).*' diff --git a/codetracer-python-recorder/src/runtime/tracer/runtime_tracer.rs b/codetracer-python-recorder/src/runtime/tracer/runtime_tracer.rs index fe77fb1..a21f611 100644 --- a/codetracer-python-recorder/src/runtime/tracer/runtime_tracer.rs +++ b/codetracer-python-recorder/src/runtime/tracer/runtime_tracer.rs @@ -151,6 +151,9 @@ mod tests { static LAST_OUTCOME: Cell> = Cell::new(None); } + const BUILTIN_TRACE_FILTER: &str = + include_str!("../../../resources/trace_filters/builtin_default.toml"); + struct ScopedTracer; impl ScopedTracer { @@ -1036,6 +1039,94 @@ sensitive("s3cr3t") }); } + #[test] + fn user_drop_default_overrides_builtin_allowance() { + Python::with_gil(|py| { + ensure_test_module(py); + + let project = tempfile::tempdir().expect("project dir"); + let project_root = project.path(); + let filters_dir = project_root.join(".codetracer"); + fs::create_dir(&filters_dir).expect("create .codetracer"); + let drop_filter_path = filters_dir.join("drop-filter.toml"); + write_filter( + &drop_filter_path, + r#" + [meta] + name = "drop-all" + version = 1 + + [scope] + default_exec = "trace" + default_value_action = "drop" + "#, + ); + + let config = TraceFilterConfig::from_inline_and_paths( + &[("builtin-default", BUILTIN_TRACE_FILTER)], + &[drop_filter_path.clone()], + ) + .expect("load filter chain"); + let engine = Arc::new(TraceFilterEngine::new(config)); + + let app_dir = project_root.join("app"); + fs::create_dir_all(&app_dir).expect("create app dir"); + let script_path = app_dir.join("dropper.py"); + let body = r#" +def dropper(): + secret = "token" + public = 42 + snapshot() + emit_return(secret) + return secret + +dropper() +"#; + let script = format!("{PRELUDE}\n{body}", PRELUDE = PRELUDE, body = body); + fs::write(&script_path, script).expect("write script"); + + let mut tracer = RuntimeTracer::new( + script_path.to_string_lossy().as_ref(), + &[], + TraceEventsFileFormat::Json, + None, + Some(engine), + ); + + { + let _guard = ScopedTracer::new(&mut tracer); + LAST_OUTCOME.with(|cell| cell.set(None)); + let run_code = format!( + "import runpy, sys\nsys.path.insert(0, r\"{}\")\nrunpy.run_path(r\"{}\")", + project_root.display(), + script_path.display() + ); + let run_code_c = CString::new(run_code).expect("script contains nul byte"); + py.run(run_code_c.as_c_str(), None, None) + .expect("execute dropper script"); + } + + let mut variable_names: Vec = Vec::new(); + let mut return_events = 0usize; + for event in &tracer.writer.events { + match event { + TraceLowLevelEvent::VariableName(name) => variable_names.push(name.clone()), + TraceLowLevelEvent::Return(_) => return_events += 1, + _ => {} + } + } + assert!( + variable_names.is_empty(), + "expected no variables captured, found {:?}", + variable_names + ); + assert_eq!( + return_events, 0, + "return value should be dropped instead of recorded" + ); + }); + } + #[test] fn trace_filter_metadata_includes_summary() { Python::with_gil(|py| { diff --git a/codetracer-python-recorder/src/trace_filter/engine.rs b/codetracer-python-recorder/src/trace_filter/engine.rs index 50666ff..411bd4f 100644 --- a/codetracer-python-recorder/src/trace_filter/engine.rs +++ b/codetracer-python-recorder/src/trace_filter/engine.rs @@ -185,6 +185,7 @@ pub struct TraceFilterEngine { config: Arc, default_exec: ExecDecision, default_value_action: ValueAction, + default_value_source: usize, rules: Arc<[CompiledScopeRule]>, cache: DashMap>, } @@ -194,12 +195,14 @@ impl TraceFilterEngine { pub fn new(config: TraceFilterConfig) -> Self { let default_exec = config.default_exec().into(); let default_value_action = config.default_value_action(); + let default_value_source = config.default_value_source(); let rules = compile_rules(config.rules()); TraceFilterEngine { config: Arc::new(config), default_exec, default_value_action, + default_value_source, rules, cache: DashMap::new(), } @@ -239,6 +242,7 @@ impl TraceFilterEngine { let mut exec = self.default_exec; let mut value_default = self.default_value_action; + let mut value_default_source = self.default_value_source; let mut patterns: Arc<[CompiledValuePattern]> = Arc::from(Vec::new()); let mut matched_rule_index = None; let mut matched_rule_source = context.source_id; @@ -251,16 +255,33 @@ impl TraceFilterEngine { } if let Some(rule_value) = rule.value_default { value_default = rule_value; + value_default_source = rule.source_id; } - if !rule.value_patterns.is_empty() { - patterns = rule.value_patterns.clone(); - } + patterns = rule.value_patterns.clone(); matched_rule_index = Some(rule.index); matched_rule_source = Some(rule.source_id); matched_rule_reason = rule.reason.clone(); } } + let patterns = if value_default == ValueAction::Drop { + if patterns + .iter() + .all(|pattern| pattern.source_id >= value_default_source) + { + patterns + } else { + let filtered: Vec = patterns + .iter() + .filter(|pattern| pattern.source_id >= value_default_source) + .cloned() + .collect(); + filtered.into() + } + } else { + patterns + }; + let value_policy = Arc::new(ValuePolicy::new(value_default, patterns)); Ok(ScopeResolution { diff --git a/codetracer-python-recorder/src/trace_filter/loader.rs b/codetracer-python-recorder/src/trace_filter/loader.rs index b568533..bba0bef 100644 --- a/codetracer-python-recorder/src/trace_filter/loader.rs +++ b/codetracer-python-recorder/src/trace_filter/loader.rs @@ -17,6 +17,7 @@ use std::path::{Component, Path, PathBuf}; pub struct ConfigAggregator { default_exec: Option, default_value_action: Option, + default_value_source: Option, io: Option, rules: Vec, sources: Vec, @@ -57,12 +58,19 @@ impl ConfigAggregator { "composed filters never set 'scope.default_value_action'" ) })?; + let default_value_source = self.default_value_source.ok_or_else(|| { + usage!( + ErrorCode::InvalidPolicyValue, + "failed to record source for 'scope.default_value_action'" + ) + })?; let io = self.io.unwrap_or_default(); Ok(TraceFilterConfig { default_exec, default_value_action, + default_value_source, io, rules: self.rules, sources: self.sources, @@ -100,6 +108,7 @@ impl ConfigAggregator { } if let Some(value_action) = defaults.value_action { self.default_value_action = Some(value_action); + self.default_value_source = Some(source_index); } if let Some(io) = parse_io(raw.io.as_ref(), path)? { diff --git a/codetracer-python-recorder/src/trace_filter/model.rs b/codetracer-python-recorder/src/trace_filter/model.rs index c685f15..2bcf10a 100644 --- a/codetracer-python-recorder/src/trace_filter/model.rs +++ b/codetracer-python-recorder/src/trace_filter/model.rs @@ -137,6 +137,7 @@ pub struct FilterSummaryEntry { pub struct TraceFilterConfig { pub(crate) default_exec: ExecDirective, pub(crate) default_value_action: ValueAction, + pub(crate) default_value_source: usize, pub(crate) io: IoConfig, pub(crate) rules: Vec, pub(crate) sources: Vec, @@ -153,6 +154,11 @@ impl TraceFilterConfig { self.default_value_action } + /// Source index of the definition that last set the default value action. + pub fn default_value_source(&self) -> usize { + self.default_value_source + } + /// IO capture configuration associated with the composed filter chain. pub fn io(&self) -> &IoConfig { &self.io