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}