use std::net::IpAddr;
use crate::ast::{ConditionExpr, Detection, DetectionItem, Modifier};
use crate::emit::modifier_str;
use crate::error::{Result, SigmaParserError};
use crate::value::SigmaValue;
use Modifier::*;
const STRING_OPERATORS: [Modifier; 3] = [Contains, StartsWith, EndsWith];
const NUMERIC_COMPARISONS: [Modifier; 4] = [Gt, Gte, Lt, Lte];
const TIMESTAMP_PARTS: [Modifier; 6] = [Minute, Hour, Day, Week, Month, Year];
const UTF16_ENCODINGS: [Modifier; 3] = [Wide, Utf16, Utf16be];
const VALUE_TRANSFORMS: [Modifier; 7] =
[Base64, Base64Offset, Wide, Utf16, Utf16be, WindAsh, Expand];
const REGEX_FLAGS: [Modifier; 3] = [IgnoreCase, Multiline, DotAll];
const STRING_ONLY: [Modifier; 14] = [
Contains,
StartsWith,
EndsWith,
Base64,
Base64Offset,
Wide,
Utf16,
Utf16be,
WindAsh,
Re,
Cidr,
Expand,
FieldRef,
Cased,
];
pub fn check_modifiers(modifiers: &[Modifier]) -> std::result::Result<(), String> {
let has = |m: Modifier| modifiers.contains(&m);
let present = |set: &[Modifier]| -> Vec<&'static str> {
set.iter()
.copied()
.filter(|m| has(*m))
.map(modifier_str)
.collect()
};
let numeric = NUMERIC_COMPARISONS.iter().any(|m| has(*m));
let timestamp = TIMESTAMP_PARTS.iter().any(|m| has(*m));
let fieldref_conflicts = has(Re)
|| has(Cidr)
|| has(Exists)
|| numeric
|| timestamp
|| present(&STRING_OPERATORS).len() > 1;
let mut operators = present(&[Contains, StartsWith, EndsWith, Re, Cidr, Exists]);
if has(FieldRef) && fieldref_conflicts {
operators.push("fieldref");
}
operators.extend(present(&NUMERIC_COMPARISONS));
operators.extend(
modifiers
.iter()
.filter(|m| TIMESTAMP_PARTS.contains(m))
.map(|m| modifier_str(*m)),
);
if has(Cased) && (has(Re) || has(Cidr) || has(Exists) || numeric || timestamp) {
operators.push("cased");
}
if has(FieldRef)
&& !fieldref_conflicts
&& let Some(name) = string_operator_before_fieldref(modifiers)
{
return Err(format!("|{name} must follow |fieldref"));
}
if operators.len() > 1 {
return Err(format!(
"at most one operator may be set per field; got |{}",
operators.join(", |")
));
}
let encodings = present(&UTF16_ENCODINGS);
if encodings.len() > 1 {
return Err(format!(
"|wide, |utf16, and |utf16be are mutually exclusive UTF-16 encodings; got |{}",
encodings.join(", |")
));
}
if has(Base64) && has(Base64Offset) {
return Err(
"|base64 and |base64offset are mutually exclusive base64 strategies; pick one".into(),
);
}
let non_string_operator =
has(Re) || has(Cidr) || has(Exists) || has(FieldRef) || numeric || timestamp;
let transforms = present(&VALUE_TRANSFORMS);
if non_string_operator && !transforms.is_empty() {
return Err(format!(
"value transformations |{} only apply to string match operators (default eq, \
contains, startswith, endswith) and cannot be combined with the operator that \
is also set on this field",
transforms.join(", |")
));
}
let flags = present(®EX_FLAGS);
if !has(Re) && !flags.is_empty() {
return Err(format!(
"regex flag modifiers |{} have no effect without |re; case sensitivity for \
substring or equality matching is controlled by |cased (or its absence, which \
keeps the default case-insensitive behavior)",
flags.join(", |")
));
}
if let Some(re_at) = modifiers.iter().position(|m| *m == Re)
&& modifiers[..re_at].iter().any(|m| REGEX_FLAGS.contains(m))
{
return Err("regex flag modifiers |i, |m, and |s must follow |re".into());
}
Ok(())
}
fn string_operator_before_fieldref(modifiers: &[Modifier]) -> Option<&'static str> {
let fieldref_at = modifiers.iter().position(|m| *m == FieldRef)?;
modifiers[..fieldref_at]
.iter()
.find(|m| STRING_OPERATORS.contains(m))
.map(|m| modifier_str(*m))
}
pub fn check_cidr(cidr: &str) -> std::result::Result<(), String> {
let invalid = |reason: &str| format!("invalid CIDR expression '{cidr}': {reason}");
let (addr, prefix) = cidr
.split_once('/')
.ok_or_else(|| invalid("expected address/prefix"))?;
let addr: IpAddr = addr.parse().map_err(|_| invalid("invalid IP address"))?;
let (bits, width) = match addr {
IpAddr::V4(a) => (u128::from(u32::from(a)), 32),
IpAddr::V6(a) => (u128::from(a), 128),
};
let prefix: u32 = prefix
.parse()
.ok()
.filter(|p| *p <= width)
.ok_or_else(|| invalid("invalid prefix length"))?;
let host_mask = u128::MAX.checked_shr(128 - width + prefix).unwrap_or(0);
if bits & host_mask != 0 {
return Err(invalid("host bits set"));
}
Ok(())
}
pub fn check_regex(pattern: &str) -> std::result::Result<(), String> {
if regex_syntax::Parser::new().parse(pattern).is_ok() {
return Ok(());
}
fancy_regex::Regex::new(pattern)
.map(drop)
.map_err(|e| e.to_string())
}
pub fn exists_flag(value: &SigmaValue) -> Option<bool> {
match value {
SigmaValue::Bool(b) => Some(*b),
SigmaValue::String(_)
| SigmaValue::Integer(_)
| SigmaValue::Float(_)
| SigmaValue::Null => None,
}
}
pub fn check_detection_item(item: &DetectionItem) -> Result<()> {
let modifiers = &item.field.modifiers;
let has = |m: Modifier| modifiers.contains(&m);
let subject = match &item.field.name {
Some(name) => format!("field '{name}'"),
None => "keyword".to_string(),
};
let invalid = |msg: String| SigmaParserError::InvalidValue(format!("{subject}: {msg}"));
check_modifiers(modifiers)
.map_err(|e| SigmaParserError::InvalidModifiers(format!("{subject}: {e}")))?;
if has(Exists) {
if item.field.name.is_none() {
return Err(invalid("|exists must be applied to a field".into()));
}
return match item.values.as_slice() {
[SigmaValue::Bool(_)] => Ok(()),
_ => Err(invalid(
"|exists takes a single boolean value, true or false".into(),
)),
};
}
if has(All) && item.values.is_empty() {
return Err(SigmaParserError::InvalidModifiers(format!(
"{subject}: |all requires at least one value"
)));
}
if item.values.is_empty() && item.field.name.is_none() {
return Err(invalid(
"an empty value list must be bound to a field".into(),
));
}
let numeric = NUMERIC_COMPARISONS
.iter()
.chain(&TIMESTAMP_PARTS)
.copied()
.find(|m| has(*m));
let string_only = STRING_ONLY.iter().copied().find(|m| has(*m));
let base64 = has(Base64) || has(Base64Offset);
let utf16 = UTF16_ENCODINGS.iter().any(|m| has(*m));
for value in &item.values {
if let Some(m) = numeric {
if !is_numeric(value) {
return Err(invalid(format!(
"|{} requires a numeric value, got {}",
modifier_str(m),
describe(value)
)));
}
continue;
}
let Some(m) = string_only else { continue };
let SigmaValue::String(s) = value else {
return Err(invalid(format!(
"|{} requires a string value, got {}",
modifier_str(m),
describe(value)
)));
};
if has(Re) {
check_regex(&s.original)
.map_err(|e| invalid(format!("invalid regular expression: {e}")))?;
}
if has(Cidr) {
check_cidr(&s.as_plain().unwrap_or_else(|| s.original.clone())).map_err(invalid)?;
}
if has(FieldRef) && s.contains_wildcards() {
return Err(invalid(
"a field reference must not contain wildcards".into(),
));
}
if base64 && s.contains_wildcards() {
return Err(invalid(
"|base64 and |base64offset do not support wildcards; escape * and ? as \\* and \\? to match them literally".into(),
));
}
if utf16 && !base64 && !s.original.is_ascii() {
return Err(invalid(
"|wide, |utf16, and |utf16be without |base64 or |base64offset require an ASCII value".into(),
));
}
}
Ok(())
}
fn is_numeric(value: &SigmaValue) -> bool {
matches!(value, SigmaValue::Integer(_) | SigmaValue::Float(_))
}
fn describe(value: &SigmaValue) -> String {
match value {
SigmaValue::String(s) => format!("'{s}'"),
other => other.to_string(),
}
}
pub(crate) fn check_detection(name: &str, detection: &Detection) -> Result<()> {
let invalid = |msg: &str| SigmaParserError::InvalidDetection(format!("'{name}' {msg}"));
match detection {
Detection::AllOf(items) if items.is_empty() => Err(invalid("is empty")),
Detection::AllOf(_) => Ok(()),
Detection::AnyOf(subs) | Detection::And(subs) => {
if subs.is_empty() {
return Err(invalid("is empty"));
}
subs.iter().try_for_each(|d| check_detection(name, d))
}
Detection::Keywords(values) => {
if values.is_empty() {
return Err(invalid("is empty"));
}
if values.iter().any(|v| matches!(v, SigmaValue::Null)) {
return Err(invalid(
"uses null as a keyword; keywords match text anywhere in the event",
));
}
Ok(())
}
Detection::ArrayMatch { body, .. } => check_detection(name, body),
Detection::Conditional { named, condition } => {
for (sub_name, sub) in named {
check_detection(sub_name, sub)?;
}
let names: Vec<&str> = named.keys().map(String::as_str).collect();
check_condition(condition, &names)
}
}
}
pub(crate) fn check_condition(expr: &ConditionExpr, names: &[&str]) -> Result<()> {
match expr {
ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
exprs.iter().try_for_each(|e| check_condition(e, names))
}
ConditionExpr::Not(inner) => check_condition(inner, names),
ConditionExpr::Identifier(id) => {
if names.contains(&id.as_str()) {
Ok(())
} else {
Err(SigmaParserError::InvalidDetection(format!(
"condition references unknown detection identifier '{id}'"
)))
}
}
ConditionExpr::Selector {
quantifier,
pattern,
} => {
if names.iter().any(|n| pattern.matches_detection_name(n)) {
Ok(())
} else {
Err(SigmaParserError::InvalidDetection(format!(
"selector '{quantifier} of {pattern}' matches no detection identifier"
)))
}
}
}
}