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"",
},
)
}
pub(crate) trait DetectorPlanAssignmentSource {
fn id(&self) -> &str;
fn kind(&self) -> keyhog_core::DetectorKind;
fn owns_entropy_policy(&self) -> bool;
fn keywords(&self) -> &[String];
fn vendor_suffixes(&self) -> &[String];
fn tail_suffixes(&self) -> &[String];
fn max_len(&self) -> Option<usize>;
}
impl DetectorPlanAssignmentSource for crate::execution_pack::detector_plan::DetectorPlanRecord {
fn id(&self) -> &str {
&self.id
}
fn kind(&self) -> keyhog_core::DetectorKind {
self.kind
}
fn owns_entropy_policy(&self) -> bool {
self.owns_entropy_policy()
}
fn keywords(&self) -> &[String] {
&self.keywords
}
fn vendor_suffixes(&self) -> &[String] {
&self.generic_vendor_suffixes
}
fn tail_suffixes(&self) -> &[String] {
&self.generic_assignment_tail_suffixes
}
fn max_len(&self) -> Option<usize> {
self.max_len
}
}
impl DetectorPlanAssignmentSource for crate::detector_plan::StreamingDetectorPlanSummary {
fn id(&self) -> &str {
&self.id
}
fn kind(&self) -> keyhog_core::DetectorKind {
self.kind
}
fn owns_entropy_policy(&self) -> bool {
self.owns_entropy_policy()
}
fn keywords(&self) -> &[String] {
&self.keywords
}
fn vendor_suffixes(&self) -> &[String] {
&self.generic_vendor_suffixes
}
fn tail_suffixes(&self) -> &[String] {
&self.generic_assignment_tail_suffixes
}
fn max_len(&self) -> Option<usize> {
self.max_len
}
}
pub(crate) fn derive_assignment_keywords_from_plan<T: DetectorPlanAssignmentSource>(
detectors: &[T],
) -> Result<Vec<String>, String> {
let mut ordered = Vec::new();
let mut seen = std::collections::BTreeSet::new();
let mut owners = 0usize;
for detector in detectors {
if !DetectorPlanAssignmentSource::owns_entropy_policy(detector) {
continue;
}
owners += 1;
for keyword in detector.keywords() {
for spelling in separator_spellings(&keyword.to_ascii_lowercase()) {
if seen.insert(spelling.clone()) {
ordered.push(spelling);
}
}
}
}
if owners == 0 {
return Err("no generic entropy-policy owner exists in the detector plan".to_owned());
}
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_suffixes_from_plan<T: DetectorPlanAssignmentSource>(
detectors: &[T],
tail: bool,
) -> Result<Vec<String>, String> {
let field = if tail {
"generic_assignment_tail_suffixes"
} else {
"generic_vendor_suffixes"
};
let what = if tail {
"generic assignment tail suffix"
} else {
"generic vendor suffix"
};
let mut owner = None;
for detector in detectors {
let values = if tail {
detector.tail_suffixes()
} else {
detector.vendor_suffixes()
};
if values.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 {previous:?} and {:?} both declare {field}",
detector.id()
));
}
owner = Some(detector.id());
}
let Some(owner) = owner else {
return Ok(Vec::new());
};
let detector = detectors
.iter()
.find(|detector| detector.id() == owner)
.expect("validated plan owner");
let values = if tail {
detector.tail_suffixes().to_vec()
} else {
detector.vendor_suffixes().to_vec()
};
crate::tier_b_list::parse_token_list(
values,
&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;