Skip to main content

stix_pattern/
ast.rs

1//! Abstract syntax tree for STIX 2.1 patterns. All nodes are serde-serializable.
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Span;
6
7/// A complete parsed pattern.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct Pattern {
10    /// The top-level observation expression.
11    pub expression: ObservationExpression,
12}
13
14/// Observation-level expression tree.
15/// `FOLLOWEDBY`/`AND`/`OR` combine observations; qualifiers attach to a sub-expression.
16///
17/// # Chain nodes
18///
19/// `And`, `Or` and `FollowedBy` are n-ary: a chain such as `a OR b OR c` is a
20/// single node holding every operand in source order, so the tree's depth does
21/// not grow with the length of a chain. Nodes produced by [`parse`](crate::parse)
22/// uphold two invariants:
23///
24/// - the operand list has **at least two** elements;
25/// - the **first** operand is never a node of the same operator. Left-nested
26///   chains are flattened (`(a OR b) OR c` and `a OR b OR c` are both
27///   `Or([a, b, c])`), while a right-nested group keeps its own node
28///   (`a OR (b OR c)` is `Or([a, Or([b, c])])`).
29///
30/// A chain `Op([a, b, c])` means the left-associative `(a op b) op c`.
31///
32/// In JSON a two-operand chain serializes as `{"Or": [l, r]}`, and longer
33/// chains as longer arrays.
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub enum ObservationExpression {
36    /// A single `[ comparisonExpr ]` observation.
37    Observation {
38        /// The comparison expression inside the brackets.
39        expression: Box<ComparisonExpression>,
40        /// Byte range of the whole `[...]` including the brackets.
41        span: Span,
42    },
43    /// `AND` of two or more observation expressions (see [chain nodes](Self#chain-nodes)).
44    And(Vec<ObservationExpression>),
45    /// `OR` of two or more observation expressions (see [chain nodes](Self#chain-nodes)).
46    Or(Vec<ObservationExpression>),
47    /// `FOLLOWEDBY` of two or more observation expressions: each operand's
48    /// observations precede the next's (see [chain nodes](Self#chain-nodes)).
49    FollowedBy(Vec<ObservationExpression>),
50    /// A sub-expression with a postfix [`Qualifier`] attached.
51    Qualified {
52        /// The qualified sub-expression.
53        expression: Box<ObservationExpression>,
54        /// The attached qualifier.
55        qualifier: Qualifier,
56        /// Byte range of the qualifier clause only (e.g. `WITHIN 60 SECONDS`).
57        span: Span,
58    },
59}
60
61/// Postfix qualifier on an observation expression.
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63pub enum Qualifier {
64    /// `WITHIN <seconds> SECONDS`
65    Within {
66        /// The window length in seconds.
67        seconds: f64,
68    },
69    /// `REPEATS <count> TIMES`
70    Repeats {
71        /// The required repetition count.
72        count: u64,
73    },
74    /// `START <start> STOP <stop>` (RFC3339 timestamps, kept as strings here)
75    StartStop {
76        /// The inclusive window start timestamp.
77        start: String,
78        /// The exclusive window stop timestamp.
79        stop: String,
80    },
81}
82
83/// Comparison-level expression tree (inside `[ ]`).
84///
85/// `And` and `Or` are n-ary chain nodes with the same invariants as
86/// [`ObservationExpression`]'s: at least two operands, and the first operand is
87/// never a node of the same operator.
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89pub enum ComparisonExpression {
90    /// A single property test.
91    Test(Comparison),
92    /// `AND` of two or more comparison expressions.
93    And(Vec<ComparisonExpression>),
94    /// `OR` of two or more comparison expressions.
95    Or(Vec<ComparisonExpression>),
96}
97
98/// A single property test.
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub struct Comparison {
101    /// The object path on the left-hand side.
102    pub path: ObjectPath,
103    /// The comparison operator.
104    pub operator: ComparisonOperator,
105    /// `true` if a `NOT` preceded the operator.
106    pub negated: bool,
107    /// The right-hand-side operand.
108    pub value: ComparisonOperand,
109    /// Byte range of the whole property test in the source pattern.
110    pub span: Span,
111}
112
113/// Right-hand side of a comparison: either a single literal or a set (for `IN`).
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115pub enum ComparisonOperand {
116    /// A single literal value.
117    Literal(Literal),
118    /// A parenthesized set of literals (for `IN`).
119    Set(Vec<Literal>),
120}
121
122/// The operator of a property test.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124pub enum ComparisonOperator {
125    /// `=`
126    Equal,
127    /// `!=` or `<>`
128    NotEqual,
129    /// `>`
130    GreaterThan,
131    /// `>=`
132    GreaterThanOrEqual,
133    /// `<`
134    LessThan,
135    /// `<=`
136    LessThanOrEqual,
137    /// `IN`: membership in a set of literals.
138    In,
139    /// `LIKE`: SQL-style wildcard match (`%`, `_`).
140    Like,
141    /// `MATCHES`: regular-expression match.
142    Matches,
143    /// `ISSUBSET`: IP address/range containment.
144    IsSubset,
145    /// `ISSUPERSET`: inverse IP address/range containment.
146    IsSuperset,
147    /// `EXISTS objectPath`; for this operator the operand is ignored.
148    Exists,
149}
150
151/// An object path: `object-type:first.step[0].next`.
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
153pub struct ObjectPath {
154    /// The STIX object type before the colon (e.g. `file`).
155    pub object_type: String,
156    /// The property steps after the colon, in order.
157    pub steps: Vec<PathStep>,
158    /// Byte range of the path text in the source pattern.
159    pub span: Span,
160}
161
162/// One step in an [`ObjectPath`].
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164pub enum PathStep {
165    /// `.key` or the first component after the colon.
166    Key(String),
167    /// `[n]`
168    Index(u64),
169    /// `[*]`
170    AnyIndex,
171}
172
173/// A primitive literal value.
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub enum Literal {
176    /// A single-quoted string literal.
177    String(String),
178    /// An integer literal.
179    Integer(i64),
180    /// A floating-point literal.
181    Float(f64),
182    /// A `true`/`false` literal.
183    Boolean(bool),
184    /// RFC3339 timestamp from a `t'...'` literal (kept as the inner string).
185    Timestamp(String),
186    /// Base64 payload from a `b'...'` literal (kept as the inner string).
187    Binary(String),
188    /// Hex payload from an `h'...'` literal (kept as the inner string).
189    Hex(String),
190}
191
192/// A span of zero length at offset 0, used as the normalized value by
193/// [`Pattern::without_spans`].
194const ZERO_SPAN: Span = Span { start: 0, end: 0 };
195
196impl Pattern {
197    /// A copy of this pattern with every source span zeroed.
198    ///
199    /// Two patterns that differ only in whitespace or in where they appeared in
200    /// a source string compare equal after this normalization. Used to state
201    /// structural equality independent of byte offsets.
202    pub fn without_spans(&self) -> Pattern {
203        let mut pattern = self.clone();
204        zero_observation_spans(&mut pattern.expression);
205        pattern
206    }
207}
208
209// The span-zeroing walks use an explicit stack, so neither nesting depth nor
210// chain length costs them any call stack.
211
212fn zero_observation_spans(root: &mut ObservationExpression) {
213    use ObservationExpression as O;
214    let mut pending = vec![root];
215    while let Some(e) = pending.pop() {
216        match e {
217            O::Observation { expression, span } => {
218                *span = ZERO_SPAN;
219                zero_comparison_spans(expression);
220            }
221            O::And(xs) | O::Or(xs) | O::FollowedBy(xs) => pending.extend(xs.iter_mut()),
222            O::Qualified {
223                expression, span, ..
224            } => {
225                *span = ZERO_SPAN;
226                pending.push(expression);
227            }
228        }
229    }
230}
231
232fn zero_comparison_spans(root: &mut ComparisonExpression) {
233    use ComparisonExpression as C;
234    let mut pending = vec![root];
235    while let Some(e) = pending.pop() {
236        match e {
237            C::Test(c) => {
238                c.span = ZERO_SPAN;
239                c.path.span = ZERO_SPAN;
240            }
241            C::And(xs) | C::Or(xs) => pending.extend(xs.iter_mut()),
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn build_simple_comparison_pattern() {
252        let path = ObjectPath {
253            object_type: "ipv4-addr".to_string(),
254            steps: vec![PathStep::Key("value".to_string())],
255            span: Span::default(),
256        };
257        let comp = Comparison {
258            path,
259            operator: ComparisonOperator::Equal,
260            negated: false,
261            value: ComparisonOperand::Literal(Literal::String("1.2.3.4".to_string())),
262            span: Span::default(),
263        };
264        let pattern = Pattern {
265            expression: ObservationExpression::Observation {
266                expression: Box::new(ComparisonExpression::Test(comp)),
267                span: Span::default(),
268            },
269        };
270        match pattern.expression {
271            ObservationExpression::Observation { .. } => {}
272            _ => panic!("expected observation"),
273        }
274    }
275
276    fn obs(value: &str) -> ObservationExpression {
277        ObservationExpression::Observation {
278            expression: Box::new(ComparisonExpression::Test(Comparison {
279                path: ObjectPath {
280                    object_type: "x".to_string(),
281                    steps: vec![PathStep::Key("v".to_string())],
282                    span: Span::default(),
283                },
284                operator: ComparisonOperator::Equal,
285                negated: false,
286                value: ComparisonOperand::Literal(Literal::String(value.to_string())),
287                span: Span::default(),
288            })),
289            span: Span::default(),
290        }
291    }
292
293    /// The JSON of a two-operand chain is exactly what the old binary
294    /// `Or(Box, Box)` produced; longer chains are longer arrays.
295    #[test]
296    fn chain_json_shape_is_pinned() {
297        let leaf = |v: &str| serde_json::to_string(&obs(v)).unwrap();
298        let two = ObservationExpression::Or(vec![obs("a"), obs("b")]);
299        assert_eq!(
300            serde_json::to_string(&two).unwrap(),
301            format!("{{\"Or\":[{},{}]}}", leaf("a"), leaf("b"))
302        );
303        let three = ObservationExpression::FollowedBy(vec![obs("a"), obs("b"), obs("c")]);
304        assert_eq!(
305            serde_json::to_string(&three).unwrap(),
306            format!(
307                "{{\"FollowedBy\":[{},{},{}]}}",
308                leaf("a"),
309                leaf("b"),
310                leaf("c")
311            )
312        );
313        let back: ObservationExpression =
314            serde_json::from_str(&serde_json::to_string(&three).unwrap()).unwrap();
315        assert_eq!(back, three);
316    }
317
318    #[test]
319    fn ast_round_trips_through_json() {
320        let lit = Literal::Integer(42);
321        let json = serde_json::to_string(&lit).unwrap();
322        let back: Literal = serde_json::from_str(&json).unwrap();
323        assert_eq!(lit, back);
324    }
325}