use serde::{Deserialize, Serialize};
use crate::matcher::Matcher;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Technique {
pub id: String,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Detection {
pub source: String,
pub rule: String,
pub confidence: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verdict: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SideEffect {
pub class: String,
pub detail: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct KbEntry {
pub id: String,
#[serde(rename = "match")]
pub matcher: Matcher,
#[serde(default)]
pub example: Option<String>,
pub description: String,
pub techniques: Vec<Technique>,
#[serde(default)]
pub telemetry: Vec<String>,
#[serde(default)]
pub detections: Vec<Detection>,
pub noise: u8,
}
impl KbEntry {
pub fn representative_line(&self) -> Option<String> {
self.example
.clone()
.or_else(|| self.matcher.representative_line())
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct KnowledgeBase {
pub platform: String,
#[serde(default)]
pub note: String,
pub entries: Vec<KbEntry>,
}
impl KnowledgeBase {
pub fn validate(&self) -> Result<(), String> {
for e in &self.entries {
if e.matcher.has_regex() && e.example.is_none() {
return Err(format!(
"entry `{}` uses a regex leaf but has no `example`",
e.id
));
}
if let Some(event) = &e.matcher.event {
event
.validate()
.map_err(|m| format!("entry `{}`: {m}", e.id))?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
impl Severity {
pub fn from_noise(noise: u8) -> Self {
match noise {
0..=24 => Severity::Low,
25..=49 => Severity::Medium,
50..=74 => Severity::High,
_ => Severity::Critical,
}
}
pub fn label(self) -> &'static str {
match self {
Severity::Low => "LOW",
Severity::Medium => "MEDIUM",
Severity::High => "HIGH",
Severity::Critical => "CRITICAL",
}
}
pub fn color(self) -> &'static str {
match self {
Severity::Low => crate::theme::CYAN,
Severity::Medium => crate::theme::YELLOW,
Severity::High => crate::theme::ORANGE,
Severity::Critical => crate::theme::RED,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdrMapping {
pub vendor: String,
pub events: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Finding {
pub line: usize,
pub source: String,
pub rule_id: String,
pub description: String,
pub techniques: Vec<Technique>,
pub telemetry: Vec<String>,
pub detections: Vec<Detection>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edr: Vec<EdrMapping>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub observed_side_effects: Vec<SideEffect>,
pub noise: u8,
pub severity: Severity,
#[serde(skip)]
pub matched_command: Option<crate::parser::Command>,
#[serde(skip)]
pub observed_event: Option<std::sync::Arc<std::collections::HashMap<String, String>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Report {
pub platform: String,
#[serde(default)]
pub note: String,
pub findings: Vec<Finding>,
pub max_noise: u8,
#[serde(default)]
pub lines_analyzed: usize,
}
impl Report {
pub fn max_severity(&self) -> Severity {
Severity::from_noise(self.max_noise)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn kb_with(matcher_json: &str, example: Option<&str>) -> KnowledgeBase {
let matcher: Matcher = serde_json::from_str(matcher_json).expect("matcher parses");
KnowledgeBase {
platform: "linux".into(),
note: String::new(),
entries: vec![KbEntry {
id: "x".into(),
matcher,
example: example.map(str::to_string),
description: "d".into(),
techniques: vec![],
telemetry: vec![],
detections: vec![],
noise: 10,
}],
}
}
#[test]
fn validate_requires_example_for_regex_entries() {
assert!(
kb_with(r#"{ "line": { "regex": "foo" } }"#, None)
.validate()
.is_err()
);
assert!(
kb_with(r#"{ "line": { "regex": "foo" } }"#, Some("foobar"))
.validate()
.is_ok()
);
assert!(
kb_with(r#"{ "line": { "contains": "foo" } }"#, None)
.validate()
.is_ok()
);
}
}