use keyhog_core::{DetectorSpec, Severity};
#[derive(Debug)]
enum CompiledDetectorKeywordMatcher {
None,
One(Box<[u8]>),
Multiple(aho_corasick::AhoCorasick),
}
impl CompiledDetectorKeywordMatcher {
fn compile(detector: &DetectorSpec) -> Result<Self, String> {
if let Some(empty_index) = detector.keywords.iter().position(String::is_empty) {
return Err(format!(
"detector {:?} keyword {empty_index} is empty; remove it or declare a non-empty detector-owned context literal",
detector.id
));
}
match detector.keywords.as_slice() {
[] => Ok(Self::None),
[keyword] => Ok(Self::One(keyword.as_bytes().into())),
keywords => aho_corasick::AhoCorasickBuilder::new()
.kind(Some(aho_corasick::AhoCorasickKind::ContiguousNFA))
.build(keywords)
.map(Self::Multiple)
.map_err(|error| {
format!(
"detector {:?} keyword matcher could not compile: {error}",
detector.id
)
}),
}
}
#[inline]
fn is_match(&self, haystack: &[u8]) -> bool {
match self {
Self::None => false,
Self::One(keyword) => memchr::memmem::find(haystack, keyword).is_some(),
Self::Multiple(matcher) => matcher.is_match(haystack),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CandidateLengthRejection {
TooShort,
TooLong,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct CompiledDetectorLengthPolicy {
pub(crate) min_len: Option<usize>,
pub(crate) max_len: Option<usize>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct CompiledRequiredDetectorLengthPolicy {
pub(crate) min_len: usize,
pub(crate) max_len: usize,
}
impl CompiledDetectorLengthPolicy {
pub(crate) const fn compile(detector: &DetectorSpec) -> Self {
Self {
min_len: detector.min_len,
max_len: detector.max_len,
}
}
#[inline]
pub(crate) fn rejection(self, candidate_len: usize) -> Option<CandidateLengthRejection> {
if self.min_len.is_some_and(|min_len| candidate_len < min_len) {
Some(CandidateLengthRejection::TooShort)
} else if self.max_len.is_some_and(|max_len| candidate_len > max_len) {
Some(CandidateLengthRejection::TooLong)
} else {
None
}
}
pub(crate) fn require_bounded(
self,
detector_id: &str,
) -> Result<CompiledRequiredDetectorLengthPolicy, String> {
let min_len = self.min_len.ok_or_else(|| {
format!(
"detector {detector_id:?} owns entropy detection but omits min_len; declare the complete policy in its detector TOML"
)
})?;
let max_len = self.max_len.ok_or_else(|| {
format!(
"detector {detector_id:?} owns entropy detection but omits max_len; declare the complete policy in its detector TOML"
)
})?;
Ok(CompiledRequiredDetectorLengthPolicy { min_len, max_len })
}
}
#[derive(Debug)]
pub(crate) struct CompiledDetectorExecutionPolicy {
pub(crate) is_generic: bool,
pub(crate) length: CompiledDetectorLengthPolicy,
pub(crate) min_confidence: Option<f64>,
pub(crate) severity: Severity,
pub(crate) structural_password_slot: bool,
keywords: CompiledDetectorKeywordMatcher,
public_identifier_assignment_markers: Box<[Box<[u8]>]>,
}
impl CompiledDetectorExecutionPolicy {
pub(crate) fn compile(detector: &DetectorSpec) -> Result<Self, String> {
Ok(Self {
is_generic: detector.owns_entropy_policy(),
length: CompiledDetectorLengthPolicy::compile(detector),
min_confidence: detector.min_confidence,
severity: detector.severity,
structural_password_slot: detector.structural_password_slot,
keywords: CompiledDetectorKeywordMatcher::compile(detector)?,
public_identifier_assignment_markers: detector
.public_identifier_assignment_markers
.iter()
.map(|marker| marker.as_bytes().into())
.collect(),
})
}
#[inline]
pub(crate) fn line_has_public_identifier_assignment(&self, line: &[u8]) -> bool {
self.public_identifier_assignment_markers
.iter()
.any(|marker| crate::ascii_ci::ci_find_nonempty(line, marker.as_ref()))
}
#[inline]
pub(crate) fn keyword_nearby(&self, chunk_data: &[u8], preprocessed: &[u8]) -> bool {
let same_buffer = chunk_data.len() == preprocessed.len()
&& std::ptr::eq(chunk_data.as_ptr(), preprocessed.as_ptr());
let text_differs = !same_buffer && preprocessed != chunk_data;
self.keywords.is_match(chunk_data) || (text_differs && self.keywords.is_match(preprocessed))
}
}