Skip to main content

ralph_workflow/cloud/
io_redaction.rs

1fn 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: &regex::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    // --- redact_bearer_tokens ---
54
55    #[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    // --- redact_common_query_params ---
78
79    #[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    // --- redact_token_like_substrings ---
106
107    #[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}