workerlib/
redact.rs

1//!
2//! Helper utility that can redact sensitive strings common in Lorry
3//!
4
5use fancy_regex::Regex;
6use std::fmt::Display;
7use std::sync::OnceLock;
8
9fn get_expressions() -> &'static Vec<Regex> {
10    static EXPRESSIONS: OnceLock<Vec<Regex>> = OnceLock::new();
11    EXPRESSIONS.get_or_init(|| {
12        vec![
13            // List of token patterns referenced from https://docs.gitlab.com/18.7/security/tokens/#token-prefixes
14            Regex::new(
15                r##"gl(pat|oas|dt|rt|rtr|cbt|ptt|ft|imt|agent|wt|soat|ffct)-[0-9a-zA-Z\-_]*"##,
16            )
17            .unwrap(),
18            // List of token patterns referenced from
19            // https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github#githubs-token-formats
20            Regex::new(r##"g(hp|ithub_pat|ho|hu|hs|hr)_[0-9a-zA-Z\-_]*"##).unwrap(),
21        ]
22    })
23}
24
25/// Removes any known sensitive strings from the input and replaces them
26/// with LORRY_REDACTED
27pub fn redact(input: &impl Display) -> String {
28    let mut copy = input.to_string();
29    for expr in get_expressions() {
30        copy = expr.replace_all(&copy, "LORRY_REDACTED").to_string();
31    }
32
33    static GENERAL_EXPRESSION: OnceLock<Regex> = OnceLock::new();
34    let general_expression = GENERAL_EXPRESSION.get_or_init(|| {
35        Regex::new(
36            r##"(?<protocol>https?://)(?<username>.+?):(?<password>.+?)@(?<address>[\w\-\./]+)"##,
37        )
38        .unwrap()
39    });
40
41    if let Ok(Some(caps)) = general_expression.captures(&copy) {
42        let group = caps.get(3).unwrap(); // the "password" group
43        copy = format!(
44            "{}{}{}",
45            &copy[..group.start()],
46            "LORRY_REDACTED",
47            &copy[group.end()..]
48        )
49    }
50
51    copy
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    struct TestCase<'a> {
59        pub input: &'a str,
60        pub expected: &'a str,
61    }
62
63    impl TestCase<'_> {
64        pub fn check(&self) {
65            let result = redact(&self.input.to_string());
66            assert!(result == self.expected);
67        }
68    }
69
70    #[test]
71    fn test_redact() {
72        TestCase {
73            input: "glpat-Y38-kU_3pviux6H1D9ec",
74            expected: "LORRY_REDACTED",
75        }
76        .check();
77        TestCase {
78            input: "glpat-AdmAbJdT-FMYPa_2bQyy",
79            expected: "LORRY_REDACTED",
80        }
81        .check();
82        TestCase {
83            input: "glpat-MjW2FqijnvVBs7yPEoDG",
84            expected: "LORRY_REDACTED",
85        }
86        .check();
87        TestCase {
88            input: "Using git binary to push remote: http://oauth2:glpat-25HjHgcpRJsY_JQweJYa@localhost:9999/lorry-mirrors/test/lorry.git",
89            expected: "Using git binary to push remote: http://oauth2:LORRY_REDACTED@localhost:9999/lorry-mirrors/test/lorry.git",
90        }.check();
91        TestCase {
92            input: "Using git binary to push remote: http://oauth2:this_is_a_random_prefix-25HjHgcpRJsY_JQweJYa@localhost:9999/lorry-mirrors/test/lorry.git",
93            expected: "Using git binary to push remote: http://oauth2:LORRY_REDACTED@localhost:9999/lorry-mirrors/test/lorry.git",
94        }.check();
95    }
96}