Skip to main content

stix_model/
sdo.rs

1//! Typed STIX Domain Objects. Phase 1 implements `observed-data`; more are additive.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::value::StixValue;
8use crate::view::ObjectView;
9
10/// The STIX `observed-data` SDO.
11///
12/// Carries the temporal fields and `object_refs` the matcher needs. Unknown or
13/// custom properties are retained in `additional` (via `#[serde(flatten)]`) so the
14/// `ObjectView` still exposes them. Tolerates STIX 2.0 inline `objects` as well as
15/// 2.1 `object_refs`.
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct ObservedData {
18    /// The STIX `type` property (always `observed-data`).
19    #[serde(rename = "type")]
20    pub type_: String,
21    /// The object's STIX id.
22    pub id: String,
23    /// The start of the observation window (RFC3339 timestamp).
24    pub first_observed: String,
25    /// The end of the observation window (RFC3339 timestamp).
26    pub last_observed: String,
27    /// How many times the window's contents were observed.
28    pub number_observed: u64,
29    /// The ids of the referenced SCOs (STIX 2.1).
30    #[serde(default)]
31    pub object_refs: Vec<String>,
32    /// STIX 2.0 inline observed objects (`objects`), if present.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub objects: Option<BTreeMap<String, StixValue>>,
35    /// Any other properties, retained for `ObjectView` and round-tripping.
36    #[serde(flatten)]
37    pub additional: BTreeMap<String, StixValue>,
38}
39
40impl ObservedData {
41    /// The referenced SCO ids. Prefers 2.1 `object_refs`; otherwise empty.
42    /// (2.0 inline `objects` are keyed locally, not by id, so they yield no refs.)
43    pub fn sco_ids(&self) -> Vec<&str> {
44        self.object_refs.iter().map(String::as_str).collect()
45    }
46}
47
48impl ObjectView for ObservedData {
49    fn id(&self) -> Option<&str> {
50        Some(&self.id)
51    }
52
53    fn type_(&self) -> Option<&str> {
54        Some(&self.type_)
55    }
56
57    fn property(&self, name: &str) -> Option<StixValue> {
58        match name {
59            "type" => Some(StixValue::String(self.type_.clone())),
60            "id" => Some(StixValue::String(self.id.clone())),
61            "first_observed" => Some(StixValue::String(self.first_observed.clone())),
62            "last_observed" => Some(StixValue::String(self.last_observed.clone())),
63            "number_observed" => Some(StixValue::Integer(self.number_observed as i64)),
64            "object_refs" => Some(StixValue::List(
65                self.object_refs
66                    .iter()
67                    .map(|s| StixValue::String(s.clone()))
68                    .collect(),
69            )),
70            "objects" => self.objects.clone().map(StixValue::Object),
71            other => self.additional.get(other).cloned(),
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::value::StixValue;
80    use crate::view::ObjectView;
81
82    fn sample_json() -> serde_json::Value {
83        serde_json::json!({
84            "type": "observed-data",
85            "id": "observed-data--1",
86            "first_observed": "2020-01-01T00:00:00Z",
87            "last_observed": "2020-01-01T00:05:00Z",
88            "number_observed": 3,
89            "object_refs": ["ipv4-addr--1", "domain-name--1"],
90            "x_custom": "keep-me"
91        })
92    }
93
94    #[test]
95    fn deserializes_typed_fields() {
96        let od: ObservedData = serde_json::from_value(sample_json()).unwrap();
97        assert_eq!(od.id, "observed-data--1");
98        assert_eq!(od.number_observed, 3);
99        assert_eq!(od.first_observed, "2020-01-01T00:00:00Z");
100        assert_eq!(od.object_refs, vec!["ipv4-addr--1", "domain-name--1"]);
101    }
102
103    #[test]
104    fn object_view_exposes_typed_and_custom_props() {
105        let od: ObservedData = serde_json::from_value(sample_json()).unwrap();
106        assert_eq!(od.type_(), Some("observed-data"));
107        assert_eq!(od.id(), Some("observed-data--1"));
108        assert_eq!(od.property("number_observed"), Some(StixValue::Integer(3)));
109        assert_eq!(
110            od.property("first_observed"),
111            Some(StixValue::String("2020-01-01T00:00:00Z".into()))
112        );
113        // custom property preserved via `additional`
114        assert_eq!(
115            od.property("x_custom"),
116            Some(StixValue::String("keep-me".into()))
117        );
118        assert_eq!(od.property("nope"), None);
119    }
120
121    #[test]
122    fn sco_ids_prefers_object_refs() {
123        let od: ObservedData = serde_json::from_value(sample_json()).unwrap();
124        assert_eq!(od.sco_ids(), vec!["ipv4-addr--1", "domain-name--1"]);
125    }
126}