use std::path::Path;
const SECRET_FILENAMES: &[&str] = &[
".env",
".envrc",
".netrc",
".pgpass",
".npmrc",
".yarnrc",
"id_rsa",
"id_dsa",
"id_ecdsa",
"id_ed25519",
"credentials",
"credentials.json",
"service-account.json",
"service_account.json",
"secrets.json",
"secret.json",
"private.key",
"auth.json",
".htpasswd",
"wallet.json",
];
const SECRET_FILENAME_PREFIXES: &[&str] = &[".env.", "secret.", "secrets.", "credential"];
const SECRET_EXTENSIONS: &[&str] = &[
"pem", "key", "crt", "cer", "der", "p12", "pfx", "p7b", "p7c", "kdbx", "kdb", "gpg", "asc",
"pgp", "jks", "keystore", "ovpn", "ppk",
];
const SECRET_PATH_SEGMENTS: &[&str] = &[
".aws", ".ssh", ".gnupg", ".gcloud", ".azure", ".kube", "secrets", "private",
];
pub fn is_secret_path(path: &Path) -> bool {
let s = path.to_string_lossy();
for seg in SECRET_PATH_SEGMENTS {
let mid = format!("/{}/", seg);
let end = format!("/{}", seg);
let win_mid = format!("\\{}\\", seg);
let win_end = format!("\\{}", seg);
if s.contains(&mid) || s.ends_with(&end) || s.contains(&win_mid) || s.ends_with(&win_end) {
return true;
}
}
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_lowercase(),
None => return false,
};
if SECRET_FILENAMES.contains(&name.as_str()) {
return true;
}
for pre in SECRET_FILENAME_PREFIXES {
if name.starts_with(pre) {
return true;
}
}
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
let ext_lower = ext.to_lowercase();
if SECRET_EXTENSIONS.contains(&ext_lower.as_str()) {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn detects_dotenv_family() {
assert!(is_secret_path(&PathBuf::from("project/.env")));
assert!(is_secret_path(&PathBuf::from("project/.env.local")));
assert!(is_secret_path(&PathBuf::from("project/.env.production")));
assert!(is_secret_path(&PathBuf::from("project/.envrc")));
}
#[test]
fn detects_credential_files() {
assert!(is_secret_path(&PathBuf::from("project/credentials.json")));
assert!(is_secret_path(&PathBuf::from(
"project/service-account.json"
)));
assert!(is_secret_path(&PathBuf::from("project/secrets.json")));
assert!(is_secret_path(&PathBuf::from(
"project/credential-store.txt"
)));
}
#[test]
fn detects_key_extensions() {
assert!(is_secret_path(&PathBuf::from("project/cert.pem")));
assert!(is_secret_path(&PathBuf::from("project/api.key")));
assert!(is_secret_path(&PathBuf::from("project/keystore.jks")));
assert!(is_secret_path(&PathBuf::from("project/wallet.kdbx")));
}
#[test]
fn detects_path_segments() {
assert!(is_secret_path(&PathBuf::from("/Users/me/.ssh/id_rsa")));
assert!(is_secret_path(&PathBuf::from("/Users/me/.aws/credentials")));
assert!(is_secret_path(&PathBuf::from(
"/home/me/proj/secrets/api.toml"
)));
assert!(is_secret_path(&PathBuf::from("/home/me/proj/private/key")));
}
#[test]
fn case_insensitive() {
assert!(is_secret_path(&PathBuf::from("PROJECT/.ENV.LOCAL")));
assert!(is_secret_path(&PathBuf::from("project/CERT.PEM")));
}
#[test]
fn allows_normal_files() {
assert!(!is_secret_path(&PathBuf::from("src/main.rs")));
assert!(!is_secret_path(&PathBuf::from("README.md")));
assert!(!is_secret_path(&PathBuf::from("package.json")));
assert!(!is_secret_path(&PathBuf::from("Cargo.toml")));
assert!(!is_secret_path(&PathBuf::from(
"src/components/SecretReveal.tsx"
))); }
#[test]
fn windows_separators() {
assert!(is_secret_path(&PathBuf::from(r"C:\Users\me\.ssh\id_rsa")));
}
}