use std::sync::Arc;
use talon_core::env::{EnvFile, current_key, key_spellings, parse_env};
use talon_security::redact::Redactor;
pub const KNOWN_SECRET_KEYS: &[&str] = &[
"TALAHON_VLLM_API_KEY",
"TALAHON_ANTHROPIC_API_KEY",
"HTTP_API_KEY",
"TELEGRAM_BOT_TOKEN",
"WEBHOOK_SECRET",
"EMACS_AUTH_TOKEN",
"TALAHON_AUDIT_KEY",
"ONECLI_API_KEY",
"ANTHROPIC_API_KEY",
"CLAUDE_CODE_OAUTH_TOKEN",
"OPENAI_API_KEY",
];
const SECRET_KEY_MARKERS: &[&str] = &[
"KEY",
"TOKEN",
"SECRET",
"PASSWORD",
"PASSWD",
"CREDENTIAL",
"PRIVATE",
];
pub fn known_secret_key_spellings() -> Vec<String> {
KNOWN_SECRET_KEYS
.iter()
.flat_map(|key| key_spellings(key))
.collect()
}
pub fn is_secret_key(key: &str) -> bool {
let upper = key.to_ascii_uppercase();
let current = current_key(&upper);
KNOWN_SECRET_KEYS.contains(&upper.as_str())
|| current
.as_deref()
.is_some_and(|k| KNOWN_SECRET_KEYS.contains(&k))
|| SECRET_KEY_MARKERS.iter().any(|m| upper.contains(m))
}
fn looks_like_secret_value(value: &str) -> bool {
let v = value.trim();
!v.is_empty()
&& !v.chars().all(|c| c.is_ascii_digit() || c == '.')
&& !matches!(
v.to_ascii_lowercase().as_str(),
"true" | "false" | "yes" | "no" | "on" | "off"
)
}
pub fn collect_secrets(env: &EnvFile) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
if let Some(path) = env.path()
&& let Ok(content) = std::fs::read_to_string(path)
{
out.extend(
parse_env(&content)
.into_iter()
.filter(|(k, _)| is_secret_key(k))
.map(|(_, v)| v),
);
}
for key in known_secret_key_spellings() {
if let Some(v) = env.file_value(&key) {
out.push(v.to_string());
}
if let Ok(v) = std::env::var(&key) {
out.push(v);
}
}
out.retain(|v| looks_like_secret_value(v));
out.sort();
out.dedup();
out
}
pub fn redactor_for(env: &EnvFile) -> Arc<Redactor> {
Arc::new(Redactor::with_secrets(&collect_secrets(env)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn secret_key_detection() {
assert!(is_secret_key("TALAHON_VLLM_API_KEY"));
assert!(is_secret_key("HTTP_API_KEY"));
assert!(is_secret_key(talon_db::AUDIT_KEY_ENV));
assert!(KNOWN_SECRET_KEYS.contains(&talon_db::AUDIT_KEY_ENV));
assert!(is_secret_key("MYSQL_PASSWORD"));
assert!(is_secret_key("some_token"));
assert!(!is_secret_key("TALAHON_VLLM_MODEL"));
assert!(!is_secret_key("TZ"));
}
#[test]
fn collects_secret_values_from_env_file() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".env"),
"TALAHON_VLLM_API_KEY=vllm-secret-123456\nHTTP_API_KEY=http-secret-abcdef\n\
TALAHON_VLLM_MODEL=qwen\nCUSTOM_DB_PASSWORD=hunter2hunter2\n\
TALAHON_VLLM_MAX_TOKENS=131072\nSOME_TOKEN_REQUIRED=true\n\
TALAHON_AUDIT_KEY=4c1f0e9a7b3d5c2e8f6a1b0d9c7e5f3a2b4d6c8e0f1a3b5c7d9e1f2a4b6c8d0e\n",
)
.unwrap();
let env = EnvFile::load(dir.path());
let secrets = collect_secrets(&env);
assert!(secrets.contains(&"vllm-secret-123456".to_string()));
assert!(secrets.contains(&"http-secret-abcdef".to_string()));
assert!(secrets.contains(&"hunter2hunter2".to_string()));
assert!(!secrets.contains(&"qwen".to_string()));
assert!(
!secrets.contains(&"131072".to_string()),
"numbers are not secrets"
);
assert!(!secrets.contains(&"true".to_string()));
let redactor = redactor_for(&env);
let out = redactor.redact("key is vllm-secret-123456 and model qwen");
assert!(!out.contains("vllm-secret-123456"));
assert!(out.contains("qwen"));
let audit_key = "4c1f0e9a7b3d5c2e8f6a1b0d9c7e5f3a2b4d6c8e0f1a3b5c7d9e1f2a4b6c8d0e";
assert!(secrets.contains(&audit_key.to_string()));
let line = format!("audit key {audit_key} leaked");
let out = redactor.redact(&line);
assert!(!out.contains(audit_key), "{out}");
}
#[test]
fn secrets_under_the_old_key_names_are_still_redacted() {
let env = EnvFile::from_str_content(
"TALON_VLLM_API_KEY=old-vllm-secret-123456\n\
TALON_ANTHROPIC_API_KEY=old-anthropic-secret-1\n\
TALAHON_ANTHROPIC_API_KEY=new-anthropic-secret-2\n\
TALON_AUDIT_KEY=old-audit-key-material-000\n",
);
assert!(is_secret_key("TALON_VLLM_API_KEY"));
assert!(is_secret_key("TALON_AUDIT_KEY"));
assert!(known_secret_key_spellings().contains(&"TALON_AUDIT_KEY".to_string()));
assert!(known_secret_key_spellings().contains(&"TALAHON_AUDIT_KEY".to_string()));
assert!(!known_secret_key_spellings().contains(&"TALON_HTTP_API_KEY".to_string()));
let secrets = collect_secrets(&env);
for secret in [
"old-vllm-secret-123456",
"old-anthropic-secret-1",
"new-anthropic-secret-2",
"old-audit-key-material-000",
] {
assert!(secrets.contains(&secret.to_string()), "{secret}");
}
let redactor = redactor_for(&env);
let out = redactor.redact("leak old-vllm-secret-123456 and old-anthropic-secret-1");
assert!(!out.contains("old-vllm-secret-123456"), "{out}");
assert!(!out.contains("old-anthropic-secret-1"), "{out}");
}
}