Skip to main content

stix_model/
bundle.rs

1//! The STIX `bundle` container.
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::{ModelError, Result};
6use crate::object::StixObject;
7
8/// A STIX bundle: a `type: "bundle"` envelope around a list of objects.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Bundle {
11    /// The STIX `type` property (always `bundle`).
12    #[serde(rename = "type")]
13    pub type_: String,
14    /// The bundle's id, if present.
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub id: Option<String>,
17    /// The objects the bundle contains.
18    #[serde(default)]
19    pub objects: Vec<StixObject>,
20}
21
22impl Bundle {
23    /// Parse a bundle from a JSON string, validating the `type` is `bundle`.
24    pub fn from_json_str(s: &str) -> Result<Self> {
25        let bundle: Bundle = serde_json::from_str(s)?;
26        if bundle.type_ != "bundle" {
27            return Err(ModelError::NotABundle(format!(
28                "type was '{}'",
29                bundle.type_
30            )));
31        }
32        Ok(bundle)
33    }
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use crate::view::ObjectView;
40
41    fn bundle_json() -> &'static str {
42        r#"{
43            "type": "bundle",
44            "id": "bundle--1",
45            "objects": [
46                {"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.2.3.4"},
47                {"type": "observed-data", "id": "observed-data--1",
48                 "first_observed": "2020-01-01T00:00:00Z",
49                 "last_observed": "2020-01-01T00:00:00Z",
50                 "number_observed": 1, "object_refs": ["ipv4-addr--1"]}
51            ]
52        }"#
53    }
54
55    #[test]
56    fn parses_bundle() {
57        let b = Bundle::from_json_str(bundle_json()).unwrap();
58        assert_eq!(b.id.as_deref(), Some("bundle--1"));
59        assert_eq!(b.objects.len(), 2);
60        assert_eq!(b.objects[0].type_(), Some("ipv4-addr"));
61        assert_eq!(b.objects[1].type_(), Some("observed-data"));
62    }
63
64    #[test]
65    fn rejects_non_bundle() {
66        let err = Bundle::from_json_str(r#"{"type": "ipv4-addr", "id": "x--1"}"#).unwrap_err();
67        assert!(matches!(err, crate::error::ModelError::NotABundle(_)));
68    }
69
70    #[test]
71    fn round_trips() {
72        let b = Bundle::from_json_str(bundle_json()).unwrap();
73        let s = serde_json::to_string(&b).unwrap();
74        let b2 = Bundle::from_json_str(&s).unwrap();
75        assert_eq!(b.objects.len(), b2.objects.len());
76    }
77}