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
5/// A complete parsed pattern.
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub struct Pattern {
8    /// The top-level observation expression.
9    pub expression: ObservationExpression,
10}
11
12/// Observation-level expression tree.
13/// `FOLLOWEDBY`/`AND`/`OR` combine observations; qualifiers attach to a sub-expression.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub enum ObservationExpression {
16    /// A single `[ comparisonExpr ]` observation.
17    Observation(Box<ComparisonExpression>),
18    /// `AND` of two observation expressions.
19    And(Box<ObservationExpression>, Box<ObservationExpression>),
20    /// `OR` of two observation expressions.
21    Or(Box<ObservationExpression>, Box<ObservationExpression>),
22    /// `FOLLOWEDBY`: the left expression's observations precede the right's.
23    FollowedBy(Box<ObservationExpression>, Box<ObservationExpression>),
24    /// A sub-expression with a postfix [`Qualifier`] attached.
25    Qualified {
26        /// The qualified sub-expression.
27        expression: Box<ObservationExpression>,
28        /// The attached qualifier.
29        qualifier: Qualifier,
30    },
31}
32
33/// Postfix qualifier on an observation expression.
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub enum Qualifier {
36    /// `WITHIN <seconds> SECONDS`
37    Within {
38        /// The window length in seconds.
39        seconds: f64,
40    },
41    /// `REPEATS <count> TIMES`
42    Repeats {
43        /// The required repetition count.
44        count: u64,
45    },
46    /// `START <start> STOP <stop>` (RFC3339 timestamps, kept as strings here)
47    StartStop {
48        /// The inclusive window start timestamp.
49        start: String,
50        /// The exclusive window stop timestamp.
51        stop: String,
52    },
53}
54
55/// Comparison-level expression tree (inside `[ ]`).
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub enum ComparisonExpression {
58    /// A single property test.
59    Test(Comparison),
60    /// `AND` of two comparison expressions.
61    And(Box<ComparisonExpression>, Box<ComparisonExpression>),
62    /// `OR` of two comparison expressions.
63    Or(Box<ComparisonExpression>, Box<ComparisonExpression>),
64}
65
66/// A single property test.
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub struct Comparison {
69    /// The object path on the left-hand side.
70    pub path: ObjectPath,
71    /// The comparison operator.
72    pub operator: ComparisonOperator,
73    /// `true` if a `NOT` preceded the operator.
74    pub negated: bool,
75    /// The right-hand-side operand.
76    pub value: ComparisonOperand,
77}
78
79/// Right-hand side of a comparison: either a single literal or a set (for `IN`).
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub enum ComparisonOperand {
82    /// A single literal value.
83    Literal(Literal),
84    /// A parenthesized set of literals (for `IN`).
85    Set(Vec<Literal>),
86}
87
88/// The operator of a property test.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90pub enum ComparisonOperator {
91    /// `=`
92    Equal,
93    /// `!=` or `<>`
94    NotEqual,
95    /// `>`
96    GreaterThan,
97    /// `>=`
98    GreaterThanOrEqual,
99    /// `<`
100    LessThan,
101    /// `<=`
102    LessThanOrEqual,
103    /// `IN`: membership in a set of literals.
104    In,
105    /// `LIKE`: SQL-style wildcard match (`%`, `_`).
106    Like,
107    /// `MATCHES`: regular-expression match.
108    Matches,
109    /// `ISSUBSET`: IP address/range containment.
110    IsSubset,
111    /// `ISSUPERSET`: inverse IP address/range containment.
112    IsSuperset,
113    /// `EXISTS objectPath`; for this operator the operand is ignored.
114    Exists,
115}
116
117/// An object path: `object-type:first.step[0].next`.
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct ObjectPath {
120    /// The STIX object type before the colon (e.g. `file`).
121    pub object_type: String,
122    /// The property steps after the colon, in order.
123    pub steps: Vec<PathStep>,
124}
125
126/// One step in an [`ObjectPath`].
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub enum PathStep {
129    /// `.key` or the first component after the colon.
130    Key(String),
131    /// `[n]`
132    Index(u64),
133    /// `[*]`
134    AnyIndex,
135}
136
137/// A primitive literal value.
138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
139pub enum Literal {
140    /// A single-quoted string literal.
141    String(String),
142    /// An integer literal.
143    Integer(i64),
144    /// A floating-point literal.
145    Float(f64),
146    /// A `true`/`false` literal.
147    Boolean(bool),
148    /// RFC3339 timestamp from a `t'...'` literal (kept as the inner string).
149    Timestamp(String),
150    /// Base64 payload from a `b'...'` literal (kept as the inner string).
151    Binary(String),
152    /// Hex payload from an `h'...'` literal (kept as the inner string).
153    Hex(String),
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn build_simple_comparison_pattern() {
162        let path = ObjectPath {
163            object_type: "ipv4-addr".to_string(),
164            steps: vec![PathStep::Key("value".to_string())],
165        };
166        let comp = Comparison {
167            path,
168            operator: ComparisonOperator::Equal,
169            negated: false,
170            value: ComparisonOperand::Literal(Literal::String("1.2.3.4".to_string())),
171        };
172        let pattern = Pattern {
173            expression: ObservationExpression::Observation(Box::new(ComparisonExpression::Test(
174                comp,
175            ))),
176        };
177        match pattern.expression {
178            ObservationExpression::Observation(_) => {}
179            _ => panic!("expected observation"),
180        }
181    }
182
183    #[test]
184    fn ast_round_trips_through_json() {
185        let lit = Literal::Integer(42);
186        let json = serde_json::to_string(&lit).unwrap();
187        let back: Literal = serde_json::from_str(&json).unwrap();
188        assert_eq!(lit, back);
189    }
190}