Skip to content

Commit 72c2eac

Browse files
authored
Add support for fieldref modifier (#6)
1 parent 5d41a36 commit 72c2eac

File tree

8 files changed

+435
-238
lines changed

8 files changed

+435
-238
lines changed

README.md

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@
55
[![Crates.io](https://img.shields.io/crates/v/sigma-rust)](https://crates.io/crates/sigma-rust)
66
[![Docs.rs](https://docs.rs/sigma-rust/badge.svg)](https://docs.rs/sigma-rust)
77

8-
98
A Rust library for parsing and evaluating Sigma rules to create custom detection pipelines.
109

1110
## Features
1211

13-
- Supports all [sigma modifiers](https://sigmahq.io/docs/basics/modifiers.html) except `expand`
12+
- Supports all[^1] [sigma modifiers](https://sigmahq.io/docs/basics/modifiers.html) including the unofficial `fieldref`
13+
modifier
1414
- Supports the whole [Sigma condition](https://sigmahq.io/docs/basics/conditions.html) syntax using Pratt parsing
1515
- Written in 100% safe Rust
1616
- Daily automated security audit of dependencies
1717
- Extensive test suite
1818

19+
[^1]: Except the [expand](https://sigmahq.io/docs/basics/modifiers.html#expand) modifier.
20+
1921
## Example
2022

2123
```rust
@@ -28,6 +30,7 @@ fn main() {
2830
category: test
2931
detection:
3032
selection_1:
33+
Event.ID: 42
3134
TargetFilename|contains: ':\temp\'
3235
TargetFilename|endswith:
3336
- '.au3'
@@ -42,15 +45,39 @@ fn main() {
4245

4346
let rule = rule_from_yaml(rule_yaml).unwrap();
4447
let event = event_from_json(
45-
r#"{"TargetFilename": "C:\\temp\\file.au3", "Image": "C:\\temp\\autoit4.exe"}"#,
48+
r#"{"TargetFilename": "C:\\temp\\file.au3", "Image": "C:\\temp\\autoit4.exe", "Event": {"ID": 42}}"#,
4649
)
4750
.unwrap();
4851

4952
assert!(rule.is_match(&event));
5053
}
5154
```
5255

53-
Check out the `examples` folder for more examples.
56+
## Matching nested fields
57+
58+
You can access nested fields by using a dot `.` as a separator. For example, if you have an event like
59+
60+
```json
61+
{
62+
"Event": {
63+
"ID": 42
64+
}
65+
}
66+
```
67+
68+
you can access the `ID` field by using `Event.ID` in the Sigma rule. Note, that fields containing a dot take
69+
precedence over nested fields. For example, if you have an event like
70+
71+
```json
72+
{
73+
"Event.ID": 42,
74+
"Event": {
75+
"ID": 43
76+
}
77+
}
78+
```
79+
80+
the engine will evaluate `Event.ID` to 42.
5481

5582
## Strong type checking
5683

@@ -73,7 +100,6 @@ selection_2:
73100
condition: 1 of them
74101
```
75102

76-
77103
## License
78104

79105
Licensed under either of
@@ -87,4 +113,5 @@ at your option.
87113

88114
Contributions are welcome! Please open an issue or create a pull request.
89115

90-
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
116+
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as
117+
defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

src/error.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ pub enum ParserError {
66
#[error("Unknown field modifier '{0}' provided")]
77
UnknownModifier(String),
88

9-
#[error("UTF16 encoding requested but no value transformation modifier provided (base64 or base64offset)")]
9+
#[error("UTF16 encoding requested but no value transformation modifier provided (base64 or base64offset)"
10+
)]
1011
Utf16WithoutBase64,
1112

1213
#[error(
@@ -20,7 +21,9 @@ pub enum ParserError {
2021
#[error("Failed to parse regular expression: '{0}'")]
2122
RegexParsing(regex::Error),
2223

23-
#[error("The modifier '{0}' must not be combined with other modifiers except 'all'")]
24+
#[error(
25+
"The modifier '{0}' must not be combined with other modifiers except 'all' and 'fieldref'"
26+
)]
2427
StandaloneViolation(String),
2528

2629
#[error("Failed to parse IP address '{0}': '{1}'")]
@@ -47,7 +50,8 @@ pub enum ParserError {
4750
#[error("Field names must be string, got: '{0}'")]
4851
InvalidFieldName(String),
4952

50-
#[error("The modifiers contains, startswith and endswith must be used with string values, got: '{0}'")]
53+
#[error("The modifiers contains, startswith and endswith must be used with string values, got: '{0}'"
54+
)]
5155
InvalidValueForStringModifier(String),
5256
}
5357

src/event.rs

Lines changed: 105 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,60 @@ struct EventProxy {
99
value: serde_json::Value,
1010
}
1111

12+
#[derive(Debug, PartialEq)]
13+
pub enum EventValue {
14+
Value(FieldValue),
15+
Sequence(Vec<EventValue>),
16+
Map(HashMap<String, EventValue>),
17+
}
18+
19+
#[cfg(feature = "serde_json")]
20+
impl TryFrom<serde_json::Value> for EventValue {
21+
type Error = crate::error::JSONError;
22+
23+
fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
24+
match value {
25+
serde_json::Value::Null
26+
| serde_json::Value::Bool(_)
27+
| serde_json::Value::Number(_)
28+
| serde_json::Value::String(_) => Ok(Self::Value(FieldValue::try_from(value)?)),
29+
serde_json::Value::Array(a) => {
30+
let mut result = Vec::with_capacity(a.len());
31+
for item in a {
32+
result.push(Self::try_from(item)?);
33+
}
34+
Ok(Self::Sequence(result))
35+
}
36+
serde_json::Value::Object(data) => {
37+
let mut result = HashMap::with_capacity(data.len());
38+
for (key, value) in data {
39+
result.insert(key, Self::try_from(value)?);
40+
}
41+
Ok(Self::Map(result))
42+
}
43+
}
44+
}
45+
}
46+
47+
impl EventValue {
48+
pub(crate) fn contains(&self, s: &str) -> bool {
49+
match self {
50+
Self::Value(v) => v.value_to_string().contains(s),
51+
Self::Sequence(seq) => seq.iter().any(|v| v.contains(s)),
52+
Self::Map(m) => m.values().any(|v| v.contains(s)),
53+
}
54+
}
55+
}
56+
57+
impl<T> From<T> for EventValue
58+
where
59+
T: Into<FieldValue>,
60+
{
61+
fn from(value: T) -> Self {
62+
Self::Value(value.into())
63+
}
64+
}
65+
1266
/// The `Event` struct represents a log event.
1367
///
1468
/// It is a collection of key-value pairs
@@ -18,7 +72,7 @@ struct EventProxy {
1872
#[cfg_attr(feature = "serde_json", derive(serde::Deserialize))]
1973
#[cfg_attr(feature = "serde_json", serde(try_from = "EventProxy"))]
2074
pub struct Event {
21-
inner: HashMap<String, FieldValue>,
75+
inner: HashMap<String, EventValue>,
2276
}
2377

2478
#[cfg(feature = "serde_json")]
@@ -33,7 +87,7 @@ impl TryFrom<EventProxy> for Event {
3387
impl<T, S, const N: usize> From<[(S, T); N]> for Event
3488
where
3589
S: Into<String> + Hash + Eq,
36-
T: Into<FieldValue>,
90+
T: Into<EventValue>,
3791
{
3892
fn from(values: [(S, T); N]) -> Self {
3993
let mut data = HashMap::with_capacity(N);
@@ -44,20 +98,6 @@ where
4498
}
4599
}
46100

47-
impl<T, S> From<HashMap<S, T>> for Event
48-
where
49-
S: Into<String> + Hash + Eq,
50-
T: Into<FieldValue>,
51-
{
52-
fn from(data: HashMap<S, T>) -> Self {
53-
let mut result = Self::default();
54-
for (key, val) in data.into_iter() {
55-
result.inner.insert(key.into(), val.into());
56-
}
57-
result
58-
}
59-
}
60-
61101
impl Event {
62102
/// Create a new empty event
63103
pub fn new() -> Self {
@@ -79,19 +119,40 @@ impl Event {
79119
pub fn insert<T, S>(&mut self, key: S, value: T)
80120
where
81121
S: Into<String> + Hash + Eq,
82-
T: Into<FieldValue>,
122+
T: Into<EventValue>,
83123
{
84124
self.inner.insert(key.into(), value.into());
85125
}
86126

87127
/// Iterate over the key-value pairs in the event
88-
pub fn iter(&self) -> impl Iterator<Item = (&String, &FieldValue)> {
128+
pub fn iter(&self) -> impl Iterator<Item = (&String, &EventValue)> {
89129
self.inner.iter()
90130
}
91131

92-
/// Get the value of a field in the event
93-
pub fn get(&self, field: &String) -> Option<&FieldValue> {
94-
self.inner.get(field)
132+
/// Get the value for a key in the event
133+
pub fn get(&self, key: &str) -> Option<&EventValue> {
134+
if let Some(ev) = self.inner.get(key) {
135+
return Some(ev);
136+
}
137+
138+
let mut nested_key = key;
139+
let mut current = &self.inner;
140+
while let Some((head, tail)) = nested_key.split_once('.') {
141+
if let Some(EventValue::Map(map)) = current.get(head) {
142+
if let Some(value) = map.get(tail) {
143+
return Some(value);
144+
}
145+
current = map;
146+
nested_key = tail;
147+
} else {
148+
return None;
149+
}
150+
}
151+
None
152+
}
153+
154+
pub fn values(&self) -> impl Iterator<Item = &EventValue> {
155+
self.inner.values()
95156
}
96157
}
97158

@@ -104,8 +165,7 @@ impl TryFrom<serde_json::Value> for Event {
104165
match data {
105166
serde_json::Value::Object(data) => {
106167
for (key, value) in data {
107-
let field_value = FieldValue::try_from(value)?;
108-
result.insert(key, field_value);
168+
result.insert(key, EventValue::try_from(value)?);
109169
}
110170
}
111171
_ => return Err(Self::Error::InvalidEvent()),
@@ -118,18 +178,31 @@ impl TryFrom<serde_json::Value> for Event {
118178
#[cfg(test)]
119179
mod tests {
120180
use super::*;
181+
use serde_json::json;
121182

122183
#[test]
123184
fn test_load_from_json() {
124-
let data = r#"
125-
{
185+
let event: Event = json!({
126186
"name": "John Doe",
127-
"age": 43
128-
}"#;
129-
130-
let event: Event = serde_json::from_str(data).unwrap();
131-
132-
assert_eq!(event.inner["name"], FieldValue::from("John Doe"));
133-
assert_eq!(event.inner["age"], FieldValue::from(43));
187+
"age": 43,
188+
"address": {
189+
"city": "New York",
190+
"state": "NY"
191+
}
192+
})
193+
.try_into()
194+
.unwrap();
195+
196+
assert_eq!(event.inner["name"], EventValue::from("John Doe"));
197+
assert_eq!(event.inner["age"], EventValue::from(43));
198+
assert_eq!(
199+
event.inner["address"],
200+
EventValue::Map({
201+
let mut map = HashMap::new();
202+
map.insert("city".to_string(), EventValue::from("New York"));
203+
map.insert("state".to_string(), EventValue::from("NY"));
204+
map
205+
})
206+
);
134207
}
135208
}

0 commit comments

Comments
 (0)