Skip to main content

stix_model/
object.rs

1//! `StixObject`: the hybrid typed-or-generic object, with deserialization dispatch.
2
3use serde::de::{Deserialize, Deserializer, Error as DeError};
4use serde::Serialize;
5
6use std::sync::Arc;
7
8use crate::error::{ModelError, Result};
9use crate::sdo::ObservedData;
10use crate::value::StixValue;
11use crate::view::{CustomObject, GenericObject, ObjectView};
12
13/// A STIX object: a recognized typed object, a generic value bag, or a
14/// consumer-registered custom object.
15#[derive(Debug, Clone)]
16pub enum StixObject {
17    /// A recognized type with a dedicated struct.
18    Typed(TypedObject),
19    /// Any other type, stored as a flat property map.
20    Generic(GenericObject),
21    /// A consumer-registered custom object.
22    Custom(Arc<dyn CustomObject>),
23}
24
25/// The set of types with dedicated typed structs. Additive: new variants slot in
26/// here and in [`StixObject::from_json`]'s dispatch.
27#[derive(Debug, Clone, PartialEq)]
28pub enum TypedObject {
29    /// The STIX `observed-data` SDO.
30    ObservedData(ObservedData),
31}
32
33impl StixObject {
34    /// Build a `StixObject` from a JSON value, dispatching on its `type` property.
35    pub fn from_json(value: serde_json::Value) -> Result<Self> {
36        let type_ = value
37            .get("type")
38            .and_then(serde_json::Value::as_str)
39            .ok_or_else(|| ModelError::InvalidObject("missing 'type' property".to_string()))?
40            .to_string();
41
42        match type_.as_str() {
43            "observed-data" => {
44                let od: ObservedData = serde_json::from_value(value)?;
45                Ok(StixObject::Typed(TypedObject::ObservedData(od)))
46            }
47            _ => Ok(StixObject::Generic(GenericObject::from_json(value)?)),
48        }
49    }
50
51    /// If this is a registered custom object of concrete type `T`, borrow it.
52    pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
53        match self {
54            StixObject::Custom(c) => c.as_any().downcast_ref::<T>(),
55            _ => None,
56        }
57    }
58}
59
60impl PartialEq for StixObject {
61    fn eq(&self, other: &Self) -> bool {
62        match (self, other) {
63            (StixObject::Typed(a), StixObject::Typed(b)) => a == b,
64            (StixObject::Generic(a), StixObject::Generic(b)) => a == b,
65            (StixObject::Custom(a), StixObject::Custom(b)) => a.as_json() == b.as_json(),
66            _ => false,
67        }
68    }
69}
70
71impl ObjectView for StixObject {
72    fn id(&self) -> Option<&str> {
73        match self {
74            StixObject::Typed(t) => t.id(),
75            StixObject::Generic(g) => g.id(),
76            StixObject::Custom(c) => c.id(),
77        }
78    }
79
80    fn type_(&self) -> Option<&str> {
81        match self {
82            StixObject::Typed(t) => t.type_(),
83            StixObject::Generic(g) => g.type_(),
84            StixObject::Custom(c) => c.type_(),
85        }
86    }
87
88    fn property(&self, name: &str) -> Option<StixValue> {
89        match self {
90            StixObject::Typed(t) => t.property(name),
91            StixObject::Generic(g) => g.property(name),
92            StixObject::Custom(c) => c.property(name),
93        }
94    }
95}
96
97impl ObjectView for TypedObject {
98    fn id(&self) -> Option<&str> {
99        match self {
100            TypedObject::ObservedData(od) => od.id(),
101        }
102    }
103
104    fn type_(&self) -> Option<&str> {
105        match self {
106            TypedObject::ObservedData(od) => od.type_(),
107        }
108    }
109
110    fn property(&self, name: &str) -> Option<StixValue> {
111        match self {
112            TypedObject::ObservedData(od) => od.property(name),
113        }
114    }
115}
116
117impl Serialize for StixObject {
118    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
119    where
120        S: serde::Serializer,
121    {
122        match self {
123            StixObject::Typed(TypedObject::ObservedData(od)) => od.serialize(serializer),
124            StixObject::Generic(g) => g.properties().serialize(serializer),
125            StixObject::Custom(c) => c.as_json().serialize(serializer),
126        }
127    }
128}
129
130impl<'de> Deserialize<'de> for StixObject {
131    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
132    where
133        D: Deserializer<'de>,
134    {
135        let value = serde_json::Value::deserialize(deserializer)?;
136        StixObject::from_json(value).map_err(DeError::custom)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::value::StixValue;
144    use crate::view::ObjectView;
145
146    #[test]
147    fn observed_data_becomes_typed() {
148        let v = serde_json::json!({
149            "type": "observed-data",
150            "id": "observed-data--1",
151            "first_observed": "2020-01-01T00:00:00Z",
152            "last_observed": "2020-01-01T00:05:00Z",
153            "number_observed": 1,
154            "object_refs": ["file--1"]
155        });
156        let obj = StixObject::from_json(v).unwrap();
157        assert!(matches!(
158            obj,
159            StixObject::Typed(TypedObject::ObservedData(_))
160        ));
161        assert_eq!(obj.type_(), Some("observed-data"));
162    }
163
164    #[test]
165    fn unknown_type_becomes_generic() {
166        let v = serde_json::json!({
167            "type": "ipv4-addr",
168            "id": "ipv4-addr--1",
169            "value": "198.51.100.1"
170        });
171        let obj = StixObject::from_json(v).unwrap();
172        assert!(matches!(obj, StixObject::Generic(_)));
173        assert_eq!(obj.type_(), Some("ipv4-addr"));
174        assert_eq!(
175            obj.property("value"),
176            Some(StixValue::String("198.51.100.1".into()))
177        );
178    }
179
180    #[test]
181    fn missing_type_is_an_error() {
182        let v = serde_json::json!({"id": "x--1"});
183        assert!(StixObject::from_json(v).is_err());
184    }
185
186    #[test]
187    fn deserializes_via_serde() {
188        // serde path (used by Bundle) routes through the same dispatch.
189        let v = serde_json::json!({
190            "type": "ipv4-addr",
191            "id": "ipv4-addr--2",
192            "value": "203.0.113.5"
193        });
194        let obj: StixObject = serde_json::from_value(v).unwrap();
195        assert_eq!(obj.id(), Some("ipv4-addr--2"));
196    }
197
198    use std::sync::Arc;
199
200    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
201    struct Widget {
202        #[serde(rename = "type")]
203        type_: String,
204        id: String,
205        risk: i64,
206    }
207
208    impl ObjectView for Widget {
209        fn id(&self) -> Option<&str> {
210            Some(&self.id)
211        }
212        fn type_(&self) -> Option<&str> {
213            Some(&self.type_)
214        }
215        fn property(&self, name: &str) -> Option<StixValue> {
216            match name {
217                "type" => Some(StixValue::String(self.type_.clone())),
218                "id" => Some(StixValue::String(self.id.clone())),
219                "risk" => Some(StixValue::Integer(self.risk)),
220                _ => None,
221            }
222        }
223    }
224
225    fn widget(risk: i64) -> StixObject {
226        StixObject::Custom(Arc::new(Widget {
227            type_: "x-widget".into(),
228            id: "x-widget--1".into(),
229            risk,
230        }))
231    }
232
233    #[test]
234    fn custom_exposes_object_view() {
235        let o = widget(90);
236        assert_eq!(o.type_(), Some("x-widget"));
237        assert_eq!(o.id(), Some("x-widget--1"));
238        assert_eq!(o.property("risk"), Some(StixValue::Integer(90)));
239        assert_eq!(o.property("missing"), None);
240    }
241
242    #[test]
243    fn custom_downcasts_to_concrete_type() {
244        let o = widget(90);
245        let w = o.downcast_ref::<Widget>().expect("downcast");
246        assert_eq!(w.risk, 90);
247        // Wrong target type yields None.
248        assert!(o.downcast_ref::<String>().is_none());
249        // Non-custom objects yield None.
250        let generic = StixObject::from_json(serde_json::json!({"type":"x","id":"x--1"})).unwrap();
251        assert!(generic.downcast_ref::<Widget>().is_none());
252    }
253
254    #[test]
255    fn custom_clone_and_eq_and_serialize() {
256        assert_eq!(widget(90), widget(90));
257        assert_ne!(widget(90), widget(10));
258        let json = serde_json::to_value(widget(90)).unwrap();
259        assert_eq!(json["risk"], serde_json::json!(90));
260    }
261}