Skip to main content

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}