use std::collections::BTreeMap;
use rsigma_parser::LogSource;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct LogSourceView {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) category: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) product: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) service: Option<String>,
}
impl LogSourceView {
pub(crate) fn from_logsource(ls: &LogSource) -> Option<Self> {
if ls.category.is_none() && ls.product.is_none() && ls.service.is_none() {
return None;
}
Some(Self {
category: ls.category.clone(),
product: ls.product.clone(),
service: ls.service.clone(),
})
}
pub(crate) fn label(view: &Option<Self>) -> String {
let Some(v) = view else {
return "(none)".to_string();
};
let parts: Vec<&str> = [
v.product.as_deref(),
v.category.as_deref(),
v.service.as_deref(),
]
.into_iter()
.flatten()
.collect();
if parts.is_empty() {
"(none)".to_string()
} else {
parts.join("/")
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct BacktestSummary {
pub(crate) corpus_files: u64,
pub(crate) events_processed: u64,
pub(crate) rules_loaded: u64,
pub(crate) expectations_total: u64,
pub(crate) expectations_passed: u64,
pub(crate) expectations_failed: u64,
pub(crate) unexpected_rules: u64,
pub(crate) unexpected_fires: u64,
pub(crate) unexpected_policy: String,
pub(crate) duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ExpectationResult {
pub(crate) rule: String,
pub(crate) rule_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) scope: Option<String>,
pub(crate) bound: String,
pub(crate) actual: u64,
pub(crate) pass: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct RuleStat {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) rule_id: Option<String>,
pub(crate) rule_title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) level: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) logsource: Option<LogSourceView>,
pub(crate) fires: u64,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub(crate) by_file: BTreeMap<String, u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct UnexpectedStat {
pub(crate) rule_key: String,
pub(crate) rule_title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) level: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) logsource: Option<LogSourceView>,
pub(crate) fires: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LogSourceRollup {
pub(crate) logsource: String,
pub(crate) unexpected_fires: u64,
pub(crate) rules: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct BacktestReport {
pub(crate) summary: BacktestSummary,
pub(crate) expectations: Vec<ExpectationResult>,
pub(crate) rules: Vec<RuleStat>,
pub(crate) unexpected: Vec<UnexpectedStat>,
pub(crate) by_logsource: Vec<LogSourceRollup>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CoverageSummary {
pub(crate) rules_total: usize,
pub(crate) rules_tagged: usize,
pub(crate) rules_untagged: usize,
pub(crate) techniques: usize,
pub(crate) subtechniques: usize,
pub(crate) tactics: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct TechniqueEntry {
pub(crate) id: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) tactics: Vec<String>,
pub(crate) rule_count: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) rules: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct AtomicsGap {
pub(crate) atomics_total: usize,
pub(crate) covered: usize,
pub(crate) atomics_without_rule: Vec<String>,
pub(crate) rules_without_atomic: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct BaselineGap {
pub(crate) baseline_total: usize,
pub(crate) covered: usize,
pub(crate) baseline_not_covered: Vec<String>,
pub(crate) ahead_of_baseline: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct TargetGap {
pub(crate) targets_total: usize,
pub(crate) covered: usize,
pub(crate) uncovered: Vec<String>,
pub(crate) covered_via_subtechnique: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CoverageReport {
pub(crate) summary: CoverageSummary,
pub(crate) techniques: Vec<TechniqueEntry>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) untagged_rules: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) atomics: Option<AtomicsGap>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) baseline: Option<BaselineGap>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) targets: Option<TargetGap>,
}