Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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).*'
Expand Down
91 changes: 91 additions & 0 deletions codetracer-python-recorder/src/runtime/tracer/runtime_tracer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ mod tests {
static LAST_OUTCOME: Cell<Option<CallbackOutcome>> = Cell::new(None);
}

const BUILTIN_TRACE_FILTER: &str =
include_str!("../../../resources/trace_filters/builtin_default.toml");

struct ScopedTracer;

impl ScopedTracer {
Expand Down Expand Up @@ -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<String> = 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| {
Expand Down
27 changes: 24 additions & 3 deletions codetracer-python-recorder/src/trace_filter/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ pub struct TraceFilterEngine {
config: Arc<TraceFilterConfig>,
default_exec: ExecDecision,
default_value_action: ValueAction,
default_value_source: usize,
rules: Arc<[CompiledScopeRule]>,
cache: DashMap<usize, Arc<ScopeResolution>>,
}
Expand All @@ -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(),
}
Expand Down Expand Up @@ -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;
Expand All @@ -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<CompiledValuePattern> = 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 {
Expand Down
9 changes: 9 additions & 0 deletions codetracer-python-recorder/src/trace_filter/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use std::path::{Component, Path, PathBuf};
pub struct ConfigAggregator {
default_exec: Option<ExecDirective>,
default_value_action: Option<ValueAction>,
default_value_source: Option<usize>,
io: Option<IoConfig>,
rules: Vec<ScopeRule>,
sources: Vec<FilterSource>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)? {
Expand Down
6 changes: 6 additions & 0 deletions codetracer-python-recorder/src/trace_filter/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScopeRule>,
pub(crate) sources: Vec<FilterSource>,
Expand All @@ -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
Expand Down
Loading