Skip to content

Commit 8406d51

Browse files
authored
Add exists modifier support (#16)
fix #15
1 parent 7162672 commit 8406d51

File tree

6 files changed

+188
-34
lines changed

6 files changed

+188
-34
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A Rust library for parsing and evaluating Sigma rules to create custom detection
1010
## Features
1111

1212
- Supports the [Sigma condition](https://sigmahq.io/docs/basics/conditions.html) syntax using Pratt parsing
13-
- Supports all [Sigma field modifiers](https://sigmahq.io/docs/basics/modifiers.html) except `expand` and `exists`
13+
- Supports all [Sigma field modifiers](https://sigmahq.io/docs/basics/modifiers.html) except `expand`
1414
- Written in 100% safe Rust
1515
- Daily automated security audit of dependencies
1616
- Extensive test suite

src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ pub enum ParserError {
2626
)]
2727
StandaloneViolation(String),
2828

29+
#[error("The 'exists' modifier must not be combined with any other modifiers")]
30+
ExistsNotStandalone(),
31+
32+
#[error("The 'exists' modifier requires a single boolean value")]
33+
InvalidValueForExists(),
34+
2935
#[error("Failed to parse IP address '{0}': '{1}'")]
3036
IPParsing(String, String),
3137

src/field.rs

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ impl Field {
8989
return Err(ParserError::EmptyValues(self.name.to_string()));
9090
}
9191

92+
if self.modifier.exists.is_some() {
93+
if self.values.len() != 1 {
94+
return Err(ParserError::InvalidValueForExists());
95+
}
96+
if let FieldValue::Boolean(b) = self.values[0] {
97+
self.modifier.exists = Some(b);
98+
} else {
99+
return Err(ParserError::InvalidValueForExists());
100+
}
101+
}
102+
92103
match self.modifier.match_modifier {
93104
Some(MatchModifier::Contains)
94105
| Some(MatchModifier::StartsWith)
@@ -175,44 +186,52 @@ impl Field {
175186
}
176187

177188
pub(crate) fn evaluate(&self, event: &Event) -> bool {
178-
if let Some(EventValue::Value(target)) = event.get(&self.name) {
179-
if self.values.is_empty() {
180-
// self.values should never be empty.
181-
// But, if it somehow happens we must return true, because
182-
// 1. the key exists in the event, and
183-
// 2. the field has no further conditions defined
184-
return true;
185-
}
189+
let Some(event_value) = event.get(&self.name) else {
190+
return matches!(self.modifier.exists, Some(false));
191+
};
192+
193+
if matches!(self.modifier.exists, Some(true)) {
194+
return true;
195+
};
186196

187-
let target = conditional_lowercase!(target, self.modifier.cased);
197+
let EventValue::Value(target) = event_value else {
198+
// We currently do not support matching against lists and hashmaps, see
199+
// https://github.com/jopohl/sigma-rust/issues/9
200+
return false;
201+
};
188202

189-
for val in self.values.iter() {
190-
let cmp = if self.modifier.fieldref {
191-
if let Some(EventValue::Value(value)) =
192-
event.get(val.value_to_string().as_str())
193-
{
194-
conditional_lowercase!(value, self.modifier.cased)
195-
} else {
196-
continue;
197-
}
203+
if self.values.is_empty() {
204+
// self.values should never be empty.
205+
// But, if it somehow happens we must return true, because
206+
// 1. the key exists in the event, and
207+
// 2. the field has no further conditions defined
208+
return true;
209+
}
210+
211+
let target = conditional_lowercase!(target, self.modifier.cased);
212+
213+
for val in self.values.iter() {
214+
let cmp = if self.modifier.fieldref {
215+
if let Some(EventValue::Value(value)) = event.get(val.value_to_string().as_str()) {
216+
conditional_lowercase!(value, self.modifier.cased)
198217
} else {
199-
conditional_lowercase!(val, self.modifier.cased)
200-
};
201-
202-
let fired = self.compare(target, cmp);
203-
if fired && !self.modifier.match_all {
204-
return true;
205-
} else if !fired && self.modifier.match_all {
206-
return false;
218+
continue;
207219
}
220+
} else {
221+
conditional_lowercase!(val, self.modifier.cased)
222+
};
223+
224+
let fired = self.compare(target, cmp);
225+
if fired && !self.modifier.match_all {
226+
return true;
227+
} else if !fired && self.modifier.match_all {
228+
return false;
208229
}
209-
// After the loop, there are two options:
210-
// 1. match_all = false: no condition fired => return false
211-
// 2. match_all = true: all conditions fired => return true
212-
self.modifier.match_all
213-
} else {
214-
false
215230
}
231+
// After the loop, there are two options:
232+
// 1. match_all = false: no condition fired => return false
233+
// 2. match_all = true: all conditions fired => return true
234+
self.modifier.match_all
216235
}
217236
}
218237

@@ -551,10 +570,42 @@ mod tests {
551570
assert!(field.evaluate(&event));
552571
}
553572

573+
#[test]
574+
fn test_empty_values() {
575+
let values: Vec<FieldValue> = vec![];
576+
let err = Field::new("test|contains", values).unwrap_err();
577+
assert!(matches!(err, ParserError::EmptyValues(a) if a == "test"));
578+
}
579+
554580
#[test]
555581
fn test_invalid_contains() {
556582
let values: Vec<FieldValue> = vec![FieldValue::from("ok"), FieldValue::Int(5)];
557583
let err = Field::new("test|contains", values).unwrap_err();
558584
assert!(matches!(err, ParserError::InvalidValueForStringModifier(_)));
559585
}
586+
587+
#[test]
588+
fn test_parse_exists_modifier() {
589+
let values: Vec<FieldValue> = vec![FieldValue::from(true)];
590+
let field = Field::new("test|exists", values).unwrap();
591+
assert!(field.modifier.exists.unwrap());
592+
593+
let values: Vec<FieldValue> = vec![FieldValue::from(false)];
594+
let field = Field::new("test|exists", values).unwrap();
595+
assert!(!field.modifier.exists.unwrap());
596+
}
597+
598+
#[test]
599+
fn test_parse_exists_modifier_invalid_values() {
600+
let values_vec: Vec<Vec<FieldValue>> = vec![
601+
vec![FieldValue::from("not a boolean")],
602+
vec![FieldValue::from("something"), FieldValue::Float(5.0)],
603+
vec![FieldValue::from(true), FieldValue::from(true)],
604+
];
605+
606+
for values in values_vec {
607+
let err = Field::new("test|exists", values).unwrap_err();
608+
assert!(matches!(err, ParserError::InvalidValueForExists()));
609+
}
610+
}
560611
}

src/field/modifier.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ pub enum ValueTransformer {
3131
Windash,
3232
}
3333

34-
#[derive(Debug, Default)]
34+
#[derive(Debug, Default, PartialEq)]
3535
pub struct Modifier {
3636
pub(crate) match_all: bool,
3737
pub(crate) fieldref: bool,
3838
pub(crate) cased: bool,
39+
pub(crate) exists: Option<bool>,
3940
pub(crate) match_modifier: Option<MatchModifier>,
4041
pub(crate) value_transformer: Option<ValueTransformer>,
4142
}
@@ -73,6 +74,12 @@ impl FromStr for Modifier {
7374
result.cased = true;
7475
continue;
7576
}
77+
if s == "exists" {
78+
// The real value of the exists modifier will be set during field parsing
79+
// because it is the field value and here we only parse the field name.
80+
result.exists = Some(bool::default());
81+
continue;
82+
}
7683

7784
if let Ok(match_modifier) = MatchModifier::from_str(&s) {
7885
if let Some(m) = result.match_modifier {
@@ -139,6 +146,17 @@ impl FromStr for Modifier {
139146
));
140147
}
141148

149+
if result.exists.is_some() {
150+
let tmp = Self {
151+
exists: Some(bool::default()),
152+
..Default::default()
153+
};
154+
155+
if result != tmp {
156+
return Err(Self::Err::ExistsNotStandalone());
157+
};
158+
}
159+
142160
Ok(result)
143161
}
144162
}
@@ -206,4 +224,16 @@ mod test {
206224
ParserError::ConflictingModifiers(ref a, ref b) if a == "cidr" && b == "re",
207225
));
208226
}
227+
228+
#[test]
229+
fn test_exists_modifier() {
230+
let modifier = Modifier::from_str("fieldname|exists").unwrap();
231+
assert!(modifier.exists.is_some());
232+
}
233+
234+
#[test]
235+
fn test_conflicting_exists_modifier() {
236+
let err = Modifier::from_str("fieldname|all|exists").unwrap_err();
237+
assert!(matches!(err, ParserError::ExistsNotStandalone()));
238+
}
209239
}

tests/json_events.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,46 @@ fn test_match_fieldref() {
163163

164164
let rule = rule_from_yaml(matching_rule).unwrap();
165165
assert!(check_rule(&rule, &event));
166+
167+
let not_matching_rule = r#"
168+
title: Fieldref test
169+
logsource:
170+
detection:
171+
selection:
172+
Image|fieldref: field_not_in_event
173+
condition: selection"#;
174+
175+
let rule = rule_from_yaml(not_matching_rule).unwrap();
176+
assert!(!check_rule(&rule, &event));
177+
}
178+
179+
#[cfg(feature = "serde_json")]
180+
#[test]
181+
fn test_nested_exists() {
182+
let event: Event = json!({
183+
"Image": "testing",
184+
"User": {
185+
"Name": {
186+
"First": ["Chuck"],
187+
"Last": "Norris",
188+
},
189+
"Mobile.phone": "1",
190+
"Age": 42,
191+
"SomeName": "Chuck",
192+
},
193+
"reference": "test",
194+
})
195+
.try_into()
196+
.unwrap();
197+
198+
let matching_rule = r#"
199+
title: Nested exists test
200+
logsource:
201+
detection:
202+
selection:
203+
User.Name.First|exists: true
204+
condition: selection"#;
205+
206+
let rule = rule_from_yaml(matching_rule).unwrap();
207+
assert!(check_rule(&rule, &event));
166208
}

tests/matching.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,28 @@ fn test_match_cased_windash() {
161161
assert!(!rule.is_match(&event_2));
162162
assert!(rule.is_match(&event_3));
163163
}
164+
165+
#[test]
166+
fn test_match_exists_modifier() {
167+
let yaml = r#"
168+
title: Existential test
169+
logsource:
170+
detection:
171+
selection:
172+
Image|exists: true
173+
OriginalFileName|exists: false
174+
condition: selection
175+
"#;
176+
177+
let rule = rule_from_yaml(yaml).unwrap();
178+
let event_1 = Event::from([("Image", "C:\\rundll32.exe")]);
179+
let event_2 = Event::from([
180+
("Image", "C:\\rundll32.exe"),
181+
("OriginalFileName", "RUNDLL32.EXE"),
182+
]);
183+
let event_3 = Event::from([("SomeField", "SomeValue")]);
184+
185+
assert!(rule.is_match(&event_1));
186+
assert!(!rule.is_match(&event_2));
187+
assert!(!rule.is_match(&event_3));
188+
}

0 commit comments

Comments
 (0)