1use std::net::IpAddr;
9
10use crate::ast::{ConditionExpr, Detection, DetectionItem, Modifier};
11use crate::emit::modifier_str;
12use crate::error::{Result, SigmaParserError};
13use crate::value::SigmaValue;
14
15use Modifier::*;
16
17const STRING_OPERATORS: [Modifier; 3] = [Contains, StartsWith, EndsWith];
18const NUMERIC_COMPARISONS: [Modifier; 4] = [Gt, Gte, Lt, Lte];
19const TIMESTAMP_PARTS: [Modifier; 6] = [Minute, Hour, Day, Week, Month, Year];
20const UTF16_ENCODINGS: [Modifier; 3] = [Wide, Utf16, Utf16be];
21const VALUE_TRANSFORMS: [Modifier; 7] =
22 [Base64, Base64Offset, Wide, Utf16, Utf16be, WindAsh, Expand];
23const REGEX_FLAGS: [Modifier; 3] = [IgnoreCase, Multiline, DotAll];
24const STRING_ONLY: [Modifier; 14] = [
25 Contains,
26 StartsWith,
27 EndsWith,
28 Base64,
29 Base64Offset,
30 Wide,
31 Utf16,
32 Utf16be,
33 WindAsh,
34 Re,
35 Cidr,
36 Expand,
37 FieldRef,
38 Cased,
39];
40
41pub fn check_modifiers(modifiers: &[Modifier]) -> std::result::Result<(), String> {
57 let has = |m: Modifier| modifiers.contains(&m);
58 let present = |set: &[Modifier]| -> Vec<&'static str> {
59 set.iter()
60 .copied()
61 .filter(|m| has(*m))
62 .map(modifier_str)
63 .collect()
64 };
65 let numeric = NUMERIC_COMPARISONS.iter().any(|m| has(*m));
66 let timestamp = TIMESTAMP_PARTS.iter().any(|m| has(*m));
67 let fieldref_conflicts = has(Re)
69 || has(Cidr)
70 || has(Exists)
71 || numeric
72 || timestamp
73 || present(&STRING_OPERATORS).len() > 1;
74
75 let mut operators = present(&[Contains, StartsWith, EndsWith, Re, Cidr, Exists]);
76 if has(FieldRef) && fieldref_conflicts {
77 operators.push("fieldref");
78 }
79 operators.extend(present(&NUMERIC_COMPARISONS));
80 operators.extend(
81 modifiers
82 .iter()
83 .filter(|m| TIMESTAMP_PARTS.contains(m))
84 .map(|m| modifier_str(*m)),
85 );
86 if has(Cased) && (has(Re) || has(Cidr) || has(Exists) || numeric || timestamp) {
87 operators.push("cased");
88 }
89 if has(FieldRef)
90 && !fieldref_conflicts
91 && let Some(name) = string_operator_before_fieldref(modifiers)
92 {
93 return Err(format!("|{name} must follow |fieldref"));
94 }
95 if operators.len() > 1 {
96 return Err(format!(
97 "at most one operator may be set per field; got |{}",
98 operators.join(", |")
99 ));
100 }
101
102 let encodings = present(&UTF16_ENCODINGS);
103 if encodings.len() > 1 {
104 return Err(format!(
105 "|wide, |utf16, and |utf16be are mutually exclusive UTF-16 encodings; got |{}",
106 encodings.join(", |")
107 ));
108 }
109 if has(Base64) && has(Base64Offset) {
110 return Err(
111 "|base64 and |base64offset are mutually exclusive base64 strategies; pick one".into(),
112 );
113 }
114
115 let non_string_operator =
116 has(Re) || has(Cidr) || has(Exists) || has(FieldRef) || numeric || timestamp;
117 let transforms = present(&VALUE_TRANSFORMS);
118 if non_string_operator && !transforms.is_empty() {
119 return Err(format!(
120 "value transformations |{} only apply to string match operators (default eq, \
121 contains, startswith, endswith) and cannot be combined with the operator that \
122 is also set on this field",
123 transforms.join(", |")
124 ));
125 }
126
127 let flags = present(®EX_FLAGS);
128 if !has(Re) && !flags.is_empty() {
129 return Err(format!(
130 "regex flag modifiers |{} have no effect without |re; case sensitivity for \
131 substring or equality matching is controlled by |cased (or its absence, which \
132 keeps the default case-insensitive behavior)",
133 flags.join(", |")
134 ));
135 }
136 if let Some(re_at) = modifiers.iter().position(|m| *m == Re)
137 && modifiers[..re_at].iter().any(|m| REGEX_FLAGS.contains(m))
138 {
139 return Err("regex flag modifiers |i, |m, and |s must follow |re".into());
140 }
141
142 Ok(())
143}
144
145fn string_operator_before_fieldref(modifiers: &[Modifier]) -> Option<&'static str> {
148 let fieldref_at = modifiers.iter().position(|m| *m == FieldRef)?;
149 modifiers[..fieldref_at]
150 .iter()
151 .find(|m| STRING_OPERATORS.contains(m))
152 .map(|m| modifier_str(*m))
153}
154
155pub fn check_cidr(cidr: &str) -> std::result::Result<(), String> {
168 let invalid = |reason: &str| format!("invalid CIDR expression '{cidr}': {reason}");
169 let (addr, prefix) = cidr
170 .split_once('/')
171 .ok_or_else(|| invalid("expected address/prefix"))?;
172 let addr: IpAddr = addr.parse().map_err(|_| invalid("invalid IP address"))?;
173 let (bits, width) = match addr {
174 IpAddr::V4(a) => (u128::from(u32::from(a)), 32),
175 IpAddr::V6(a) => (u128::from(a), 128),
176 };
177 let prefix: u32 = prefix
178 .parse()
179 .ok()
180 .filter(|p| *p <= width)
181 .ok_or_else(|| invalid("invalid prefix length"))?;
182 let host_mask = u128::MAX.checked_shr(128 - width + prefix).unwrap_or(0);
183 if bits & host_mask != 0 {
184 return Err(invalid("host bits set"));
185 }
186 Ok(())
187}
188
189pub fn check_regex(pattern: &str) -> std::result::Result<(), String> {
205 if regex_syntax::Parser::new().parse(pattern).is_ok() {
206 return Ok(());
207 }
208 fancy_regex::Regex::new(pattern)
209 .map(drop)
210 .map_err(|e| e.to_string())
211}
212
213pub fn exists_flag(value: &SigmaValue) -> Option<bool> {
215 match value {
216 SigmaValue::Bool(b) => Some(*b),
217 SigmaValue::String(_)
218 | SigmaValue::Integer(_)
219 | SigmaValue::Float(_)
220 | SigmaValue::Null => None,
221 }
222}
223
224pub fn check_detection_item(item: &DetectionItem) -> Result<()> {
227 let modifiers = &item.field.modifiers;
228 let has = |m: Modifier| modifiers.contains(&m);
229 let subject = match &item.field.name {
230 Some(name) => format!("field '{name}'"),
231 None => "keyword".to_string(),
232 };
233 let invalid = |msg: String| SigmaParserError::InvalidValue(format!("{subject}: {msg}"));
234
235 check_modifiers(modifiers)
236 .map_err(|e| SigmaParserError::InvalidModifiers(format!("{subject}: {e}")))?;
237
238 if has(Exists) {
239 if item.field.name.is_none() {
240 return Err(invalid("|exists must be applied to a field".into()));
241 }
242 return match item.values.as_slice() {
243 [SigmaValue::Bool(_)] => Ok(()),
244 _ => Err(invalid(
245 "|exists takes a single boolean value, true or false".into(),
246 )),
247 };
248 }
249 if has(All) && item.values.is_empty() {
250 return Err(SigmaParserError::InvalidModifiers(format!(
251 "{subject}: |all requires at least one value"
252 )));
253 }
254 if item.values.is_empty() && item.field.name.is_none() {
255 return Err(invalid(
256 "an empty value list must be bound to a field".into(),
257 ));
258 }
259
260 let numeric = NUMERIC_COMPARISONS
261 .iter()
262 .chain(&TIMESTAMP_PARTS)
263 .copied()
264 .find(|m| has(*m));
265 let string_only = STRING_ONLY.iter().copied().find(|m| has(*m));
266 let base64 = has(Base64) || has(Base64Offset);
267 let utf16 = UTF16_ENCODINGS.iter().any(|m| has(*m));
268
269 for value in &item.values {
270 if let Some(m) = numeric {
271 if !is_numeric(value) {
272 return Err(invalid(format!(
273 "|{} requires a numeric value, got {}",
274 modifier_str(m),
275 describe(value)
276 )));
277 }
278 continue;
279 }
280 let Some(m) = string_only else { continue };
281 let SigmaValue::String(s) = value else {
282 return Err(invalid(format!(
283 "|{} requires a string value, got {}",
284 modifier_str(m),
285 describe(value)
286 )));
287 };
288 if has(Re) {
289 check_regex(&s.original)
290 .map_err(|e| invalid(format!("invalid regular expression: {e}")))?;
291 }
292 if has(Cidr) {
293 check_cidr(&s.as_plain().unwrap_or_else(|| s.original.clone())).map_err(invalid)?;
294 }
295 if has(FieldRef) && s.contains_wildcards() {
296 return Err(invalid(
297 "a field reference must not contain wildcards".into(),
298 ));
299 }
300 if base64 && s.contains_wildcards() {
301 return Err(invalid(
302 "|base64 and |base64offset do not support wildcards; escape * and ? as \\* and \\? to match them literally".into(),
303 ));
304 }
305 if utf16 && !base64 && !s.original.is_ascii() {
306 return Err(invalid(
307 "|wide, |utf16, and |utf16be without |base64 or |base64offset require an ASCII value".into(),
308 ));
309 }
310 }
311 Ok(())
312}
313
314fn is_numeric(value: &SigmaValue) -> bool {
315 matches!(value, SigmaValue::Integer(_) | SigmaValue::Float(_))
316}
317
318fn describe(value: &SigmaValue) -> String {
319 match value {
320 SigmaValue::String(s) => format!("'{s}'"),
321 other => other.to_string(),
322 }
323}
324
325pub(crate) fn check_detection(name: &str, detection: &Detection) -> Result<()> {
328 let invalid = |msg: &str| SigmaParserError::InvalidDetection(format!("'{name}' {msg}"));
329 match detection {
330 Detection::AllOf(items) if items.is_empty() => Err(invalid("is empty")),
331 Detection::AllOf(_) => Ok(()),
332 Detection::AnyOf(subs) | Detection::And(subs) => {
333 if subs.is_empty() {
334 return Err(invalid("is empty"));
335 }
336 subs.iter().try_for_each(|d| check_detection(name, d))
337 }
338 Detection::Keywords(values) => {
339 if values.is_empty() {
340 return Err(invalid("is empty"));
341 }
342 if values.iter().any(|v| matches!(v, SigmaValue::Null)) {
343 return Err(invalid(
344 "uses null as a keyword; keywords match text anywhere in the event",
345 ));
346 }
347 Ok(())
348 }
349 Detection::ArrayMatch { body, .. } => check_detection(name, body),
350 Detection::Conditional { named, condition } => {
351 for (sub_name, sub) in named {
352 check_detection(sub_name, sub)?;
353 }
354 let names: Vec<&str> = named.keys().map(String::as_str).collect();
355 check_condition(condition, &names)
356 }
357 }
358}
359
360pub(crate) fn check_condition(expr: &ConditionExpr, names: &[&str]) -> Result<()> {
363 match expr {
364 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
365 exprs.iter().try_for_each(|e| check_condition(e, names))
366 }
367 ConditionExpr::Not(inner) => check_condition(inner, names),
368 ConditionExpr::Identifier(id) => {
369 if names.contains(&id.as_str()) {
370 Ok(())
371 } else {
372 Err(SigmaParserError::InvalidDetection(format!(
373 "condition references unknown detection identifier '{id}'"
374 )))
375 }
376 }
377 ConditionExpr::Selector {
378 quantifier,
379 pattern,
380 } => {
381 if names.iter().any(|n| pattern.matches_detection_name(n)) {
382 Ok(())
383 } else {
384 Err(SigmaParserError::InvalidDetection(format!(
385 "selector '{quantifier} of {pattern}' matches no detection identifier"
386 )))
387 }
388 }
389 }
390}