Skip to main content

ferro_cli/deploy/
secret_keys.rs

1//! Secret-shaped env key classifier (D-08).
2//!
3//! Case-insensitive substring match against a fixed vocabulary. Keys ending
4//! in `_URL` are non-secret UNLESS they also match another substring hit
5//! (e.g. `DATABASE_URL` → false, `STRIPE_SECRET_URL` → true).
6//!
7//! Known behavior (documented, not a bug for this phase): plain webhook URL
8//! envs like `SLACK_WEBHOOK_URL` classify as non-secret because the `_URL`
9//! carve-out trumps the lack of other substring hits. Extending the
10//! heuristic is out of scope for Phase 127.
11
12const SECRET_SUBSTRINGS: &[&str] = &[
13    "secret",
14    "password",
15    "passwd",
16    "token",
17    "key",
18    "api_key",
19    "dsn",
20    "private",
21    "credential",
22];
23
24/// Classify an env var key as secret-shaped per D-08.
25pub fn is_secret_key(key: &str) -> bool {
26    let lower = key.to_ascii_lowercase();
27    let non_url_hit = SECRET_SUBSTRINGS.iter().any(|n| lower.contains(n));
28    if lower.ends_with("_url") {
29        // _URL carve-out: non-secret unless another substring matches the
30        // portion before the trailing `_url`.
31        let head = &lower[..lower.len() - 4];
32        return SECRET_SUBSTRINGS.iter().any(|n| head.contains(n));
33    }
34    non_url_hit
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    #[test]
42    fn is_secret_key_stripe_secret_key() {
43        assert!(is_secret_key("STRIPE_SECRET_KEY"));
44    }
45    #[test]
46    fn is_secret_key_api_token() {
47        assert!(is_secret_key("API_TOKEN"));
48    }
49    #[test]
50    fn is_secret_key_db_password() {
51        assert!(is_secret_key("DB_PASSWORD"));
52    }
53    #[test]
54    fn is_secret_key_passwd() {
55        assert!(is_secret_key("USER_PASSWD"));
56    }
57    #[test]
58    fn is_secret_key_dsn() {
59        assert!(is_secret_key("SENTRY_DSN"));
60    }
61    #[test]
62    fn is_secret_key_private() {
63        assert!(is_secret_key("PRIVATE_RSA"));
64    }
65    #[test]
66    fn is_secret_key_credential() {
67        assert!(is_secret_key("DB_CREDENTIAL"));
68    }
69    #[test]
70    fn is_secret_key_api_key() {
71        assert!(is_secret_key("MY_API_KEY"));
72    }
73    #[test]
74    fn is_secret_key_database_url_carve_out() {
75        assert!(!is_secret_key("DATABASE_URL"));
76    }
77    #[test]
78    fn is_secret_key_redis_url_carve_out() {
79        assert!(!is_secret_key("REDIS_URL"));
80    }
81    #[test]
82    fn is_secret_key_stripe_secret_url_still_secret() {
83        assert!(is_secret_key("STRIPE_SECRET_URL"));
84    }
85    #[test]
86    fn is_secret_key_app_name_not_secret() {
87        assert!(!is_secret_key("APP_NAME"));
88    }
89}