context_harness/
redact.rs1use toml::Value;
16
17pub const REDACTED: &str = "[REDACTED]";
19
20const SAFE_KEYS: &[&str] = &[
22 "path",
23 "root",
24 "branch",
25 "region",
26 "bucket",
27 "prefix",
28 "include_globs",
29 "exclude_globs",
30 "shallow",
31 "follow_symlinks",
32 "max_extract_bytes",
33 "timeout",
34 "timeout_secs",
35 "enabled",
36 "name",
37 "dims",
38 "batch_size",
39 "max_retries",
40 "provider",
41 "model",
42 "metric",
43 "backend",
44 "index",
45 "fallback",
46];
47
48const URL_KEYS: &[&str] = &["url", "endpoint_url", "endpoint", "host"];
50
51pub fn redact_url(value: &str) -> String {
58 let Some(scheme_end) = value.find("://") else {
59 return value.to_string();
60 };
61 let auth_start = scheme_end + 3;
62 let rest = &value[auth_start..];
63 let auth_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
64 let authority = &rest[..auth_end];
65 match authority.rfind('@') {
66 Some(at) => {
67 let host = &authority[at + 1..];
68 format!(
69 "{}{}@{}{}",
70 &value[..auth_start],
71 REDACTED,
72 host,
73 &rest[auth_end..]
74 )
75 }
76 None => value.to_string(),
77 }
78}
79
80pub fn redact_connector_map(table: &toml::value::Table) -> toml::value::Table {
87 table
88 .iter()
89 .map(|(k, v)| (k.clone(), redact_value(k, v)))
90 .collect()
91}
92
93fn redact_value(key: &str, value: &Value) -> Value {
94 let lk = key.to_ascii_lowercase();
95 match value {
96 Value::Table(t) => Value::Table(redact_connector_map(t)),
97 Value::Array(a) => Value::Array(a.iter().map(|v| redact_value(key, v)).collect()),
98 Value::String(s) if URL_KEYS.contains(&lk.as_str()) => Value::String(redact_url(s)),
99 _ if SAFE_KEYS.contains(&lk.as_str()) => value.clone(),
100 _ => Value::String(REDACTED.to_string()),
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn redacts_userinfo_from_https_url() {
110 assert_eq!(
111 redact_url("https://user:ghp_secrettoken@github.com/acme/platform.git"),
112 "https://[REDACTED]@github.com/acme/platform.git"
113 );
114 }
115
116 #[test]
117 fn redacts_token_only_userinfo() {
118 assert_eq!(
119 redact_url("https://x-access-token:abc123@github.com/o/r.git"),
120 "https://[REDACTED]@github.com/o/r.git"
121 );
122 }
123
124 #[test]
125 fn leaves_clean_url_unchanged() {
126 let url = "https://github.com/acme/platform.git";
127 assert_eq!(redact_url(url), url);
128 }
129
130 #[test]
131 fn leaves_scp_like_and_bare_paths_unchanged() {
132 assert_eq!(
134 redact_url("git@github.com:acme/platform.git"),
135 "git@github.com:acme/platform.git"
136 );
137 assert_eq!(
138 redact_url("/srv/repos/platform.git"),
139 "/srv/repos/platform.git"
140 );
141 }
142
143 #[test]
144 fn preserves_query_and_port() {
145 assert_eq!(
146 redact_url("http://user:pw@localhost:9000/bucket?x=1"),
147 "http://[REDACTED]@localhost:9000/bucket?x=1"
148 );
149 }
150
151 #[test]
152 fn connector_map_is_deny_by_default() {
153 let mut t = toml::value::Table::new();
154 t.insert(
155 "base_url".into(),
156 Value::String("https://svc.example.com".into()),
157 );
158 t.insert(
159 "url".into(),
160 Value::String("https://u:p@git.example.com/x".into()),
161 );
162 t.insert(
163 "api_token".into(),
164 Value::String("${JIRA_API_TOKEN}".into()),
165 );
166 t.insert("project".into(), Value::String("ENG".into()));
167 t.insert("path".into(), Value::String("/scripts/jira.lua".into()));
168
169 let r = redact_connector_map(&t);
170 assert_eq!(
172 r["url"].as_str().unwrap(),
173 "https://[REDACTED]@git.example.com/x"
174 );
175 assert_eq!(r["path"].as_str().unwrap(), "/scripts/jira.lua");
177 assert_eq!(r["api_token"].as_str().unwrap(), REDACTED);
179 assert_eq!(r["project"].as_str().unwrap(), REDACTED);
180 assert_eq!(r["base_url"].as_str().unwrap(), REDACTED);
182 }
183}