usiem/components/rule/
mod.rs

1use crate::prelude::holder::DatasetHolder;
2use crate::prelude::{AlertAggregation, AlertSeverity, SiemField, SiemIp, SiemLog};
3
4use super::dataset::SiemDatasetType;
5use super::mitre::{MitreTactics, MitreTechniques};
6use crate::prelude::types::LogString;
7use regex::Regex;
8use serde::{de, Deserialize, Serialize, Serializer};
9use std::borrow::Cow;
10use std::collections::BTreeMap;
11use std::str::FromStr;
12
13pub mod sigma;
14
15#[derive(Clone, Serialize, Deserialize, Debug)]
16pub struct SiemRule {
17    pub id: LogString,
18    /// Name of the rule
19    pub name: LogString,
20    /// A description of the rule to be showed in the UI
21    pub description: LogString,
22    /// tactics and techniques covered by this rule
23    pub mitre: Cow<'static, MitreInfo>,
24    /// List of datasets needed by this rule
25    pub needed_datasets: Vec<SiemDatasetType>,
26    /// List of subrules that this rule is made of
27    pub subrules: Cow<'static, BTreeMap<LogString, SiemSubRule>>,
28    /// List of subrules that triggers this rule.
29    pub conditions: Cow<'static, Vec<Vec<LogString>>>,
30    /// Generates the content of the alert
31    pub alert: Cow<'static, AlertGenerator>,
32}
33
34#[derive(Clone, Serialize, Deserialize, Debug)]
35pub struct AlertGenerator {
36    pub content: Vec<AlertContent>,
37    pub severity: AlertSeverity,
38    pub tags: Vec<LogString>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub aggregation: Option<AlertAggregation>,
41}
42
43#[derive(Clone, Serialize, Deserialize, Debug)]
44pub struct MitreInfo {
45    pub tactics: Vec<MitreTactics>,
46    pub techniques: Vec<MitreTechniques>,
47}
48
49#[derive(Clone, Serialize, Deserialize, Debug)]
50pub struct SiemSubRule {
51    pub conditions: Vec<RuleCondition>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub rule_state: Option<RuleState>,
54}
55
56#[derive(Clone, Serialize, Deserialize, Debug)]
57pub struct RuleCondition {
58    pub field: LogString,
59    #[serde(flatten)]
60    pub operator: RuleOperator,
61}
62
63#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
64pub enum AlertContent {
65    /// A basic text
66    Text(LogString),
67    /// Content of a Log field
68    Field(LogString),
69    /// List of matched rules joined by a string. Ex ("\n", ","...)
70    MatchedRules(LogString),
71}
72
73#[derive(Clone, Debug, Serialize, Deserialize)]
74#[non_exhaustive]
75#[serde(rename_all = "snake_case")]
76pub enum RuleOperator {
77    All(Vec<Box<RuleOperator>>),
78    Any(Vec<Box<RuleOperator>>),
79    Not(Box<RuleOperator>),
80    Equals(SiemField),
81    StartsWith(String),
82    EndsWith(String),
83    Contains(String),
84    GT(SiemField),
85    LT(SiemField),
86    GTE(SiemField),
87    LTE(SiemField),
88    #[serde(
89        serialize_with = "regex_to_string",
90        deserialize_with = "string_to_regex"
91    )]
92    Matches(Regex),
93    SameNet((SiemIp, u8)),
94    IsLocalIp(bool),
95    IsExternalIp(bool),
96    Exists(bool),
97    IsNull(bool),
98    B64(Box<RuleOperator>),
99    InDataset(SiemDatasetType),
100    ExistsRuleState(Vec<RuleState>),
101    InCountry(String),
102}
103
104impl PartialEq for RuleOperator {
105    fn eq(&self, other: &Self) -> bool {
106        match (self, other) {
107            (Self::All(v1), Self::All(v2)) => v1 == v2,
108            (Self::Any(v1), Self::Any(v2)) => v1 == v2,
109            (Self::Not(v1), Self::Not(v2)) => v1 == v2,
110            (Self::Equals(v1), Self::Equals(v2)) => v1 == v2,
111            (Self::StartsWith(v1), Self::StartsWith(v2)) => v1 == v2,
112            (Self::EndsWith(v1), Self::EndsWith(v2)) => v1 == v2,
113            (Self::Contains(v1), Self::Contains(v2)) => v1 == v2,
114            (Self::GT(v1), Self::GT(v2)) => v1 == v2,
115            (Self::LT(v1), Self::LT(v2)) => v1 == v2,
116            (Self::GTE(v1), Self::GTE(v2)) => v1 == v2,
117            (Self::LTE(v1), Self::LTE(v2)) => v1 == v2,
118            (Self::Matches(v1), Self::Matches(v2)) => v1.as_str() == v2.as_str(),
119            (Self::SameNet((v1, v11)), Self::SameNet((v2, v22))) => v1 == v2 && v11 == v22,
120            (Self::IsLocalIp(v1), Self::IsLocalIp(v2)) => v1 == v2,
121            (Self::IsExternalIp(v1), Self::IsExternalIp(v2)) => v1 == v2,
122            (Self::Exists(v1), Self::Exists(v2)) => v1 == v2,
123            (Self::B64(v1), Self::B64(v2)) => v1 == v2,
124            (Self::InDataset(v1), Self::InDataset(v2)) => v1 == v2,
125            (Self::ExistsRuleState(v1), Self::ExistsRuleState(v2)) => v1 == v2,
126            (Self::InCountry(v1), Self::InCountry(v2)) => v1 == v2,
127            (Self::IsNull(v1), Self::IsNull(v2)) => v1 == v2,
128            _ => false,
129        }
130    }
131}
132
133fn regex_to_string<S>(x: &Regex, s: S) -> Result<S::Ok, S::Error>
134where
135    S: Serializer,
136{
137    s.serialize_str(x.as_str())
138}
139
140fn string_to_regex<'de, D>(deserializer: D) -> Result<Regex, D::Error>
141where
142    D: de::Deserializer<'de>,
143{
144    // define a visitor that deserializes
145    // `ActualData` encoded as json within a string
146    struct RegexVisitor;
147
148    impl<'de> de::Visitor<'de> for RegexVisitor {
149        type Value = Regex;
150
151        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
152            formatter.write_str("a string containing json data")
153        }
154
155        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
156        where
157            E: de::Error,
158        {
159            // unfortunately we lose some typed information
160            // from errors deserializing the json string
161            Regex::from_str(v).map_err(E::custom)
162        }
163    }
164
165    // use our visitor to deserialize an `ActualValue`
166    deserializer.deserialize_any(RegexVisitor)
167}
168
169#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
170pub struct RuleState {
171    pub states: RuleStateValue,
172}
173
174#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
175pub enum RuleStateValue {
176    Text(LogString),
177    Field(LogString),
178}
179
180#[derive(Clone, Serialize, Deserialize)]
181pub struct AlertDictionary {
182    pub language_map: BTreeMap<LogString, BTreeMap<LogString, LogString>>,
183}
184impl AlertDictionary {
185    pub fn get_mappings_for(&self, language: &str) -> Option<&BTreeMap<LogString, LogString>> {
186        self.language_map.get(language)
187    }
188
189    pub fn get_mappings_for_id(&self, language: &str, id: &str) -> Option<&LogString> {
190        self.language_map.get(language).and_then(|v| v.get(id))
191    }
192}
193
194impl SiemRule {
195    pub fn matches(&self, log: &mut SiemLog, datasets: &DatasetHolder) -> bool {
196        for rule in self.subrules.as_ref().values() {
197            for condition in &rule.conditions {
198                if !condition.matches(log, datasets) {
199                    return false;
200                }
201            }
202        }
203        true
204    }
205}
206
207impl RuleCondition {
208    pub fn matches(&self, log: &mut SiemLog, _datasets: &DatasetHolder) -> bool {
209        let field = log.field(&self.field);
210        if field.is_none() {
211            match &self.operator {
212                RuleOperator::Exists(cond) => return !*cond,
213                RuleOperator::IsNull(cond) => return *cond,
214                _ => return false,
215            }
216        }
217        let _field = field.unwrap();
218        match &self.operator {
219            RuleOperator::All(_) => todo!(),
220            RuleOperator::Any(_) => todo!(),
221            RuleOperator::Not(_) => todo!(),
222            RuleOperator::Equals(_) => todo!(),
223            RuleOperator::StartsWith(_) => todo!(),
224            RuleOperator::EndsWith(_) => todo!(),
225            RuleOperator::Contains(_) => todo!(),
226            RuleOperator::GT(_) => todo!(),
227            RuleOperator::LT(_) => todo!(),
228            RuleOperator::GTE(_) => todo!(),
229            RuleOperator::LTE(_) => todo!(),
230            RuleOperator::Matches(_) => todo!(),
231            RuleOperator::SameNet(_) => todo!(),
232            RuleOperator::IsLocalIp(_) => todo!(),
233            RuleOperator::IsExternalIp(_) => todo!(),
234            RuleOperator::Exists(cond) => *cond,
235            RuleOperator::IsNull(cond) => !*cond,
236            RuleOperator::B64(_) => todo!(),
237            RuleOperator::InDataset(_) => todo!(),
238            RuleOperator::ExistsRuleState(_) => todo!(),
239            RuleOperator::InCountry(_) => todo!(),
240        }
241    }
242}
243
244#[test]
245fn should_be_serialized_and_deserialize() {
246    let superrule = SiemRule {
247        id: LogString::Borrowed("id001"),
248        name: LogString::Borrowed("Rule001"),
249        description: LogString::Borrowed("descripcion"),
250        mitre: Cow::Owned(MitreInfo {
251            tactics: vec![MitreTactics::TA0001],
252            techniques: vec![],
253        }),
254        needed_datasets: vec![SiemDatasetType::BlockIp],
255        subrules: {
256            let mut map = BTreeMap::new();
257            map.insert(
258                LogString::Borrowed("rule_source_ip"),
259                SiemSubRule {
260                    conditions: vec![RuleCondition {
261                        field: LogString::Borrowed("source.ip"),
262                        operator: RuleOperator::Not(Box::new(RuleOperator::Equals(SiemField::IP(
263                            [192, 168, 1, 1].into(),
264                        )))),
265                    }],
266                    rule_state: None,
267                },
268            );
269            map.insert(
270                LogString::Borrowed("rule_destination_ip"),
271                SiemSubRule {
272                    conditions: vec![RuleCondition {
273                        field: LogString::Borrowed("destination.ip"),
274                        operator: RuleOperator::All(vec![
275                            Box::new(RuleOperator::IsExternalIp(true)),
276                            Box::new(RuleOperator::InDataset(SiemDatasetType::BlockIp)),
277                        ]),
278                    }],
279                    rule_state: None,
280                },
281            );
282            Cow::Owned(map)
283        },
284        conditions: Cow::Owned(vec![vec![
285            LogString::Borrowed("rule_source_ip"),
286            LogString::Borrowed("rule_destination_ip"),
287        ]]),
288        alert: Cow::Owned(AlertGenerator {
289            content: vec![
290                AlertContent::Text(LogString::Borrowed(
291                    "A local ip tried to connect to a a malicious IP: source.ip=",
292                )),
293                AlertContent::Field(LogString::Borrowed("source.ip")),
294                AlertContent::Text(LogString::Borrowed(", destination.ip=")),
295                AlertContent::Field(LogString::Borrowed("destination.ip")),
296            ],
297            severity: AlertSeverity::HIGH,
298            tags: vec![LogString::Borrowed("external_attack")],
299            aggregation: None,
300        }),
301    };
302    let json_txt = serde_json::to_string_pretty(&superrule).unwrap();
303    let _v: SiemRule = serde_json::from_str(&json_txt).unwrap();
304
305    let new_superrule = superrule.clone();
306    match new_superrule.name {
307        Cow::Borrowed(_) => {}
308        _ => {
309            unreachable!("Should not be owned")
310        }
311    };
312}