use keyhog_core::DetectorSpec;
use std::sync::LazyLock;
const KEYWORD_SEPARATORS: [char; 3] = ['_', '-', '.'];
struct EmbeddedAssignmentPolicy {
keywords: Vec<String>,
vendor_suffixes: Vec<String>,
tail_suffixes: Vec<String>,
}
static ASSIGNMENT_POLICY: LazyLock<EmbeddedAssignmentPolicy> = LazyLock::new(|| {
let detectors = keyhog_core::load_embedded_detectors_or_fail()
.unwrap_or_else(|error| panic!("embedded detector corpus is corrupt: {error}"));
EmbeddedAssignmentPolicy {
keywords: derive_assignment_keywords(&detectors).unwrap_or_else(|error| {
panic!(
"cannot derive the generic assignment-keyword vocabulary: {error}. Fix the bundled generic phase-2 detector specs"
)
}),
vendor_suffixes: derive_generic_vendor_suffixes(&detectors).unwrap_or_else(|error| {
panic!(
"cannot derive generic vendor assignment suffixes: {error}. Fix the bundled generic phase-2 detector specs"
)
}),
tail_suffixes: derive_generic_assignment_tail_suffixes(&detectors).unwrap_or_else(
|error| {
panic!(
"cannot derive generic assignment tail suffixes: {error}. Fix the bundled generic phase-2 detector specs"
)
},
),
}
});
pub(crate) fn assignment_keywords() -> &'static [String] {
&ASSIGNMENT_POLICY.keywords
}
pub(crate) fn generic_vendor_suffixes() -> &'static [String] {
&ASSIGNMENT_POLICY.vendor_suffixes
}
pub(crate) fn generic_assignment_tail_suffixes() -> &'static [String] {
&ASSIGNMENT_POLICY.tail_suffixes
}
pub(crate) fn derive_assignment_keywords(
detectors: &[DetectorSpec],
) -> Result<Vec<String>, String> {
let mut ordered: Vec<String> = Vec::new();
let mut seen = std::collections::BTreeSet::<String>::new();
let mut generic_entropy_owners = 0usize;
for detector in detectors {
if !detector.owns_entropy_policy() {
continue;
}
generic_entropy_owners += 1;
for keyword in &detector.keywords {
let lower = keyword.to_ascii_lowercase();
for spelling in separator_spellings(&lower) {
if seen.insert(spelling.clone()) {
ordered.push(spelling);
}
}
}
}
if generic_entropy_owners == 0 {
return Err("no generic entropy-policy owner exists in the corpus; the \
assignment-keyword prefilter would admit nothing and silently drop every \
generic-credential chunk"
.to_string());
}
crate::tier_b_list::parse_token_list(
ordered,
&crate::tier_b_list::ListPolicy {
what: "assignment keyword",
require_lowercase: true,
separators: b"_-.",
},
)
}
pub(crate) fn derive_generic_vendor_suffixes(
detectors: &[DetectorSpec],
) -> Result<Vec<String>, String> {
derive_unique_phase2_suffixes(
detectors,
|detector| &detector.generic_vendor_suffixes,
"generic_vendor_suffixes",
"generic vendor suffix",
)
}
pub(crate) fn derive_generic_assignment_tail_suffixes(
detectors: &[DetectorSpec],
) -> Result<Vec<String>, String> {
derive_unique_phase2_suffixes(
detectors,
|detector| &detector.generic_assignment_tail_suffixes,
"generic_assignment_tail_suffixes",
"generic assignment tail suffix",
)
}
fn derive_unique_phase2_suffixes(
detectors: &[DetectorSpec],
select: for<'a> fn(&'a DetectorSpec) -> &'a [String],
field: &str,
what: &'static str,
) -> Result<Vec<String>, String> {
let mut owner: Option<&DetectorSpec> = None;
for detector in detectors {
if select(detector).is_empty() {
continue;
}
if detector.kind != keyhog_core::DetectorKind::Phase2Generic {
return Err(format!(
"detector {:?} declares {field} but is not phase2-generic",
detector.id
));
}
if let Some(previous) = owner {
return Err(format!(
"detectors {:?} and {:?} both declare {field}; exactly one phase2-generic detector may own the list",
previous.id, detector.id
));
}
owner = Some(detector);
}
let Some(owner) = owner else {
return Ok(Vec::new());
};
crate::tier_b_list::parse_token_list(
select(owner).to_vec(),
&crate::tier_b_list::ListPolicy {
what,
require_lowercase: true,
separators: b"",
},
)
}
fn separator_spellings(keyword: &str) -> Vec<String> {
if !keyword.contains(KEYWORD_SEPARATORS) {
return vec![keyword.to_string()];
}
KEYWORD_SEPARATORS
.iter()
.map(|&sep| {
keyword
.chars()
.map(|c| {
if KEYWORD_SEPARATORS.contains(&c) {
sep
} else {
c
}
})
.collect::<String>()
})
.collect()
}
#[cfg(test)]
#[path = "../tests/unit/assignment_keywords.rs"]
mod tests;