use std::collections::HashMap;
use std::time::Duration;
use rsigma_eval::EvaluationResult;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::matcher::MatcherSet;
use crate::selector::Selector;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InhibitSourceSnap {
pub rule: String,
pub equal_key: String,
pub last_seen: i64,
}
#[derive(Debug, Clone)]
pub struct InhibitRule {
pub name: String,
pub source_match: MatcherSet,
pub target_match: MatcherSet,
pub equal: Vec<Selector>,
pub duration: Duration,
}
#[derive(Debug, Clone)]
pub struct InhibitConfig {
pub rules: Vec<InhibitRule>,
}
#[derive(Debug, Default)]
pub struct InhibitStore {
sources: HashMap<(String, String), i64>,
}
impl InhibitStore {
pub fn is_empty(&self) -> bool {
self.sources.is_empty()
}
pub fn evaluate(
&mut self,
cfg: &InhibitConfig,
result: &EvaluationResult,
now: i64,
) -> Option<String> {
for rule in &cfg.rules {
if rule.target_match.matches(result) && !rule.source_match.matches(result) {
let key = (rule.name.clone(), equal_key(&rule.equal, result));
if let Some(&last) = self.sources.get(&key)
&& now - last < rule.duration.as_secs() as i64
{
return Some(rule.name.clone());
}
}
}
for rule in &cfg.rules {
if rule.source_match.matches(result) {
let key = (rule.name.clone(), equal_key(&rule.equal, result));
self.sources.insert(key, now);
}
}
None
}
pub fn gc(&mut self, cfg: &InhibitConfig, now: i64) {
self.sources.retain(|(name, _), &mut last| {
cfg.rules
.iter()
.find(|r| &r.name == name)
.is_some_and(|r| now - last < r.duration.as_secs() as i64)
});
}
pub(crate) fn snapshot(&self) -> Vec<InhibitSourceSnap> {
self.sources
.iter()
.map(|((rule, equal_key), &last_seen)| InhibitSourceSnap {
rule: rule.clone(),
equal_key: equal_key.clone(),
last_seen,
})
.collect()
}
pub(crate) fn restore(&mut self, snaps: Vec<InhibitSourceSnap>, cfg: &InhibitConfig, now: i64) {
for snap in snaps {
if let Some(rule) = cfg.rules.iter().find(|r| r.name == snap.rule)
&& now - snap.last_seen < rule.duration.as_secs() as i64
{
self.sources
.insert((snap.rule, snap.equal_key), snap.last_seen);
}
}
}
pub fn active_count(&self, cfg: &InhibitConfig, now: i64) -> usize {
self.sources
.iter()
.filter(|entry| {
let name = &entry.0.0;
let last = *entry.1;
cfg.rules
.iter()
.find(|r| &r.name == name)
.is_some_and(|r| now - last < r.duration.as_secs() as i64)
})
.count()
}
}
fn equal_key(equal: &[Selector], result: &EvaluationResult) -> String {
let mut buf = String::new();
for sel in equal {
let value = sel
.resolve(result)
.map(|v| match v {
Value::String(s) => s,
other => other.to_string(),
})
.unwrap_or_default();
buf.push('\u{1f}');
buf.push_str(&value);
}
buf
}
#[cfg(test)]
mod tests {
use super::*;
use rsigma_eval::{DetectionBody, EvaluationResult, FieldMatch, ResultBody, RuleHeader};
use rsigma_parser::Level;
use std::collections::HashMap as Map;
use std::sync::Arc;
use super::super::matcher::{MatchOp, MatcherSet, MatcherSpec};
fn detection(ip: &str, level: Level) -> EvaluationResult {
EvaluationResult {
header: RuleHeader {
rule_title: "t".to_string(),
rule_id: Some("rule-1".to_string()),
level: Some(level),
tags: vec![],
custom_attributes: Arc::new(Map::new()),
enrichments: None,
},
body: ResultBody::Detection(DetectionBody {
matched_selections: vec![],
matched_fields: vec![FieldMatch::new("SourceIp", serde_json::json!(ip))],
event: None,
}),
}
}
fn matcher(selector: &str, value: &str) -> MatcherSpec {
MatcherSpec {
selector: selector.to_string(),
op: MatchOp::Eq,
value: value.to_string(),
}
}
fn cfg() -> InhibitConfig {
InhibitConfig {
rules: vec![InhibitRule {
name: "critical-inhibits-high".to_string(),
source_match: MatcherSet::compile(&[matcher("level", "critical")]).unwrap(),
target_match: MatcherSet::compile(&[matcher("level", "high")]).unwrap(),
equal: vec![Selector::parse("match.SourceIp").unwrap()],
duration: Duration::from_secs(300),
}],
}
}
#[test]
fn source_inhibits_matching_target() {
let cfg = cfg();
let mut store = InhibitStore::default();
assert_eq!(
store.evaluate(&cfg, &detection("10.0.0.1", Level::Critical), 0),
None
);
assert_eq!(
store.evaluate(&cfg, &detection("10.0.0.1", Level::High), 1),
Some("critical-inhibits-high".to_string())
);
assert_eq!(
store.evaluate(&cfg, &detection("10.0.0.2", Level::High), 2),
None
);
}
#[test]
fn source_expires_after_duration() {
let cfg = cfg();
let mut store = InhibitStore::default();
store.evaluate(&cfg, &detection("10.0.0.1", Level::Critical), 0);
assert_eq!(
store.evaluate(&cfg, &detection("10.0.0.1", Level::High), 400),
None
);
}
#[test]
fn self_inhibition_guard() {
let cfg = InhibitConfig {
rules: vec![InhibitRule {
name: "self".to_string(),
source_match: MatcherSet::compile(&[matcher("level", "high")]).unwrap(),
target_match: MatcherSet::compile(&[matcher("level", "high")]).unwrap(),
equal: vec![Selector::parse("match.SourceIp").unwrap()],
duration: Duration::from_secs(300),
}],
};
let mut store = InhibitStore::default();
store.evaluate(&cfg, &detection("10.0.0.1", Level::High), 0);
assert_eq!(
store.evaluate(&cfg, &detection("10.0.0.1", Level::High), 1),
None
);
}
#[test]
fn gc_and_active_count() {
let cfg = cfg();
let mut store = InhibitStore::default();
store.evaluate(&cfg, &detection("10.0.0.1", Level::Critical), 0);
assert_eq!(store.active_count(&cfg, 100), 1);
assert_eq!(store.active_count(&cfg, 400), 0, "past duration");
store.gc(&cfg, 400);
assert!(store.is_empty());
}
}