Skip to main content

stix_matcher/
resolve.rs

1//! Resolve a pattern `ObjectPath` to the set of values it selects on an object.
2
3use stix_model::{ObjectStore, ObjectView, StixValue};
4use stix_pattern::ast::{ObjectPath, PathStep};
5
6/// Resolve `path` against `obj`, returning every value the path selects.
7///
8/// Returns an empty vec if the object's type does not match the path root, or the
9/// path leads nowhere. A `[*]` step or a `_refs` list can produce several values.
10/// When a step descends *into* a string value, that string is treated as a STIX id
11/// and dereferenced through `store` (if provided) — this implements `_ref`/`_refs`
12/// traversal such as `network-traffic:src_ref.value`.
13pub fn resolve_path(
14    obj: &dyn ObjectView,
15    path: &ObjectPath,
16    store: Option<&ObjectStore>,
17) -> Vec<StixValue> {
18    if obj.type_() != Some(path.object_type.as_str()) {
19        return Vec::new();
20    }
21    let mut steps = path.steps.iter();
22
23    // The first step is always a key looked up on the object itself.
24    let first = match steps.next() {
25        Some(PathStep::Key(k)) => k,
26        // A path with no steps, or a leading index, selects nothing meaningful.
27        _ => return Vec::new(),
28    };
29    let mut current: Vec<StixValue> = match obj.property(first) {
30        Some(v) => vec![v],
31        None => Vec::new(),
32    };
33
34    for step in steps {
35        let mut next = Vec::new();
36        for value in current.drain(..) {
37            apply_step(value, step, store, &mut next);
38        }
39        current = next;
40    }
41    current
42}
43
44/// Apply one path step to one value, pushing any resulting values into `out`.
45fn apply_step(
46    value: StixValue,
47    step: &PathStep,
48    store: Option<&ObjectStore>,
49    out: &mut Vec<StixValue>,
50) {
51    match step {
52        PathStep::Key(key) => match value {
53            // Descend into a nested object.
54            StixValue::Object(map) => {
55                if let Some(v) = map.get(key) {
56                    out.push(v.clone());
57                }
58            }
59            // Descend into a referenced object: treat the string as an id.
60            StixValue::String(id) => {
61                if let Some(store) = store {
62                    if let Some(referenced) = store.get(&id) {
63                        if let Some(v) = referenced.property(key) {
64                            out.push(v);
65                        }
66                    }
67                }
68            }
69            _ => {}
70        },
71        PathStep::Index(i) => {
72            if let StixValue::List(items) = value {
73                if let Some(v) = items.into_iter().nth(*i as usize) {
74                    out.push(v);
75                }
76            }
77        }
78        PathStep::AnyIndex => {
79            if let StixValue::List(items) = value {
80                out.extend(items);
81            }
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use stix_model::{Bundle, ObjectStore, StixObject, StixValue};
90    use stix_pattern::ast::{ObjectPath, PathStep};
91
92    fn obj(json: serde_json::Value) -> StixObject {
93        StixObject::from_json(json).unwrap()
94    }
95
96    fn path(object_type: &str, steps: Vec<PathStep>) -> ObjectPath {
97        ObjectPath {
98            object_type: object_type.to_string(),
99            steps,
100        }
101    }
102
103    #[test]
104    fn resolves_top_level_property() {
105        let o =
106            obj(serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.2.3.4"}));
107        let p = path("ipv4-addr", vec![PathStep::Key("value".into())]);
108        assert_eq!(
109            resolve_path(&o, &p, None),
110            vec![StixValue::String("1.2.3.4".into())]
111        );
112    }
113
114    #[test]
115    fn type_mismatch_yields_nothing() {
116        let o =
117            obj(serde_json::json!({"type": "domain-name", "id": "domain-name--1", "value": "x"}));
118        let p = path("ipv4-addr", vec![PathStep::Key("value".into())]);
119        assert!(resolve_path(&o, &p, None).is_empty());
120    }
121
122    #[test]
123    fn resolves_nested_key() {
124        let o = obj(serde_json::json!({
125            "type": "file", "id": "file--1",
126            "hashes": {"SHA-256": "abc"}
127        }));
128        let p = path(
129            "file",
130            vec![
131                PathStep::Key("hashes".into()),
132                PathStep::Key("SHA-256".into()),
133            ],
134        );
135        assert_eq!(
136            resolve_path(&o, &p, None),
137            vec![StixValue::String("abc".into())]
138        );
139    }
140
141    #[test]
142    fn resolves_index_and_any_index() {
143        let o = obj(serde_json::json!({
144            "type": "network-traffic", "id": "network-traffic--1",
145            "protocols": ["ipv4", "tcp"]
146        }));
147        let idx = path(
148            "network-traffic",
149            vec![PathStep::Key("protocols".into()), PathStep::Index(1)],
150        );
151        assert_eq!(
152            resolve_path(&o, &idx, None),
153            vec![StixValue::String("tcp".into())]
154        );
155
156        let any = path(
157            "network-traffic",
158            vec![PathStep::Key("protocols".into()), PathStep::AnyIndex],
159        );
160        assert_eq!(
161            resolve_path(&o, &any, None),
162            vec![
163                StixValue::String("ipv4".into()),
164                StixValue::String("tcp".into())
165            ]
166        );
167    }
168
169    #[test]
170    fn dereferences_ref_through_store() {
171        let bundle = Bundle::from_json_str(
172            r#"{"type":"bundle","id":"bundle--1","objects":[
173                {"type":"ipv4-addr","id":"ipv4-addr--1","value":"1.2.3.4"},
174                {"type":"network-traffic","id":"network-traffic--1","src_ref":"ipv4-addr--1"}
175            ]}"#,
176        )
177        .unwrap();
178        let store = ObjectStore::from_bundle(&bundle);
179        let nt = obj(serde_json::json!({
180            "type": "network-traffic", "id": "network-traffic--1", "src_ref": "ipv4-addr--1"
181        }));
182        let p = path(
183            "network-traffic",
184            vec![
185                PathStep::Key("src_ref".into()),
186                PathStep::Key("value".into()),
187            ],
188        );
189        assert_eq!(
190            resolve_path(&nt, &p, Some(&store)),
191            vec![StixValue::String("1.2.3.4".into())]
192        );
193    }
194
195    #[test]
196    fn missing_property_yields_nothing() {
197        let o = obj(serde_json::json!({"type": "file", "id": "file--1", "name": "x"}));
198        let p = path("file", vec![PathStep::Key("size".into())]);
199        assert!(resolve_path(&o, &p, None).is_empty());
200    }
201}