Skip to main content

stix_rs/
lib.rs

1//! stix-rs: STIX 2.1 types and helpers
2
3// MIME Type Constants for STIX and TAXII
4/// STIX 2.1 JSON media type for HTTP Content-Type headers
5pub const MEDIA_TYPE_STIX: &str = "application/stix+json;version=2.1";
6
7/// TAXII 2.1 JSON media type for HTTP Content-Type headers
8pub const MEDIA_TYPE_TAXII: &str = "application/taxii+json;version=2.1";
9
10/// Generic STIX JSON media type (without version)
11pub const MEDIA_TYPE_STIX_GENERIC: &str = "application/stix+json";
12
13/// Generic TAXII JSON media type (without version)
14pub const MEDIA_TYPE_TAXII_GENERIC: &str = "application/taxii+json";
15
16pub mod common;
17pub mod sdos;
18pub mod sros;
19pub mod observables;
20pub mod vocab;
21pub mod bundle;
22pub mod objects;
23pub mod pattern;
24
25pub use common::{
26    CommonProperties, ExtensionDefinition, ExternalReference, GranularMarking, LanguageContent,
27    MarkingDefinition, StixObject, extract_type_from_id, generate_stix_id, is_valid_ref_for_type,
28    is_valid_stix_id,
29};
30pub use objects::*;
31pub use observables::*;
32pub use sdos::*;
33pub use sros::Relationship;
34pub use vocab::*;
35pub use bundle::*;
36pub use pattern::{validate_pattern, PatternBuilder, PatternError};
37
38use uuid::Uuid;
39const SCO_NAMESPACE: Uuid = Uuid::from_u128(0x00abedb4_aa42_466c_9c01_def7442f5a74);
40
41fn generate_sco_id(object_type: &str, data: &str) -> String {
42    let id_part = Uuid::new_v5(&SCO_NAMESPACE, data.as_bytes());
43    format!("{}--{}", object_type, id_part)
44}
45use serde::{Deserialize, Serialize};
46use serde::de::Deserializer;
47use serde_json::Value;
48
49/// A wrapper enum for STIX objects. Deserializes based on the `type` field.
50#[derive(Debug, Clone, PartialEq, Serialize)]
51#[serde(tag = "type", rename_all = "kebab-case")]
52pub enum StixObjectEnum {
53    Identity(Identity),
54    Malware(Malware),
55    Indicator(Indicator),
56    ObservedData(ObservedData),
57    MalwareAnalysis(MalwareAnalysis),
58    Sighting(Sighting),
59    Relationship(Relationship),
60    File(File),
61    Incident(Incident),
62    Location(Location),
63    NetworkTraffic(NetworkTraffic),
64    DomainName(DomainName),
65    #[serde(rename = "ipv4-addr")]
66    IPv4Addr(IPv4Addr),
67    Url(Url),
68    Process(Process),
69    Artifact(Artifact),
70    #[serde(rename = "ipv6-addr")]
71    IPv6Addr(IPv6Addr),
72    MacAddr(MacAddr),
73    Software(Software),
74    UserAccount(UserAccount),
75    EmailAddr(EmailAddr),
76    EmailMessage(EmailMessage),
77    SocketAddr(SocketAddr),
78    AutonomousSystem(AutonomousSystem),
79    SoftwarePackage(SoftwarePackage),
80    Directory(Directory),
81    Mutex(Mutex),
82    WindowsRegistryKey(WindowsRegistryKey),
83    X509Certificate(X509Certificate),
84    AttackPattern(AttackPattern),
85    Campaign(Campaign),
86    ThreatActor(ThreatActor),
87    Tool(Tool),
88    Vulnerability(Vulnerability),
89    CourseOfAction(CourseOfAction),
90    IntrusionSet(IntrusionSet),
91    Infrastructure(Infrastructure),
92    Report(Report),
93    Note(Note),
94    Opinion(Opinion),
95    Grouping(Grouping),
96    MarkingDefinition(MarkingDefinition),
97    LanguageContent(LanguageContent),
98    ExtensionDefinition(ExtensionDefinition),
99    Custom(serde_json::Value),
100}
101
102impl StixObjectEnum {
103    /// Get the ID of the wrapped object
104    pub fn name(&self) -> Option<&str> {
105        match self {
106            StixObjectEnum::Identity(o) => Some(&o.name),
107            StixObjectEnum::Malware(o) => Some(&o.name),
108            StixObjectEnum::ThreatActor(o) => Some(&o.name),
109            StixObjectEnum::AttackPattern(o) => Some(&o.name),
110            StixObjectEnum::Campaign(o) => Some(&o.name),
111            StixObjectEnum::Tool(o) => Some(&o.name),
112            StixObjectEnum::Vulnerability(o) => Some(&o.name),
113            StixObjectEnum::CourseOfAction(o) => Some(&o.name),
114            StixObjectEnum::Infrastructure(o) => Some(&o.name),
115            StixObjectEnum::Report(o) => Some(&o.name),
116            _ => None,
117        }
118    }
119
120    pub fn created(&self) -> chrono::DateTime<chrono::Utc> {
121        match self {
122            StixObjectEnum::Indicator(o) => o.common.created,
123            StixObjectEnum::Malware(o) => o.common.created,
124            StixObjectEnum::ThreatActor(o) => o.common.created,
125            StixObjectEnum::Identity(o) => o.common.created,
126            StixObjectEnum::IntrusionSet(o) => o.common.created,
127            StixObjectEnum::Campaign(o) => o.common.created,
128            StixObjectEnum::Relationship(o) => o.common.created,
129            StixObjectEnum::Custom(v) => v.get("created").and_then(|c| c.as_str()).and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()).map(|dt| dt.with_timezone(&chrono::Utc)).unwrap_or_else(chrono::Utc::now),
130            _ => chrono::Utc::now(),
131        }
132    }
133
134    pub fn labels(&self) -> Option<&Vec<String>> {
135        match self {
136            StixObjectEnum::Indicator(o) => o.common.labels.as_ref(),
137            StixObjectEnum::Malware(o) => o.common.labels.as_ref(),
138            StixObjectEnum::ThreatActor(o) => o.common.labels.as_ref(),
139            StixObjectEnum::Identity(o) => o.common.labels.as_ref(),
140            StixObjectEnum::IntrusionSet(o) => o.common.labels.as_ref(),
141            StixObjectEnum::Campaign(o) => o.common.labels.as_ref(),
142            _ => None,
143        }
144    }
145
146    pub fn id(&self) -> String {
147        match self {
148            StixObjectEnum::Identity(o) => o.id().to_string(),
149            StixObjectEnum::Malware(o) => o.id().to_string(),
150            StixObjectEnum::Indicator(o) => o.id().to_string(),
151            StixObjectEnum::ObservedData(o) => o.id().to_string(),
152            StixObjectEnum::MalwareAnalysis(o) => o.id().to_string(),
153            StixObjectEnum::Sighting(o) => o.id().to_string(),
154            StixObjectEnum::Relationship(o) => o.id().to_string(),
155            StixObjectEnum::File(o) => {
156                if let Some(hashes) = &o.hashes {
157                    if let Some(h) = hashes.get("SHA-256").or(hashes.get("MD5")) {
158                        return generate_sco_id("file", h);
159                    }
160                }
161                generate_sco_id("file", o.name.as_deref().unwrap_or("unknown"))
162            },
163            StixObjectEnum::Incident(o) => o.id().to_string(),
164            StixObjectEnum::Location(o) => o.id().to_string(),
165            StixObjectEnum::NetworkTraffic(_) => generate_sco_id("network-traffic", "unknown"),
166            StixObjectEnum::DomainName(o) => generate_sco_id("domain-name", &o.value),
167            StixObjectEnum::IPv4Addr(o) => generate_sco_id("ipv4-addr", &o.value),
168            StixObjectEnum::Url(o) => generate_sco_id("url", &o.value),
169            StixObjectEnum::Process(_) => generate_sco_id("process", "unknown"),
170            StixObjectEnum::Artifact(_) => generate_sco_id("artifact", "unknown"),
171            StixObjectEnum::IPv6Addr(o) => generate_sco_id("ipv6-addr", &o.value),
172            StixObjectEnum::MacAddr(o) => generate_sco_id("mac-addr", &o.value),
173            StixObjectEnum::Software(o) => generate_sco_id("software", o.name.as_deref().unwrap_or("unknown")),
174            StixObjectEnum::UserAccount(o) => generate_sco_id("user-account", o.user_id.as_deref().unwrap_or("unknown")),
175            StixObjectEnum::EmailAddr(o) => generate_sco_id("email-addr", &o.value),
176            StixObjectEnum::EmailMessage(_) => generate_sco_id("email-message", "unknown"),
177            StixObjectEnum::SocketAddr(_) => generate_sco_id("socket-addr", "unknown"),
178            StixObjectEnum::AutonomousSystem(o) => generate_sco_id("autonomous-system", &o.number.map(|n| n.to_string()).unwrap_or_else(|| "unknown".to_string())),
179            StixObjectEnum::SoftwarePackage(_) => generate_sco_id("software-package", "unknown"),
180            StixObjectEnum::Directory(o) => generate_sco_id("directory", o.path.as_deref().unwrap_or("unknown")),
181            StixObjectEnum::Mutex(o) => generate_sco_id("mutex", o.name.as_deref().unwrap_or("unknown")),
182            StixObjectEnum::WindowsRegistryKey(o) => generate_sco_id("windows-registry-key", o.key.as_deref().unwrap_or("unknown")),
183            StixObjectEnum::X509Certificate(_) => generate_sco_id("x509-certificate", "unknown"),
184            StixObjectEnum::AttackPattern(o) => o.id().to_string(),
185            StixObjectEnum::Campaign(o) => o.id().to_string(),
186            StixObjectEnum::ThreatActor(o) => o.id().to_string(),
187            StixObjectEnum::Tool(o) => o.id().to_string(),
188            StixObjectEnum::Vulnerability(o) => o.id().to_string(),
189            StixObjectEnum::CourseOfAction(o) => o.id().to_string(),
190            StixObjectEnum::IntrusionSet(o) => o.id().to_string(),
191            StixObjectEnum::Infrastructure(o) => o.id().to_string(),
192            StixObjectEnum::Report(o) => o.id().to_string(),
193            StixObjectEnum::Note(o) => o.id().to_string(),
194            StixObjectEnum::Opinion(o) => o.id().to_string(),
195            StixObjectEnum::Grouping(o) => o.id().to_string(),
196            StixObjectEnum::MarkingDefinition(o) => o.id().to_string(),
197            StixObjectEnum::LanguageContent(o) => o.id().to_string(),
198            StixObjectEnum::ExtensionDefinition(o) => o.id().to_string(),
199            StixObjectEnum::Custom(v) => v.get("id").and_then(|i| i.as_str()).map(|s| s.to_string()).unwrap_or_else(|| "unknown".to_string()),
200        }
201    }
202
203    /// Get the type of the wrapped object
204    pub fn type_(&self) -> &str {
205        match self {
206            StixObjectEnum::Identity(o) => o.type_(),
207            StixObjectEnum::Malware(o) => o.type_(),
208            StixObjectEnum::Indicator(o) => o.type_(),
209            StixObjectEnum::ObservedData(o) => o.type_(),
210            StixObjectEnum::MalwareAnalysis(o) => o.type_(),
211            StixObjectEnum::Sighting(o) => o.type_(),
212            StixObjectEnum::Relationship(o) => o.type_(),
213            StixObjectEnum::File(_) => "file",
214            StixObjectEnum::Incident(o) => o.type_(),
215            StixObjectEnum::Location(o) => o.type_(),
216            StixObjectEnum::NetworkTraffic(_) => "network-traffic",
217            StixObjectEnum::DomainName(_) => "domain-name",
218            StixObjectEnum::IPv4Addr(_) => "ipv4-addr",
219            StixObjectEnum::Url(_) => "url",
220            StixObjectEnum::Process(_) => "process",
221            StixObjectEnum::Artifact(_) => "artifact",
222            StixObjectEnum::IPv6Addr(_) => "ipv6-addr",
223            StixObjectEnum::MacAddr(_) => "mac-addr",
224            StixObjectEnum::Software(_) => "software",
225            StixObjectEnum::UserAccount(_) => "user-account",
226            StixObjectEnum::EmailAddr(_) => "email-addr",
227            StixObjectEnum::EmailMessage(_) => "email-message",
228            StixObjectEnum::SocketAddr(_) => "socket-addr",
229            StixObjectEnum::AutonomousSystem(_) => "autonomous-system",
230            StixObjectEnum::SoftwarePackage(_) => "software-package",
231            StixObjectEnum::Directory(_) => "directory",
232            StixObjectEnum::Mutex(_) => "mutex",
233            StixObjectEnum::WindowsRegistryKey(_) => "windows-registry-key",
234            StixObjectEnum::X509Certificate(_) => "x509-certificate",
235            StixObjectEnum::AttackPattern(o) => o.type_(),
236            StixObjectEnum::Campaign(o) => o.type_(),
237            StixObjectEnum::ThreatActor(o) => o.type_(),
238            StixObjectEnum::Tool(o) => o.type_(),
239            StixObjectEnum::Vulnerability(o) => o.type_(),
240            StixObjectEnum::CourseOfAction(o) => o.type_(),
241            StixObjectEnum::IntrusionSet(o) => o.type_(),
242            StixObjectEnum::Infrastructure(o) => o.type_(),
243            StixObjectEnum::Report(o) => o.type_(),
244            StixObjectEnum::Note(o) => o.type_(),
245            StixObjectEnum::Opinion(o) => o.type_(),
246            StixObjectEnum::Grouping(o) => o.type_(),
247            StixObjectEnum::MarkingDefinition(o) => o.type_(),
248            StixObjectEnum::LanguageContent(o) => o.type_(),
249            StixObjectEnum::ExtensionDefinition(o) => o.type_(),
250            StixObjectEnum::Custom(v) => v.get("type").and_then(|t| t.as_str()).unwrap_or("unknown"),
251        }
252    }
253}
254
255// Custom Deserialize impl: internal tag combined with flattened `type` fields in
256// the inner structs requires us to inspect the `type` field first and then
257// deserialize the whole value into the appropriate struct (including its
258// own `type` field via the flattened `CommonProperties`).
259impl<'de> Deserialize<'de> for StixObjectEnum {
260    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
261    where
262        D: Deserializer<'de>,
263    {
264        let v = Value::deserialize(deserializer).map_err(serde::de::Error::custom)?;
265        let t = v
266            .get("type")
267            .and_then(Value::as_str)
268            .ok_or_else(|| serde::de::Error::custom("missing or invalid `type` field"))?;
269        match t {
270            "identity" => Ok(StixObjectEnum::Identity(serde_json::from_value(v).map_err(serde::de::Error::custom)?)),
271            "malware" => Ok(StixObjectEnum::Malware(serde_json::from_value(v).map_err(serde::de::Error::custom)?)),
272            "indicator" => Ok(StixObjectEnum::Indicator(serde_json::from_value(v).map_err(serde::de::Error::custom)?)),
273            "observed-data" => Ok(StixObjectEnum::ObservedData(serde_json::from_value(v).map_err(serde::de::Error::custom)?)),
274            "file" => Ok(StixObjectEnum::File(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
275            "network-traffic" => Ok(StixObjectEnum::NetworkTraffic(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
276            "domain-name" => Ok(StixObjectEnum::DomainName(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
277            "ipv4-addr" => Ok(StixObjectEnum::IPv4Addr(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
278            "ipv6-addr" => Ok(StixObjectEnum::IPv6Addr(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
279            "url" => Ok(StixObjectEnum::Url(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
280            "process" => Ok(StixObjectEnum::Process(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
281            "artifact" => Ok(StixObjectEnum::Artifact(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
282            "mac-addr" => Ok(StixObjectEnum::MacAddr(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
283            "software" => Ok(StixObjectEnum::Software(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
284            "user-account" => Ok(StixObjectEnum::UserAccount(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
285            "email-addr" => Ok(StixObjectEnum::EmailAddr(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
286            "email-message" => Ok(StixObjectEnum::EmailMessage(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
287            "socket-addr" => Ok(StixObjectEnum::SocketAddr(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
288            "autonomous-system" => Ok(StixObjectEnum::AutonomousSystem(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
289            "software-package" => Ok(StixObjectEnum::SoftwarePackage(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
290            "directory" => Ok(StixObjectEnum::Directory(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
291            "mutex" => Ok(StixObjectEnum::Mutex(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
292            "windows-registry-key" => Ok(StixObjectEnum::WindowsRegistryKey(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
293            "x509-certificate" => Ok(StixObjectEnum::X509Certificate(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
294            "malware-analysis" => Ok(StixObjectEnum::MalwareAnalysis(serde_json::from_value(v).map_err(serde::de::Error::custom)?)),
295            "sighting" => Ok(StixObjectEnum::Sighting(serde_json::from_value(v).map_err(serde::de::Error::custom)?)),
296            "grouping" => Ok(StixObjectEnum::Grouping(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
297            "incident" => Ok(StixObjectEnum::Incident(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
298            "location" => Ok(StixObjectEnum::Location(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
299            "opinion" => Ok(StixObjectEnum::Opinion(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
300            "relationship" => Ok(StixObjectEnum::Relationship(serde_json::from_value(v).map_err(serde::de::Error::custom)?)),
301            "marking-definition" => Ok(StixObjectEnum::MarkingDefinition(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
302            "language-content" => Ok(StixObjectEnum::LanguageContent(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
303            "extension-definition" => Ok(StixObjectEnum::ExtensionDefinition(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
304            other if other.starts_with("x-") => Ok(StixObjectEnum::Custom(v.clone())),
305            "attack-pattern" => Ok(StixObjectEnum::AttackPattern(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
306            "campaign" => Ok(StixObjectEnum::Campaign(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
307            "threat-actor" => Ok(StixObjectEnum::ThreatActor(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
308            "tool" => Ok(StixObjectEnum::Tool(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
309            "vulnerability" => Ok(StixObjectEnum::Vulnerability(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
310            "course-of-action" => Ok(StixObjectEnum::CourseOfAction(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
311            "intrusion-set" => Ok(StixObjectEnum::IntrusionSet(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
312            "infrastructure" => Ok(StixObjectEnum::Infrastructure(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
313            "report" => Ok(StixObjectEnum::Report(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
314            "note" => Ok(StixObjectEnum::Note(serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?)),
315            other => Err(serde::de::Error::custom(format!("unknown type: {}", other))),
316        }
317    }
318}
319