use std::{borrow::Borrow, collections::HashSet, error::Error, fmt, sync::Arc};
use crate::{
compiled_rule::CompiledRuleSet,
rule::{Rule, RuleError, RuleId, RuleSpec},
scanner::Scanner,
};
#[derive(Debug)]
pub enum ScannerBuildError {
Rule(RuleError),
EmptyRuleId,
DuplicateRuleId {
rule_id: RuleId,
},
EmptyMatcher {
rule_id: RuleId,
},
TooManyRules,
AutomatonBuild(aho_corasick::BuildError),
}
impl fmt::Display for ScannerBuildError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Rule(error) => write!(formatter, "could not construct rule: {error}"),
Self::EmptyRuleId => formatter.write_str("rule identifier cannot be empty"),
Self::DuplicateRuleId { rule_id } => {
write!(formatter, "duplicate rule identifier `{rule_id}`")
}
Self::EmptyMatcher { rule_id } => {
write!(formatter, "rule `{rule_id}` uses an empty matcher")
}
Self::TooManyRules => {
formatter.write_str("configured rule count exceeds the scanner limit")
}
Self::AutomatonBuild(error) => {
write!(
formatter,
"could not compile multi-pattern matcher: {error}"
)
}
}
}
}
impl Error for ScannerBuildError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Rule(error) => Some(error),
Self::AutomatonBuild(error) => Some(error),
Self::EmptyRuleId
| Self::DuplicateRuleId { .. }
| Self::EmptyMatcher { .. }
| Self::TooManyRules => None,
}
}
}
impl From<RuleError> for ScannerBuildError {
fn from(error: RuleError) -> Self {
Self::Rule(error)
}
}
#[derive(Debug, Default)]
pub struct ScannerBuilder {
rules: Vec<Rule>,
builtin_rules: Vec<RuleSpec>,
}
impl ScannerBuilder {
#[must_use]
pub const fn new() -> Self {
Self {
rules: Vec::new(),
builtin_rules: Vec::new(),
}
}
#[must_use]
pub fn builtin(mut self, rule: RuleSpec) -> Self {
self.builtin_rules.push(rule);
self
}
#[must_use]
pub fn builtins<I>(mut self, rules: I) -> Self
where
I: IntoIterator,
I::Item: Borrow<RuleSpec>,
{
self.builtin_rules
.extend(rules.into_iter().map(|rule| *rule.borrow()));
self
}
#[must_use]
pub fn rule(mut self, rule: Rule) -> Self {
self.rules.push(rule);
self
}
#[must_use]
pub fn rules<I>(mut self, rules: I) -> Self
where
I: IntoIterator<Item = Rule>,
{
self.rules.extend(rules);
self
}
pub fn build(mut self) -> Result<Scanner, ScannerBuildError> {
self.rules.reserve(self.builtin_rules.len());
for specification in self.builtin_rules {
self.rules.push(specification.to_rule()?);
}
validate_unique_rule_ids(&self.rules)?;
let rules = CompiledRuleSet::compile(self.rules)?;
Ok(Scanner::new(Arc::new(rules)))
}
}
fn validate_unique_rule_ids(rules: &[Rule]) -> Result<(), ScannerBuildError> {
let mut seen = HashSet::with_capacity(rules.len());
for rule in rules {
if !seen.insert(rule.id().clone()) {
return Err(ScannerBuildError::DuplicateRuleId {
rule_id: rule.id().clone(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Severity, builtins};
#[test]
fn builtins_accept_borrowed_catalog_slice() {
let scanner = ScannerBuilder::new()
.builtins(builtins::CURRENT)
.build()
.expect("borrowed built-in catalog should compile");
assert_eq!(scanner.rules_count(), builtins::CURRENT.len());
}
#[test]
fn duplicate_custom_rule_ids_are_rejected() {
let error = ScannerBuilder::new()
.rules([
Rule::literal("acme.shared", "FIRST", Severity::High),
Rule::literal("acme.shared", "SECOND", Severity::Critical),
])
.build()
.expect_err("duplicate custom rule identifiers should fail");
assert!(matches!(
error,
ScannerBuildError::DuplicateRuleId { ref rule_id }
if rule_id.as_str() == "acme.shared"
));
}
#[test]
fn custom_rule_cannot_shadow_builtin_identifier() {
let builtin = builtins::CURRENT[0];
let error = ScannerBuilder::new()
.builtin(builtin)
.rule(Rule::literal(
builtin.id(),
"CUSTOM_VALUE",
Severity::Critical,
))
.build()
.expect_err("custom rule should not shadow built-in identity");
assert!(matches!(
error,
ScannerBuildError::DuplicateRuleId { ref rule_id }
if rule_id.as_str() == builtin.id()
));
}
}