use super::{CONFIDENCE_MAX, CONFIDENCE_MIN};
#[derive(serde::Deserialize)]
struct ExamplePathComponents {
components: Vec<String>,
}
fn parse_fixture_path_components(raw: &str) -> Result<Vec<String>, String> {
toml::from_str::<ExamplePathComponents>(raw)
.map(|parsed| parsed.components)
.map_err(|error| error.to_string())
}
static FIXTURE_PATH_COMPONENTS: std::sync::LazyLock<Vec<String>> = std::sync::LazyLock::new(|| {
match parse_fixture_path_components(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/rules/example-path-components.toml"
))) {
Ok(components) => components,
Err(error) => panic!(
"rules/example-path-components.toml is invalid: {error}. \
Fix the bundled Tier-B example path components list."
),
}
});
#[inline]
pub(crate) fn finalize_confidence(score: f64) -> f64 {
if score.is_nan() {
return CONFIDENCE_MIN;
}
score.clamp(CONFIDENCE_MIN, CONFIDENCE_MAX)
}
pub(crate) fn contains_placeholder_word(credential: &str) -> bool {
crate::placeholder_words::contains_placeholder_word(credential)
}
fn has_credential_url_userinfo_without_placeholder(credential: &str) -> bool {
let Some(scheme_end) = credential.find("://") else {
return false;
};
if scheme_end == 0
|| !credential[..scheme_end]
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'.' | b'-'))
{
return false;
}
let authority = &credential[scheme_end + 3..];
let authority_end = authority
.find(|ch: char| {
matches!(ch, '/' | '?' | '#' | '"' | '\'' | '<' | '>' | ')' | '(') || ch.is_whitespace()
})
.unwrap_or(authority.len()); let authority = &authority[..authority_end];
let Some(at) = authority.rfind('@') else {
return false;
};
let userinfo = &authority[..at];
let Some(colon) = userinfo.find(':') else {
return false;
};
colon + 1 < userinfo.len() && !contains_placeholder_word(userinfo)
}
pub(crate) fn char_diversity(credential: &str) -> f64 {
let len = credential.len();
if len == 0 {
return 1.0;
}
crate::entropy::unique_byte_count(credential.as_bytes()) as f64 / len as f64
}
fn longest_repeat_run_len(credential: &str) -> usize {
let bytes = credential.as_bytes();
if bytes.is_empty() {
return 0;
}
let mut max_run = 1usize;
let mut current_run = 1usize;
for index in 1..bytes.len() {
if bytes[index] == bytes[index - 1] {
current_run += 1;
if current_run > max_run {
max_run = current_run;
}
} else {
current_run = 1;
}
}
max_run
}
pub(crate) fn max_repeat_run(credential: &str) -> f64 {
let len = credential.len();
if len == 0 {
return 0.0;
}
longest_repeat_run_len(credential) as f64 / len as f64
}
pub(crate) fn is_degenerate_repeat_at(credential: &str, minimum_run_length: usize) -> bool {
longest_repeat_run_len(credential) >= minimum_run_length
}
pub(crate) fn apply_post_ml_penalties_with_encoded_text_lift(
score: f64,
credential: &str,
is_named: bool,
allow_encoded_text_secret: bool,
allow_canonical_hex_key: bool,
policy: keyhog_core::DetectorPostMatchConfidenceSpec,
) -> f64 {
if credential.is_empty() {
return score;
}
let mut adjusted = score;
let has_surface_placeholder = contains_placeholder_word(credential);
let decode_evidence = crate::decode_structure::evidence(credential);
let has_decoded_placeholder = decode_evidence.decoded_contains_placeholder();
let placeholder_is_only_url_host = is_named
&& has_surface_placeholder
&& !has_decoded_placeholder
&& has_credential_url_userinfo_without_placeholder(credential);
if (has_surface_placeholder || has_decoded_placeholder) && !placeholder_is_only_url_host {
adjusted *= policy.placeholder_multiplier;
}
if is_named {
if char_diversity(credential) < policy.minimum_byte_diversity {
adjusted *= policy.low_diversity_multiplier;
}
if max_repeat_run(credential) > policy.maximum_repeat_ratio
|| is_degenerate_repeat_at(credential, policy.degenerate_run_min_length)
{
adjusted *= policy.degenerate_repeat_multiplier;
}
} else {
if !allow_canonical_hex_key && char_diversity(credential) < policy.minimum_byte_diversity {
adjusted *= policy.low_diversity_multiplier;
}
if max_repeat_run(credential) > policy.maximum_repeat_ratio
|| is_degenerate_repeat_at(credential, policy.degenerate_run_min_length)
{
adjusted *= policy.degenerate_repeat_multiplier;
}
if !allow_canonical_hex_key && decode_evidence.is_binary_payload() {
if let Some(multiplier) = policy.data_envelope_multiplier {
adjusted *= multiplier;
}
}
if !allow_canonical_hex_key
&& !allow_encoded_text_secret
&& crate::decode_structure::looks_like_uniform_base64_blob(credential)
{
if let Some(multiplier) = policy.data_envelope_multiplier {
adjusted *= multiplier;
}
}
if !allow_canonical_hex_key
&& !allow_encoded_text_secret
&& decode_evidence.decoded_is_base64_blob()
{
if let Some(multiplier) = policy.data_envelope_multiplier {
adjusted *= multiplier;
}
}
}
finalize_confidence(adjusted)
}
pub(crate) fn apply_calibration_multiplier(
score: f64,
detector_id: &str,
calibration: Option<&keyhog_core::Calibration>,
) -> f64 {
let Some(calibration) = calibration else {
return finalize_confidence(score);
};
let counters = calibration.counters(detector_id);
if counters.observations() == 0 {
return finalize_confidence(score);
}
finalize_confidence(score * counters.posterior_mean())
}
pub(crate) fn apply_path_confidence_penalties(
score: f64,
path: Option<&str>,
penalize: bool,
fixture_path_multiplier: f64,
) -> f64 {
let Some(path) = path else {
return finalize_confidence(score);
};
if !penalize {
return finalize_confidence(score);
}
let is_fixture_like = crate::platform_compat::path_component_matches(path, |component| {
FIXTURE_PATH_COMPONENTS
.iter()
.any(|fixture| component.eq_ignore_ascii_case(fixture))
|| crate::placeholder_words::words()
.iter()
.any(|word| component.eq_ignore_ascii_case(word.lower()))
});
let adjusted = if is_fixture_like {
score * fixture_path_multiplier
} else {
score
};
finalize_confidence(adjusted)
}
#[cfg(test)]
#[path = "../../tests/unit/confidence_penalties_inline.rs"]
mod tests;