use crate::prelude::holder::DatasetHolder;
use crate::prelude::{AlertAggregation, AlertSeverity, SiemField, SiemIp, SiemLog};
use super::dataset::SiemDatasetType;
use super::mitre::{MitreTactics, MitreTechniques};
use crate::prelude::types::LogString;
use regex::Regex;
use serde::{de, Deserialize, Serialize, Serializer};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::str::FromStr;
pub mod sigma;
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct SiemRule {
pub id: LogString,
pub name: LogString,
pub description: LogString,
pub mitre: Cow<'static, MitreInfo>,
pub needed_datasets: Vec<SiemDatasetType>,
pub subrules: Cow<'static, BTreeMap<LogString, SiemSubRule>>,
pub conditions: Cow<'static, Vec<Vec<LogString>>>,
pub alert: Cow<'static, AlertGenerator>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct AlertGenerator {
pub content: Vec<AlertContent>,
pub severity: AlertSeverity,
pub tags: Vec<LogString>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aggregation: Option<AlertAggregation>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct MitreInfo {
pub tactics: Vec<MitreTactics>,
pub techniques: Vec<MitreTechniques>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct SiemSubRule {
pub conditions: Vec<RuleCondition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rule_state: Option<RuleState>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct RuleCondition {
pub field: LogString,
#[serde(flatten)]
pub operator: RuleOperator,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum AlertContent {
Text(LogString),
Field(LogString),
MatchedRules(LogString),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum RuleOperator {
All(Vec<Box<RuleOperator>>),
Any(Vec<Box<RuleOperator>>),
Not(Box<RuleOperator>),
Equals(SiemField),
StartsWith(String),
EndsWith(String),
Contains(String),
GT(SiemField),
LT(SiemField),
GTE(SiemField),
LTE(SiemField),
#[serde(
serialize_with = "regex_to_string",
deserialize_with = "string_to_regex"
)]
Matches(Regex),
SameNet((SiemIp, u8)),
IsLocalIp(bool),
IsExternalIp(bool),
Exists(bool),
IsNull(bool),
B64(Box<RuleOperator>),
InDataset(SiemDatasetType),
ExistsRuleState(Vec<RuleState>),
InCountry(String),
}
impl PartialEq for RuleOperator {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::All(v1), Self::All(v2)) => v1 == v2,
(Self::Any(v1), Self::Any(v2)) => v1 == v2,
(Self::Not(v1), Self::Not(v2)) => v1 == v2,
(Self::Equals(v1), Self::Equals(v2)) => v1 == v2,
(Self::StartsWith(v1), Self::StartsWith(v2)) => v1 == v2,
(Self::EndsWith(v1), Self::EndsWith(v2)) => v1 == v2,
(Self::Contains(v1), Self::Contains(v2)) => v1 == v2,
(Self::GT(v1), Self::GT(v2)) => v1 == v2,
(Self::LT(v1), Self::LT(v2)) => v1 == v2,
(Self::GTE(v1), Self::GTE(v2)) => v1 == v2,
(Self::LTE(v1), Self::LTE(v2)) => v1 == v2,
(Self::Matches(v1), Self::Matches(v2)) => v1.as_str() == v2.as_str(),
(Self::SameNet((v1, v11)), Self::SameNet((v2, v22))) => v1 == v2 && v11 == v22,
(Self::IsLocalIp(v1), Self::IsLocalIp(v2)) => v1 == v2,
(Self::IsExternalIp(v1), Self::IsExternalIp(v2)) => v1 == v2,
(Self::Exists(v1), Self::Exists(v2)) => v1 == v2,
(Self::B64(v1), Self::B64(v2)) => v1 == v2,
(Self::InDataset(v1), Self::InDataset(v2)) => v1 == v2,
(Self::ExistsRuleState(v1), Self::ExistsRuleState(v2)) => v1 == v2,
(Self::InCountry(v1), Self::InCountry(v2)) => v1 == v2,
(Self::IsNull(v1), Self::IsNull(v2)) => v1 == v2,
_ => false,
}
}
}
fn regex_to_string<S>(x: &Regex, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_str(x.as_str())
}
fn string_to_regex<'de, D>(deserializer: D) -> Result<Regex, D::Error>
where
D: de::Deserializer<'de>,
{
struct RegexVisitor;
impl<'de> de::Visitor<'de> for RegexVisitor {
type Value = Regex;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string containing json data")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Regex::from_str(v).map_err(E::custom)
}
}
deserializer.deserialize_any(RegexVisitor)
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct RuleState {
pub states: RuleStateValue,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum RuleStateValue {
Text(LogString),
Field(LogString),
}
#[derive(Clone, Serialize, Deserialize)]
pub struct AlertDictionary {
pub language_map: BTreeMap<LogString, BTreeMap<LogString, LogString>>,
}
impl AlertDictionary {
pub fn get_mappings_for(&self, language: &str) -> Option<&BTreeMap<LogString, LogString>> {
self.language_map.get(language)
}
pub fn get_mappings_for_id(&self, language: &str, id: &str) -> Option<&LogString> {
self.language_map.get(language).and_then(|v| v.get(id))
}
}
impl SiemRule {
pub fn matches(&self, log: &mut SiemLog, datasets: &DatasetHolder) -> bool {
for rule in self.subrules.as_ref().values() {
for condition in &rule.conditions {
if !condition.matches(log, datasets) {
return false;
}
}
}
true
}
}
impl RuleCondition {
pub fn matches(&self, log: &mut SiemLog, _datasets: &DatasetHolder) -> bool {
let field = log.field(&self.field);
if field.is_none() {
match &self.operator {
RuleOperator::Exists(cond) => return !*cond,
RuleOperator::IsNull(cond) => return *cond,
_ => return false,
}
}
let _field = field.unwrap();
match &self.operator {
RuleOperator::All(_) => todo!(),
RuleOperator::Any(_) => todo!(),
RuleOperator::Not(_) => todo!(),
RuleOperator::Equals(_) => todo!(),
RuleOperator::StartsWith(_) => todo!(),
RuleOperator::EndsWith(_) => todo!(),
RuleOperator::Contains(_) => todo!(),
RuleOperator::GT(_) => todo!(),
RuleOperator::LT(_) => todo!(),
RuleOperator::GTE(_) => todo!(),
RuleOperator::LTE(_) => todo!(),
RuleOperator::Matches(_) => todo!(),
RuleOperator::SameNet(_) => todo!(),
RuleOperator::IsLocalIp(_) => todo!(),
RuleOperator::IsExternalIp(_) => todo!(),
RuleOperator::Exists(cond) => *cond,
RuleOperator::IsNull(cond) => !*cond,
RuleOperator::B64(_) => todo!(),
RuleOperator::InDataset(_) => todo!(),
RuleOperator::ExistsRuleState(_) => todo!(),
RuleOperator::InCountry(_) => todo!(),
}
}
}
#[test]
fn should_be_serialized_and_deserialize() {
let superrule = SiemRule {
id: LogString::Borrowed("id001"),
name: LogString::Borrowed("Rule001"),
description: LogString::Borrowed("descripcion"),
mitre: Cow::Owned(MitreInfo {
tactics: vec![MitreTactics::TA0001],
techniques: vec![],
}),
needed_datasets: vec![SiemDatasetType::BlockIp],
subrules: {
let mut map = BTreeMap::new();
map.insert(
LogString::Borrowed("rule_source_ip"),
SiemSubRule {
conditions: vec![RuleCondition {
field: LogString::Borrowed("source.ip"),
operator: RuleOperator::Not(Box::new(RuleOperator::Equals(SiemField::IP(
[192, 168, 1, 1].into(),
)))),
}],
rule_state: None,
},
);
map.insert(
LogString::Borrowed("rule_destination_ip"),
SiemSubRule {
conditions: vec![RuleCondition {
field: LogString::Borrowed("destination.ip"),
operator: RuleOperator::All(vec![
Box::new(RuleOperator::IsExternalIp(true)),
Box::new(RuleOperator::InDataset(SiemDatasetType::BlockIp)),
]),
}],
rule_state: None,
},
);
Cow::Owned(map)
},
conditions: Cow::Owned(vec![vec![
LogString::Borrowed("rule_source_ip"),
LogString::Borrowed("rule_destination_ip"),
]]),
alert: Cow::Owned(AlertGenerator {
content: vec![
AlertContent::Text(LogString::Borrowed(
"A local ip tried to connect to a a malicious IP: source.ip=",
)),
AlertContent::Field(LogString::Borrowed("source.ip")),
AlertContent::Text(LogString::Borrowed(", destination.ip=")),
AlertContent::Field(LogString::Borrowed("destination.ip")),
],
severity: AlertSeverity::HIGH,
tags: vec![LogString::Borrowed("external_attack")],
aggregation: None,
}),
};
let json_txt = serde_json::to_string_pretty(&superrule).unwrap();
let _v: SiemRule = serde_json::from_str(&json_txt).unwrap();
let new_superrule = superrule.clone();
match new_superrule.name {
Cow::Borrowed(_) => {}
_ => {
unreachable!("Should not be owned")
}
};
}