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 pub name: LogString,
20 pub description: LogString,
22 pub mitre: Cow<'static, MitreInfo>,
24 pub needed_datasets: Vec<SiemDatasetType>,
26 pub subrules: Cow<'static, BTreeMap<LogString, SiemSubRule>>,
28 pub conditions: Cow<'static, Vec<Vec<LogString>>>,
30 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 Text(LogString),
67 Field(LogString),
69 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 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 Regex::from_str(v).map_err(E::custom)
162 }
163 }
164
165 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}