fn assigned_value_after_key(after_key: &str) -> Option<&str> {
let trimmed = after_key.trim_start();
if let Some(value) = trimmed.strip_prefix('=') {
return Some(value.trim_start());
}
let after_colon = trimmed.strip_prefix(':')?.trim_start();
if let Some((_, value)) = after_colon.split_once('=') {
return Some(value.trim_start());
}
Some(after_colon)
}
fn assigned_secret_value_for_key<'a>(line: &'a str, lower_line: &str, key: &str) -> Option<&'a str> {
let mut search_start = 0;
while let Some(offset) = lower_line[search_start..].find(key) {
let key_start = search_start + offset;
let after_key = &line[key_start + key.len()..];
if let Some(value) = assigned_value_after_key(after_key)
&& is_secret_literal(value)
{
return Some(value);
}
search_start = key_start + key.len();
}
None
}
fn is_secret_literal(value: &str) -> bool {
let value = value.trim().trim_end_matches([',', ';']).trim();
let unquoted = value.trim_matches('"').trim_matches('\'');
let lower = unquoted.to_lowercase();
let is_placeholder = unquoted.is_empty()
|| is_env_var_reference(unquoted)
|| unquoted.starts_with("${")
|| unquoted.starts_with("{{")
|| unquoted.starts_with('<')
|| unquoted.starts_with('[')
|| unquoted.ends_with('…')
|| unquoted.ends_with("...")
|| looks_like_placeholder_value(unquoted)
|| matches!(
lower.as_str(),
"null"
| "nil"
| "none"
| "your_key_here"
| "short"
| "password"
| "test-password"
| "test_password"
| "dummy"
| "example"
| "changeme"
| "replace_me"
| "placeholder"
| "todo"
| "xxx"
| "your_secret"
| "your_token"
| "your_api_key"
| "insert_here"
| "set_me"
| "fixme"
);
const NON_SECRET_VALUES: &[&str] = &[
"bearer", "rs256", "hs256", "es256", "rs512", "hs512", "es512", "sha256", "sha512", "md5",
"aes", "aes256", "rsa", "hmac", "basic", "digest", "oauth", "oauth2", "true", "false",
];
if is_placeholder || NON_SECRET_VALUES.contains(&lower.as_str()) || unquoted.len() < 8 {
return false;
}
if shannon_entropy(unquoted) < 3.0 {
return false;
}
if value.starts_with('"') || value.starts_with('\'') {
return true;
}
is_unquoted_secret_token(unquoted)
}
fn looks_like_placeholder_value(value: &str) -> bool {
let lower = value.trim().to_ascii_lowercase();
if lower.is_empty() {
return true;
}
if lower.starts_with("replace-with-")
|| lower.starts_with("replace_with_")
|| lower.starts_with("your-")
|| lower.starts_with("your_")
|| lower.starts_with("example-")
|| lower.starts_with("example_")
|| lower.starts_with("sample-")
|| lower.starts_with("sample_")
|| lower.starts_with("fake-")
|| lower.starts_with("fake_")
|| lower.starts_with("dummy-")
|| lower.starts_with("dummy_")
|| lower.starts_with("placeholder-")
|| lower.starts_with("placeholder_")
{
return true;
}
let compact = compact_secret_name(&lower);
if compact.is_empty() {
return true;
}
const PLACEHOLDER_PREFIXES: &[&str] = &[
"your",
"replacewith",
"replace",
"example",
"sample",
"dummy",
"fake",
"mock",
"demo",
"placeholder",
"insert",
"set",
];
PLACEHOLDER_PREFIXES
.iter()
.any(|prefix| compact.starts_with(prefix) && looks_like_secret_name(&compact))
}
fn is_unquoted_secret_token(value: &str) -> bool {
if value.chars().any(|c| {
c.is_whitespace() || matches!(c, '(' | ')' | '[' | ']' | '{' | '}' | '?' | '<' | '>')
}) {
return false;
}
if has_known_secret_prefix(value) {
return true;
}
if looks_like_code_reference(value) {
return false;
}
let has_letter = value.chars().any(|c| c.is_ascii_alphabetic());
let has_digit = value.chars().any(|c| c.is_ascii_digit());
let has_symbol = value.chars().any(|c| !c.is_ascii_alphanumeric());
value.len() >= 16 && has_letter && (has_digit || has_symbol)
}
fn is_env_var_reference(value: &str) -> bool {
let value = value.trim();
if let Some(name) = value.strip_prefix('$') {
let name = name
.strip_prefix('{')
.and_then(|name| name.strip_suffix('}'))
.unwrap_or(name);
return !name.is_empty()
&& name
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
}
false
}
fn has_known_secret_prefix(value: &str) -> bool {
let lower = value.to_ascii_lowercase();
lower.starts_with("sk-")
|| lower.starts_with("sk_")
|| lower.starts_with("sk_live_")
|| lower.starts_with("sk_test_")
|| lower.starts_with("ghp_")
|| lower.starts_with("gho_")
|| lower.starts_with("ghu_")
|| lower.starts_with("ghs_")
|| lower.starts_with("github_pat_")
|| lower.starts_with("xoxb-")
|| lower.starts_with("xoxp-")
|| lower.starts_with("ya29.")
|| value.starts_with("AKIA")
|| value.starts_with("ASIA")
|| value.starts_with("AIza")
}
fn looks_like_code_reference(value: &str) -> bool {
let value = value
.trim_start_matches('&')
.trim_start_matches('*')
.trim_start_matches('$');
let has_path_separator = value.contains('.') || value.contains("::");
let parts: Vec<&str> = if value.contains("::") {
value.split("::").collect()
} else {
value.split('.').collect()
};
if parts.is_empty() || !parts.iter().all(|part| is_identifier(part)) {
return false;
}
if has_path_separator || looks_like_secret_name(value) || value.contains('_') {
return true;
}
let has_digit = value.chars().any(|c| c.is_ascii_digit());
let is_lowercase_word = value
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit());
let is_uppercase_name = value
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_');
!has_digit && (is_lowercase_word || is_uppercase_name)
}
fn is_identifier(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
(first.is_ascii_alphabetic() || first == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn looks_like_secret_name(value: &str) -> bool {
let compact_value = compact_secret_name(value);
SECRET_KEYS.iter().any(|key| {
let compact_key = compact_secret_name(key);
compact_value.contains(&compact_key)
})
}
fn compact_secret_name(value: &str) -> String {
value
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.flat_map(char::to_lowercase)
.collect()
}
fn shannon_entropy(s: &str) -> f64 {
if s.is_empty() {
return 0.0;
}
let len = s.len() as f64;
let mut freq = [0u32; 256];
for &b in s.as_bytes() {
freq[b as usize] += 1;
}
freq.iter()
.filter(|&&c| c > 0)
.map(|&c| {
let p = c as f64 / len;
-p * p.log2()
})
.sum()
}