rsigma_ir/hir.rs
1//! Intermediate representation types for Sigma rules.
2//!
3//! The HIR captures the full semantic meaning of a Sigma rule after static
4//! pipeline transforms. It is:
5//!
6//! - **modifier-resolved**: `FieldSpec + modifiers + SigmaValue` lowers into
7//! explicit [`IrMatcher`] variants.
8//! - **Selector-preserving**: [`IrCondition::Selector`] keeps the quantifier
9//! and name pattern so counting and reported matched-selections stay
10//! identical to native evaluation (no boolean expansion).
11//! - **Array-scope complete**: [`IrDetection`] mirrors
12//! `CompiledDetection::{ArrayMatch, And, Conditional}`.
13//! - **Faithful and lossless**: string matches keep a wildcard-aware,
14//! original-case [`IrPattern`]; encoding modifiers stay explicit as
15//! [`IrEncoding`]. Lowering never lowercases, compiles regexes, or expands
16//! encodings, so both eval (at compile time) and convert (at emit time)
17//! render it exactly.
18//! - **`RuleHeader`-projecting**: [`IrRuleMetadata`] is a superset; compile
19//! projects the subset used by `rsigma_eval::result::RuleHeader`.
20
21use std::collections::HashMap;
22
23use rsigma_parser::{
24 ArrayQuantifier, CorrelationCondition, CorrelationType, FieldAlias, FilterRuleTarget, Level,
25 LogSource, Quantifier, Related, SelectorPattern, Status, Timespan, WindowMode,
26};
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29
30// =============================================================================
31// IrRule
32// =============================================================================
33
34/// Top-level detection rule in the intermediate representation.
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct IrRule {
37 pub metadata: IrRuleMetadata,
38 pub logsource: LogSource,
39 /// Sigma major from `sigma-version` (gates array-matching, etc.).
40 pub sigma_version: Option<u32>,
41 pub detections: HashMap<String, IrDetection>,
42 pub conditions: Vec<IrCondition>,
43}
44
45// =============================================================================
46// IrRuleMetadata
47// =============================================================================
48
49/// Metadata carried by an [`IrRule`] / [`IrCorrelation`] / [`IrFilter`].
50///
51/// Superset of `rsigma_eval::result::RuleHeader` plus the rest of the Sigma
52/// rule metadata convert and offline tools need.
53#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
54pub struct IrRuleMetadata {
55 pub title: String,
56 pub id: Option<String>,
57 pub name: Option<String>,
58 pub level: Option<Level>,
59 pub tags: Vec<String>,
60 pub status: Option<Status>,
61 pub description: Option<String>,
62 pub author: Option<String>,
63 pub date: Option<String>,
64 pub modified: Option<String>,
65 pub references: Vec<String>,
66 pub falsepositives: Vec<String>,
67 pub fields: Vec<String>,
68 pub related: Vec<Related>,
69 pub license: Option<String>,
70 pub taxonomy: Option<String>,
71 pub scope: Vec<String>,
72 /// Arbitrary / `custom_attributes:` / pipeline-set keys. Behavior-driving
73 /// `rsigma.*` keys must survive here.
74 pub custom_attributes: HashMap<String, Value>,
75 /// Optional affinity hint for pack consumers; schema routing stays outside IR.
76 pub schema_affinity: Option<Vec<String>>,
77}
78
79// =============================================================================
80// IrDetection
81// =============================================================================
82
83/// Detection definition — semantic shape, independent of compiled matchers.
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub enum IrDetection {
86 AllOf(Vec<IrDetectionItem>),
87 AnyOf(Vec<IrDetection>),
88 Keywords(IrMatcher),
89 ArrayMatch {
90 field: String,
91 quantifier: ArrayQuantifier,
92 body: Box<IrDetection>,
93 },
94 /// Heterogeneous AND of plain items and nested `ArrayMatch` blocks.
95 And(Vec<IrDetection>),
96 Conditional {
97 named: HashMap<String, IrDetection>,
98 condition: IrCondition,
99 },
100}
101
102// =============================================================================
103// IrDetectionItem
104// =============================================================================
105
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107pub struct IrDetectionItem {
108 pub field: Option<String>,
109 pub matcher: IrMatcher,
110 pub exists: Option<bool>,
111}
112
113// =============================================================================
114// IrMatcher
115// =============================================================================
116
117/// String match operator.
118///
119/// The comparison is decided here rather than re-derived from modifiers by
120/// each consumer. `Exact` is full-value equality.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122pub enum IrStrOp {
123 Exact,
124 Contains,
125 StartsWith,
126 EndsWith,
127}
128
129/// A backend-neutral string pattern: decoded literal segments interleaved with
130/// wildcards, **original case preserved**.
131///
132/// This is the lossless heart of the faithful HIR. Eval lowercases (for
133/// case-insensitive matching) and compiles wildcards into a regex at compile
134/// time; convert renders the wildcards into backend-native tokens. Neither
135/// transform happens during lowering, so the pattern round-trips exactly.
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub enum IrPatternPart {
138 Literal(String),
139 /// `*` — matches any run of characters.
140 WildcardMulti,
141 /// `?` — matches any single character.
142 WildcardSingle,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
146pub struct IrPattern {
147 pub parts: Vec<IrPatternPart>,
148}
149
150impl IrPattern {
151 /// A pattern with no wildcards.
152 pub fn is_plain(&self) -> bool {
153 self.parts
154 .iter()
155 .all(|p| matches!(p, IrPatternPart::Literal(_)))
156 }
157
158 /// Whether the pattern contains any wildcard.
159 pub fn has_wildcards(&self) -> bool {
160 !self.is_plain()
161 }
162
163 /// The concatenated literal text if the pattern is plain.
164 pub fn as_plain(&self) -> Option<String> {
165 if !self.is_plain() {
166 return None;
167 }
168 let mut s = String::new();
169 for p in &self.parts {
170 if let IrPatternPart::Literal(t) = p {
171 s.push_str(t);
172 }
173 }
174 Some(s)
175 }
176}
177
178/// A value transformation applied before matching (an encoding modifier).
179///
180/// Kept explicit rather than pre-expanded so consumers can either replay the
181/// transform (eval) or reject it as inexpressible (convert backends).
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183pub enum IrEncoding {
184 Wide,
185 Utf16,
186 Utf16Be,
187 Base64,
188 Base64Offset,
189 Windash,
190}
191
192/// Resolved match operation. Grow by appending variants and bumping the pack
193/// IR schema major. No `Unknown` catch-all.
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
195pub enum IrMatcher {
196 /// Structured string match (equality/substring/prefix/suffix) over a
197 /// wildcard-aware, original-case [`IrPattern`].
198 Str {
199 op: IrStrOp,
200 pattern: IrPattern,
201 case_insensitive: bool,
202 },
203 /// Encoding-transformed string match (`base64`, `base64offset`, `wide`,
204 /// `utf16`, `utf16be`, `windash`). `pattern` is the untransformed value;
205 /// it has wildcards only when no base64 encoding applies. Eval replays
206 /// `encodings` to build the concrete matcher; convert backends that cannot
207 /// express the transform reject it.
208 Encoded {
209 encodings: Vec<IrEncoding>,
210 op: IrStrOp,
211 pattern: IrPattern,
212 case_insensitive: bool,
213 },
214 /// Explicit regex (`|re`) with raw pattern and flags kept separate so eval
215 /// compiles them and convert renders them (case-sensitive vs insensitive).
216 ///
217 /// `case_insensitive` is the `|i` flag (eval's regex case sensitivity).
218 /// `cased` records the `|cased` modifier, which eval ignores for regex but
219 /// some backends use to choose a case-sensitive regex operator.
220 Regex {
221 pattern: String,
222 case_insensitive: bool,
223 multiline: bool,
224 dotall: bool,
225 cased: bool,
226 },
227 Cidr {
228 network: String,
229 },
230 NumericEq(IrNumber),
231 NumericGt(IrNumber),
232 NumericGte(IrNumber),
233 NumericLt(IrNumber),
234 NumericLte(IrNumber),
235 Exists(bool),
236 /// Field-to-field comparison. `op` is equality, or a substring,
237 /// prefix, or suffix check against the referenced field's value
238 /// (`fieldref` followed by `contains`, `startswith`, or `endswith`).
239 FieldRef {
240 field: String,
241 op: IrStrOp,
242 case_insensitive: bool,
243 },
244 Null,
245 BoolEq(bool),
246 /// `expand` value whose `%name%` placeholders no pipeline resolved; they
247 /// are filled from the event field of the same name at match time and
248 /// the result is compared with `op`.
249 Expand {
250 template: Vec<IrExpandPart>,
251 op: IrStrOp,
252 case_insensitive: bool,
253 },
254 TimestampPart {
255 part: IrTimePart,
256 inner: Box<IrMatcher>,
257 },
258 Not(Box<IrMatcher>),
259 AnyOf(Vec<IrMatcher>),
260 AllOf(Vec<IrMatcher>),
261}
262
263#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
264pub enum IrExpandPart {
265 Literal(String),
266 /// `%name%` placeholder.
267 Placeholder(String),
268}
269
270/// Mirrors `rsigma_eval::matcher::TimePart`.
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
272pub enum IrTimePart {
273 Minute,
274 Hour,
275 Day,
276 Week,
277 Month,
278 Year,
279}
280
281// =============================================================================
282// IrNumber
283// =============================================================================
284
285/// Numeric literal or deferred source reference.
286#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
287pub enum IrNumber {
288 Literal(f64),
289 DynamicSourceRef {
290 source_id: String,
291 extract: Option<IrExtractExpr>,
292 },
293}
294
295#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
296pub enum IrExtractExpr {
297 Jq(String),
298 JsonPath(String),
299 Cel(String),
300}
301
302// =============================================================================
303// IrCondition
304// =============================================================================
305
306/// Selector-free condition expression.
307///
308/// A quantified selector keeps its [`Quantifier`] and [`SelectorPattern`]
309/// rather than being expanded into a boolean tree. This preserves eval's
310/// count-based semantics (evaluate every matching detection, report all that
311/// match) and avoids the combinatorial blow-up of expanding `N of` into an
312/// `Or` of `And`s.
313#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
314pub enum IrCondition {
315 Detection(String),
316 And(Vec<IrCondition>),
317 Or(Vec<IrCondition>),
318 Not(Box<IrCondition>),
319 Selector {
320 quantifier: Quantifier,
321 pattern: SelectorPattern,
322 },
323}
324
325// =============================================================================
326// IrCorrelation
327// =============================================================================
328
329/// Correlation rule shape. Field set mirrors [`rsigma_parser::CorrelationRule`]
330/// (no logsource on correlations).
331#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
332pub struct IrCorrelation {
333 pub metadata: IrRuleMetadata,
334 pub sigma_version: Option<u32>,
335 pub correlation_type: CorrelationType,
336 pub rules: Vec<String>,
337 pub group_by: Vec<String>,
338 pub timespan: Timespan,
339 pub window: WindowMode,
340 pub gap: Option<Timespan>,
341 pub condition: CorrelationCondition,
342 pub aliases: Vec<FieldAlias>,
343 pub generate: bool,
344}
345
346// =============================================================================
347// IrFilter
348// =============================================================================
349
350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
351pub struct IrFilter {
352 pub metadata: IrRuleMetadata,
353 pub sigma_version: Option<u32>,
354 pub rules: FilterRuleTarget,
355 pub logsource: Option<LogSource>,
356 pub detections: HashMap<String, IrDetection>,
357 pub conditions: Vec<IrCondition>,
358}