Skip to content

Commit e0e2254

Browse files

17 files changed

+1408
-524
lines changed

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ repository = "https://github.com/jopohl/sigma-rust"
1414
[dependencies]
1515
base64 = "0.22.1"
1616
cidr = "0.3.0"
17-
glob-match = "0.2.1"
1817
regex = "1.11.0"
1918
serde = { version = "1.0.210", features = ["derive"] }
2019
serde_yml = "0.0.12"
@@ -24,7 +23,11 @@ serde_json = { version = "1.0.132", optional = true }
2423

2524
[dev-dependencies]
2625
walkdir = "2.5.0"
26+
criterion = { version = "0.5", features = ["html_reports"] }
2727

28+
[[bench]]
29+
name = "matching_benchmark"
30+
harness = false
2831

2932
[features]
3033
default = ["serde_json"]

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ A Rust library for parsing and evaluating Sigma rules to create custom detection
1111

1212
- Supports the [Sigma condition](https://sigmahq.io/docs/basics/conditions.html) syntax using Pratt parsing
1313
- Supports all [Sigma field modifiers](https://sigmahq.io/docs/basics/modifiers.html) except `expand`
14+
- Support
15+
for [String wildcards](https://github.com/SigmaHQ/sigma-specification/blob/main/specification/sigma-rules-specification.md#string-wildcard)
1416
- Written in 100% safe Rust
1517
- Daily automated security audit of dependencies
1618
- Extensive test suite

benches/matching_benchmark.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use criterion::{black_box, criterion_group, criterion_main, Criterion};
2+
use serde_json::json;
3+
use sigma_rust::{check_rule, Event, Rule};
4+
5+
fn rule_match_benchmark(c: &mut Criterion) {
6+
let rule_yaml = r#"
7+
title: Rule for benchmarking
8+
logsource:
9+
detection:
10+
selection_filename_suffix:
11+
TargetFilename: ':\temp\'
12+
TargetFilename|endswith: .exe
13+
selection_image_suffix:
14+
Image: ':\temp\'
15+
condition: all of them
16+
"#;
17+
18+
let rule: Rule = serde_yml::from_str(rule_yaml).unwrap();
19+
20+
let event: Event = json!( {
21+
"EventID": 4624,
22+
"LogName": "Security",
23+
"TimeCreated": "2023-10-01T12:34:56.789Z",
24+
"EventRecordID": 123456,
25+
"Channel": "Security",
26+
"Computer": "DESKTOP-1234ABCD",
27+
"UserData": {
28+
"SubjectUserName": "johndoe",
29+
"SubjectDomainName": "WORKGROUP",
30+
"SubjectLogonId": "0x123456",
31+
"TargetUserName": "johndoe",
32+
"TargetDomainName": "WORKGROUP",
33+
"TargetLogonId": "0x654321",
34+
"LogonType": 2,
35+
"LogonProcessName": "User32",
36+
"AuthenticationPackageName": "Negotiate",
37+
"WorkstationName": "DESKTOP-1234ABCD",
38+
"LogonGuid": "{00000000-0000-0000-0000-000000000000}",
39+
"TransmittedServices": "-",
40+
"LmPackageName": "-",
41+
"KeyLength": 0,
42+
"ProcessId": 1234,
43+
"ProcessName": "C:\\Windows\\System32\\winlogon.exe",
44+
"IpAddress": "192.168.1.100",
45+
"IpPort": "12345"
46+
},
47+
"SomeKey": {
48+
"SomeValue": "yes"
49+
},
50+
"TargetFilename": "C:\\temp\\autoit3.exe",
51+
"Image": "C:\\temp\\hello.au3"
52+
})
53+
.try_into()
54+
.unwrap();
55+
56+
c.bench_function("rule_match", |b| {
57+
b.iter(|| check_rule(black_box(&rule), black_box(&event)))
58+
});
59+
}
60+
61+
criterion_group!(benches, rule_match_benchmark);
62+
criterion_main!(benches);

src/basevalue.rs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
use crate::error::ParserError;
2+
use std::cmp::Ordering;
3+
4+
#[derive(Debug, Clone)]
5+
pub enum BaseValue {
6+
String(String),
7+
Int(i64),
8+
Unsigned(u64),
9+
Float(f64),
10+
Boolean(bool),
11+
Null,
12+
}
13+
14+
impl From<i32> for BaseValue {
15+
fn from(i: i32) -> Self {
16+
Self::from(i as i64)
17+
}
18+
}
19+
20+
impl From<Option<i32>> for BaseValue {
21+
fn from(option: Option<i32>) -> Self {
22+
match option {
23+
Some(i) => Self::from(i),
24+
None => Self::Null,
25+
}
26+
}
27+
}
28+
29+
impl From<i64> for BaseValue {
30+
fn from(i: i64) -> Self {
31+
Self::Int(i)
32+
}
33+
}
34+
35+
impl From<u32> for BaseValue {
36+
fn from(u: u32) -> Self {
37+
Self::from(u as u64)
38+
}
39+
}
40+
41+
impl From<u64> for BaseValue {
42+
fn from(u: u64) -> Self {
43+
Self::Unsigned(u)
44+
}
45+
}
46+
47+
impl From<f32> for BaseValue {
48+
fn from(f: f32) -> Self {
49+
Self::from(f as f64)
50+
}
51+
}
52+
53+
impl From<f64> for BaseValue {
54+
fn from(f: f64) -> Self {
55+
Self::Float(f)
56+
}
57+
}
58+
59+
impl From<bool> for BaseValue {
60+
fn from(b: bool) -> Self {
61+
Self::Boolean(b)
62+
}
63+
}
64+
65+
impl From<String> for BaseValue {
66+
fn from(s: String) -> Self {
67+
Self::String(s)
68+
}
69+
}
70+
71+
impl From<&str> for BaseValue {
72+
fn from(s: &str) -> Self {
73+
Self::from(s.to_string())
74+
}
75+
}
76+
77+
impl PartialEq for BaseValue {
78+
fn eq(&self, other: &Self) -> bool {
79+
match (self, other) {
80+
(Self::String(a), Self::String(b)) => a.eq(b),
81+
(Self::Int(a), Self::Int(b)) => a.eq(b),
82+
(Self::Unsigned(a), Self::Unsigned(b)) => a.eq(b),
83+
(Self::Float(a), Self::Float(b)) => a.eq(b),
84+
(Self::Boolean(a), Self::Boolean(b)) => a.eq(b),
85+
(Self::Null, Self::Null) => true,
86+
_ => false,
87+
}
88+
}
89+
}
90+
91+
impl PartialOrd for BaseValue {
92+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
93+
match (self, other) {
94+
(Self::String(a), Self::String(b)) => a.partial_cmp(b),
95+
(Self::Int(a), Self::Int(b)) => a.partial_cmp(b),
96+
(Self::Unsigned(a), Self::Unsigned(b)) => a.partial_cmp(b),
97+
(Self::Float(a), Self::Float(b)) => a.partial_cmp(b),
98+
(Self::Boolean(a), Self::Boolean(b)) => a.partial_cmp(b),
99+
(Self::Null, Self::Null) => Some(Ordering::Equal),
100+
_ => None,
101+
}
102+
}
103+
}
104+
105+
impl BaseValue {
106+
pub(crate) fn value_to_string(&self) -> String {
107+
match self {
108+
Self::String(s) => s.to_string(),
109+
Self::Int(i) => i.to_string(),
110+
Self::Float(f) => f.to_string(),
111+
Self::Unsigned(u) => u.to_string(),
112+
Self::Boolean(b) => b.to_string(),
113+
Self::Null => "".to_string(),
114+
}
115+
}
116+
}
117+
118+
macro_rules! number {
119+
($n:expr) => {
120+
if let Some(i) = $n.as_i64() {
121+
Ok(Self::Int(i))
122+
} else if let Some(u) = $n.as_u64() {
123+
Ok(Self::Unsigned(u))
124+
} else {
125+
Ok(Self::Float(
126+
$n.as_f64().expect("Number is neither Int nor Unsigned"),
127+
))
128+
}
129+
};
130+
}
131+
132+
#[cfg(feature = "serde_json")]
133+
impl TryFrom<serde_json::Value> for BaseValue {
134+
type Error = crate::error::JSONError;
135+
136+
fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
137+
match value {
138+
serde_json::Value::String(s) => Ok(Self::String(s)),
139+
serde_json::Value::Number(n) => number!(n),
140+
serde_json::Value::Bool(b) => Ok(Self::Boolean(b)),
141+
serde_json::Value::Null => Ok(Self::Null),
142+
_ => Err(Self::Error::InvalidFieldValue(format!("{:?}", value))),
143+
}
144+
}
145+
}
146+
147+
impl TryFrom<serde_yml::Value> for BaseValue {
148+
type Error = ParserError;
149+
150+
fn try_from(value: serde_yml::Value) -> Result<Self, Self::Error> {
151+
match value {
152+
serde_yml::Value::Bool(b) => Ok(Self::Boolean(b)),
153+
serde_yml::Value::Number(n) => number!(n),
154+
serde_yml::Value::String(s) => Ok(Self::String(s)),
155+
serde_yml::Value::Null => Ok(Self::Null),
156+
_ => Err(ParserError::InvalidYAML(format!("{:?}", value))),
157+
}
158+
}
159+
}
160+
161+
#[cfg(test)]
162+
mod tests {
163+
use super::*;
164+
165+
#[allow(clippy::neg_cmp_op_on_partial_ord)]
166+
#[test]
167+
fn test_field_value_type() {
168+
assert_eq!(BaseValue::from("1"), BaseValue::String("1".to_string()));
169+
assert_eq!(BaseValue::from("2"), BaseValue::String("2".to_string()));
170+
assert_eq!(BaseValue::from(Some(42)), BaseValue::Int(42));
171+
assert_eq!(BaseValue::from(2u32), BaseValue::Unsigned(2));
172+
assert_eq!(BaseValue::from(3f32), BaseValue::Float(3.0));
173+
assert_ne!(BaseValue::from("1"), BaseValue::from("3"));
174+
assert_ne!(BaseValue::from("2"), BaseValue::Int(2_i64));
175+
assert_ne!(BaseValue::Int(3), BaseValue::Float(3.0));
176+
177+
assert!(BaseValue::Int(10) < BaseValue::Int(20));
178+
assert!(!(BaseValue::Int(20) < BaseValue::from("30")));
179+
assert!(!(BaseValue::Int(20) < BaseValue::Float(30.0)));
180+
assert!(!(BaseValue::Int(34) < BaseValue::Float(30.0)));
181+
assert!(BaseValue::Boolean(false) < BaseValue::Boolean(true));
182+
assert!(BaseValue::Int(10) >= BaseValue::Int(10));
183+
assert!(BaseValue::Int(10) > BaseValue::Int(4));
184+
assert!(BaseValue::Int(10) >= BaseValue::Int(4));
185+
assert!(
186+
BaseValue::from(18446744073709551615_u64) > BaseValue::from(18446744073709551614_u64)
187+
);
188+
assert_eq!(
189+
BaseValue::from(18446744073709551615_u64),
190+
BaseValue::from(18446744073709551615_u64)
191+
);
192+
193+
let yaml = r#"
194+
EventID: 18446744073709551615
195+
"#;
196+
let v: serde_yml::Value = serde_yml::from_str(yaml).unwrap();
197+
let base_value = BaseValue::try_from(v["EventID"].clone()).unwrap();
198+
assert_eq!(base_value, BaseValue::Unsigned(18446744073709551615));
199+
}
200+
}

src/detection.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::detection::ast::Ast;
55
use crate::error::ParserError;
66
use crate::event::Event;
77
use crate::selection::Selection;
8-
use glob_match::glob_match;
8+
use crate::wildcard::match_tokenized;
99
use serde::Deserialize;
1010
use serde_yml::Value;
1111
use std::collections::HashMap;
@@ -121,7 +121,7 @@ impl Detection {
121121
Ast::OneOf(s) => self
122122
.selections
123123
.keys()
124-
.filter(|name| glob_match(s, name))
124+
.filter(|name| match_tokenized(s, name, false))
125125
.any(|name| self.evaluate_selection(name, lookup, event)),
126126
Ast::OneOfThem => self
127127
.selections
@@ -130,7 +130,7 @@ impl Detection {
130130
Ast::AllOf(s) => self
131131
.selections
132132
.keys()
133-
.filter(|name| glob_match(s, name))
133+
.filter(|name| match_tokenized(s, name, false))
134134
.all(|name| self.evaluate_selection(name, lookup, event)),
135135
Ast::AllOfThem => self
136136
.selections
@@ -242,15 +242,15 @@ mod tests {
242242
#[test]
243243
fn test_evaluate_all_of() {
244244
let detection_yaml = r#"
245-
selection_1:
245+
selection_1x:
246246
EventID: 6416
247247
RandomID|contains:
248248
- ab
249249
- cd
250250
- ed
251-
selection_2:
251+
selection_2x:
252252
EventID: 5555
253-
condition: all of selection*
253+
condition: all of sel*tion*x
254254
"#;
255255

256256
let mut event = Event::from([("EventID", 6416)]);

0 commit comments

Comments
 (0)