| 86 | 86 | | || value == "…" |
| 87 | 87 | | } |
| 88 | 88 | | |
| 89 | + | /// `secret: the` in a sentence: after a colon, an unquoted plain word |
| 90 | + | /// short of a password's length is the sentence going on, not a value. |
| 91 | + | /// `password: hunter2!` (a digit, a symbol) and a long run still count; |
| 92 | + | /// after `=` everything does. |
| 93 | + | fn prose(head: &str, value: &str) -> bool { |
| 94 | + | head.trim_end().ends_with(':') |
| 95 | + | && !value.starts_with(['"', '\'', '`']) |
| 96 | + | && value.chars().count() < 16 |
| 97 | + | && value.chars().all(|c| c.is_alphabetic() || matches!(c, '\'' | '’' | '-')) |
| 98 | + | } |
| 99 | + | |
| 89 | 100 | | fn shannon(text: &str) -> f64 { |
| 90 | 101 | | let mut counts = std::collections::HashMap::new(); |
| 91 | 102 | | for c in text.chars() { |
| ⋯ |
| 157 | 168 | | let value = &c[2]; |
| 158 | 169 | | // `token: string` in code is a type, not a token. |
| 159 | 170 | | let typed = matches!(value, "string" | "String" | "str" | "&str" | "number" | "bool" | "boolean" | "null" | "None" | "undefined" | "true" | "false" | "Option<String>"); |
| 160 | | − | if reference(value) || typed { c[0].to_owned() } else { format!("{}{REDACTED}", &c[1]) } |
| 171 | + | if reference(value) || typed || prose(&c[1], value) { c[0].to_owned() } else { format!("{}{REDACTED}", &c[1]) } |
| 161 | 172 | | }); |
| 162 | 173 | | let out = LONG.replace_all(&out, |c: &Captures| { |
| 163 | 174 | | if random_looking(&c[0]) { REDACTED.to_owned() } else { c[0].to_owned() } |
| ⋯ |
| 240 | 251 | | } |
| 241 | 252 | | |
| 242 | 253 | | #[test] |
| 254 | + | fn a_sentence_after_a_colon_is_not_a_value() { |
| 255 | + | for ordinary in [ |
| 256 | + | "A path is not a secret: the redactor's rule read it as one", |
| 257 | + | "The API key: what it is and where to keep it", |
| 258 | + | "password: the one you set at sign-up", |
| 259 | + | ] { |
| 260 | + | assert_eq!(redact(ordinary), ordinary); |
| 261 | + | } |
| 262 | + | hidden("password: hunter2", "hunter2"); |
| 263 | + | hidden("db_password: correcthorsebatterystaple", "correcthorsebatterystaple"); |
| 264 | + | hidden("api_key: 'plainword'", "plainword"); |
| 265 | + | hidden("SECRET=plainword", "plainword"); |
| 266 | + | } |
| 267 | + | |
| 268 | + | #[test] |
| 243 | 269 | | fn references_and_types_are_not_secrets() { |
| 244 | 270 | | let line = "TOKEN=$G1T_TOKEN; token: ${{ secrets.DEPLOY }}; password: process.env.PW; fn f(token: String)"; |
| 245 | 271 | | assert_eq!(redact(line), line); |