ralph_workflow/cloud/
io_redaction.rs1fn bearer_token_re() -> regex::Regex {
2 regex::Regex::new(r"(?i)(bearer\s+)\S+").expect("valid regex")
3}
4
5fn common_query_re() -> regex::Regex {
6 const KEYS: [&str; 5] = [
7 "access_token=",
8 "token=",
9 "password=",
10 "passwd=",
11 "oauth_token=",
12 ];
13 let pattern = format!("(?i)({})([^&\\s]*)", KEYS.join("|"));
14 regex::Regex::new(&pattern).expect("valid regex")
15}
16
17fn token_like_re() -> regex::Regex {
18 const PREFIXES: [&str; 6] = ["ghp_", "github_pat_", "glpat-", "xoxb-", "xapp-", "ya29."];
19 let pattern = format!(
20 "({})[A-Za-z0-9_\\-\\.]+",
21 PREFIXES
22 .iter()
23 .map(|&s| regex::escape(s))
24 .collect::<Vec<_>>()
25 .join("|")
26 );
27 regex::Regex::new(&pattern).expect("valid regex")
28}
29
30pub fn redact_bearer_tokens(input: &str) -> String {
31 bearer_token_re()
32 .replace_all(input, "$1<redacted>")
33 .to_string()
34}
35
36pub fn redact_common_query_params(input: &str) -> String {
37 common_query_re()
38 .replace_all(input, |caps: ®ex::Captures| {
39 let key = caps.get(1).map_or("", |m| m.as_str());
40 format!("{}<redacted>", key)
41 })
42 .to_string()
43}
44
45pub fn redact_token_like_substrings(input: &str) -> String {
46 token_like_re().replace_all(input, "<redacted>").to_string()
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
56 fn redact_bearer_tokens_replaces_token_value() {
57 let input = "Authorization: Bearer abc123xyz";
58 let result = redact_bearer_tokens(input);
59 assert!(result.contains("Bearer <redacted>"), "got: {result}");
60 assert!(!result.contains("abc123xyz"), "token should be redacted");
61 }
62
63 #[test]
64 fn redact_bearer_tokens_case_insensitive() {
65 let input = "authorization: bearer SECRET_TOKEN";
66 let result = redact_bearer_tokens(input);
67 assert!(result.contains("<redacted>"), "got: {result}");
68 assert!(!result.contains("SECRET_TOKEN"), "token should be redacted");
69 }
70
71 #[test]
72 fn redact_bearer_tokens_no_match_unchanged() {
73 let input = "Hello, no tokens here.";
74 assert_eq!(redact_bearer_tokens(input), input);
75 }
76
77 #[test]
80 fn redact_common_query_params_replaces_access_token() {
81 let input = "https://example.com/api?access_token=supersecret&foo=bar";
82 let result = redact_common_query_params(input);
83 assert!(result.contains("access_token=<redacted>"), "got: {result}");
84 assert!(!result.contains("supersecret"), "value should be redacted");
85 assert!(
86 result.contains("foo=bar"),
87 "unrelated param should be untouched"
88 );
89 }
90
91 #[test]
92 fn redact_common_query_params_replaces_password() {
93 let input = "https://example.com/api?password=hunter2";
94 let result = redact_common_query_params(input);
95 assert!(result.contains("password=<redacted>"), "got: {result}");
96 assert!(!result.contains("hunter2"), "password should be redacted");
97 }
98
99 #[test]
100 fn redact_common_query_params_no_match_unchanged() {
101 let input = "https://example.com/api?foo=bar&baz=qux";
102 assert_eq!(redact_common_query_params(input), input);
103 }
104
105 #[test]
108 fn redact_token_like_substrings_replaces_github_pat() {
109 let input = "token=ghp_AbCdEfGhIjKlMnOpQrStUvWxYz012345";
110 let result = redact_token_like_substrings(input);
111 assert!(result.contains("<redacted>"), "got: {result}");
112 assert!(
113 !result.contains("ghp_AbCdEfGhIjKlMnOpQrStUvWxYz012345"),
114 "PAT should be redacted"
115 );
116 }
117
118 #[test]
119 fn redact_token_like_substrings_replaces_gitlab_token() {
120 let input = "CI_TOKEN=glpat-xyz_abc123";
121 let result = redact_token_like_substrings(input);
122 assert!(result.contains("<redacted>"), "got: {result}");
123 assert!(
124 !result.contains("glpat-xyz_abc123"),
125 "GitLab PAT should be redacted"
126 );
127 }
128
129 #[test]
130 fn redact_token_like_substrings_no_match_unchanged() {
131 let input = "nothing sensitive here";
132 assert_eq!(redact_token_like_substrings(input), input);
133 }
134}