Skip to main content

stix_rs/
objects.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::common::{CommonProperties, StixObject};
5use crate::vocab::{IdentityClass, IndicatorPatternType};
6fn default_pattern_type() -> IndicatorPatternType { IndicatorPatternType::Stix }
7fn default_valid_from() -> DateTime<Utc> { Utc::now() }
8use crate::pattern::validate_pattern;
9
10// Re-export BuilderError from sdos to avoid duplication
11pub use crate::sdos::BuilderError;
12
13/// A kill chain phase (used by Malware and Indicator)
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, )]
15#[serde(rename_all = "snake_case")]
16pub struct KillChainPhase {
17    #[serde(rename = "kill_chain_name")]
18    pub name: String,
19
20    #[serde(rename = "phase_name")]
21    pub phase_name: String,
22}
23
24/// Identity Domain Object
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, )]
26#[serde(rename_all = "snake_case")]
27pub struct Identity {
28    #[serde(flatten)]
29    pub common: CommonProperties,
30
31    pub name: String,
32
33    pub identity_class: Option<IdentityClass>,
34
35    pub sectors: Option<Vec<String>>,
36}
37
38impl Identity {
39    pub fn builder() -> IdentityBuilder {
40        IdentityBuilder::default()
41    }
42}
43
44#[derive(Debug, Default)]
45pub struct IdentityBuilder {
46    name: Option<String>,
47    identity_class: Option<IdentityClass>,
48    sectors: Option<Vec<String>>,
49    created_by_ref: Option<String>,
50    custom_properties: std::collections::HashMap<String, serde_json::Value>,
51}
52
53impl IdentityBuilder {
54    pub fn name(mut self, name: impl Into<String>) -> Self {
55        self.name = Some(name.into());
56        self
57    }
58
59    /// Set identity class (e.g., System, Organization)
60    pub fn identity_class(mut self, identity_class: IdentityClass) -> Self {
61        self.identity_class = Some(identity_class);
62        self
63    }
64
65    // Backwards-compatible alias
66    pub fn class(self, identity_class: IdentityClass) -> Self {
67        self.identity_class(identity_class)
68    }
69
70    pub fn sectors(mut self, sectors: Vec<String>) -> Self {
71        self.sectors = Some(sectors);
72        self
73    }
74
75    /// Add a custom/stix extension property (e.g., x_my_tag)
76    pub fn property(mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
77        self.custom_properties.insert(key.into(), value.into());
78        self
79    }
80
81    pub fn created_by_ref(mut self, r: impl Into<String>) -> Self {
82        self.created_by_ref = Some(r.into());
83        self
84    }
85
86    pub fn build(mut self) -> Result<Identity, BuilderError> {
87        let name = self.name.ok_or(BuilderError::MissingField("name"))?;
88        let identity_class = self.identity_class;
89
90        let mut common = CommonProperties::new("identity", self.created_by_ref);
91        // Attach any custom properties provided by the builder
92        if !self.custom_properties.is_empty() {
93            common.custom_properties.extend(self.custom_properties.drain());
94        }
95
96        Ok(Identity {
97            common,
98            name,
99            identity_class,
100            sectors: self.sectors,
101        })
102    }
103}
104
105impl StixObject for Identity {
106    fn id(&self) -> &str {
107        &self.common.id
108    }
109
110    fn type_(&self) -> &str {
111        &self.common.r#type
112    }
113
114    fn created(&self) -> DateTime<Utc> {
115        self.common.created
116    }
117}
118
119// Allow converting domain objects into the StixObjectEnum for easy bundling
120impl From<Identity> for crate::StixObjectEnum {
121    fn from(i: Identity) -> Self {
122        crate::StixObjectEnum::Identity(i)
123    }
124}
125/// Malware Domain Object
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, )]
127#[serde(rename_all = "snake_case")]
128pub struct Malware {
129    #[serde(flatten)]
130    pub common: CommonProperties,
131
132    pub name: String,
133
134    pub description: Option<String>,
135
136        #[serde(default)]
137    pub malware_types: Vec<String>,
138
139        #[serde(default)]
140    pub is_family: bool,
141
142    pub aliases: Option<Vec<String>>,
143
144    pub kill_chain_phases: Option<Vec<KillChainPhase>>,
145
146    pub first_seen: Option<DateTime<Utc>>,
147
148    pub last_seen: Option<DateTime<Utc>>,
149
150    pub operating_system_refs: Option<Vec<String>>,
151
152    pub architecture_execution_envs: Option<Vec<String>>,
153
154    pub implementation_languages: Option<Vec<String>>,
155
156    pub capabilities: Option<Vec<String>>,
157
158    pub sample_refs: Option<Vec<String>>,
159}
160
161impl Malware {
162    pub fn builder() -> MalwareBuilder {
163        MalwareBuilder::default()
164    }
165}
166
167#[derive(Debug, Default)]
168pub struct MalwareBuilder {
169    name: Option<String>,
170    description: Option<String>,
171    is_family: Option<bool>,
172    malware_types: Option<Vec<String>>,
173    aliases: Option<Vec<String>>,
174    kill_chain_phases: Option<Vec<KillChainPhase>>,
175    first_seen: Option<DateTime<Utc>>,
176    last_seen: Option<DateTime<Utc>>,
177    operating_system_refs: Option<Vec<String>>,
178    architecture_execution_envs: Option<Vec<String>>,
179    implementation_languages: Option<Vec<String>>,
180    capabilities: Option<Vec<String>>,
181    sample_refs: Option<Vec<String>>,
182    created_by_ref: Option<String>,
183}
184
185impl MalwareBuilder {
186    pub fn name(mut self, name: impl Into<String>) -> Self {
187        self.name = Some(name.into());
188        self
189    }
190
191    pub fn description(mut self, desc: impl Into<String>) -> Self {
192        self.description = Some(desc.into());
193        self
194    }
195
196    pub fn is_family(mut self, is_family: bool) -> Self {
197        self.is_family = Some(is_family);
198        self
199    }
200
201    pub fn malware_types(mut self, types: Vec<String>) -> Self {
202        self.malware_types = Some(types);
203        self
204    }
205
206    pub fn aliases(mut self, aliases: Vec<String>) -> Self {
207        self.aliases = Some(aliases);
208        self
209    }
210
211    pub fn kill_chain_phases(mut self, phases: Vec<KillChainPhase>) -> Self {
212        self.kill_chain_phases = Some(phases);
213        self
214    }
215
216    pub fn first_seen(mut self, t: DateTime<Utc>) -> Self {
217        self.first_seen = Some(t);
218        self
219    }
220
221    pub fn last_seen(mut self, t: DateTime<Utc>) -> Self {
222        self.last_seen = Some(t);
223        self
224    }
225
226    pub fn operating_system_refs(mut self, refs: Vec<String>) -> Self {
227        self.operating_system_refs = Some(refs);
228        self
229    }
230
231    pub fn architecture_execution_envs(mut self, envs: Vec<String>) -> Self {
232        self.architecture_execution_envs = Some(envs);
233        self
234    }
235
236    pub fn implementation_languages(mut self, langs: Vec<String>) -> Self {
237        self.implementation_languages = Some(langs);
238        self
239    }
240
241    pub fn capabilities(mut self, caps: Vec<String>) -> Self {
242        self.capabilities = Some(caps);
243        self
244    }
245
246    pub fn sample_refs(mut self, refs: Vec<String>) -> Self {
247        self.sample_refs = Some(refs);
248        self
249    }
250
251    pub fn created_by_ref(mut self, r: impl Into<String>) -> Self {
252        self.created_by_ref = Some(r.into());
253        self
254    }
255
256    pub fn build(self) -> Result<Malware, BuilderError> {
257        let name = self.name.ok_or(BuilderError::MissingField("name"))?;
258        let is_family = self.is_family.unwrap_or(false);
259        let malware_types = self.malware_types.unwrap_or_default();
260
261        let common = CommonProperties::new("malware", self.created_by_ref);
262
263        Ok(Malware {
264            common,
265            name,
266            description: self.description,
267            malware_types,
268            is_family,
269            aliases: self.aliases,
270            kill_chain_phases: self.kill_chain_phases,
271            first_seen: self.first_seen,
272            last_seen: self.last_seen,
273            operating_system_refs: self.operating_system_refs,
274            architecture_execution_envs: self.architecture_execution_envs,
275            implementation_languages: self.implementation_languages,
276            capabilities: self.capabilities,
277            sample_refs: self.sample_refs,
278        })
279    }
280}
281
282impl StixObject for Malware {
283    fn id(&self) -> &str {
284        &self.common.id
285    }
286
287    fn type_(&self) -> &str {
288        &self.common.r#type
289    }
290
291    fn created(&self) -> DateTime<Utc> {
292        self.common.created
293    }
294}
295
296/// Indicator Domain Object
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, )]
298#[serde(rename_all = "snake_case")]
299pub struct Indicator {
300    #[serde(flatten)]
301    pub common: CommonProperties,
302
303    pub name: Option<String>,
304
305    pub description: Option<String>,
306
307    pub indicator_types: Option<Vec<String>>,
308
309    pub pattern: String,
310
311    #[serde(default = "default_pattern_type")]
312    pub pattern_type: IndicatorPatternType,
313
314    pub pattern_version: Option<String>,
315
316    #[serde(default = "default_valid_from")]
317    pub valid_from: DateTime<Utc>,
318
319    pub valid_until: Option<DateTime<Utc>>,
320
321    pub kill_chain_phases: Option<Vec<KillChainPhase>>,
322}
323
324impl Indicator {
325    pub fn builder() -> IndicatorBuilder {
326        IndicatorBuilder::default()
327    }
328
329    /// Validate the pattern syntax
330    pub fn validate_pattern(&self) -> Result<(), crate::pattern::PatternError> {
331        if self.pattern_type == IndicatorPatternType::Stix {
332            validate_pattern(&self.pattern)
333        } else {
334            // Other pattern types (PCRE, Snort, etc.) aren't validated
335            Ok(())
336        }
337    }
338}
339
340#[derive(Debug, Default)]
341pub struct IndicatorBuilder {
342    name: Option<String>,
343    description: Option<String>,
344    indicator_types: Option<Vec<String>>,
345    pattern: Option<String>,
346    pattern_type: Option<IndicatorPatternType>,
347    pattern_version: Option<String>,
348    valid_from: Option<DateTime<Utc>>,
349    valid_until: Option<DateTime<Utc>>,
350    kill_chain_phases: Option<Vec<KillChainPhase>>,
351    created_by_ref: Option<String>,
352    validate_pattern: bool,
353}
354
355impl IndicatorBuilder {
356    pub fn name(mut self, name: impl Into<String>) -> Self {
357        self.name = Some(name.into());
358        self
359    }
360
361    pub fn description(mut self, desc: impl Into<String>) -> Self {
362        self.description = Some(desc.into());
363        self
364    }
365
366    pub fn indicator_types(mut self, types: Vec<String>) -> Self {
367        self.indicator_types = Some(types);
368        self
369    }
370
371    pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
372        self.pattern = Some(pattern.into());
373        self
374    }
375
376    pub fn pattern_type(mut self, pt: IndicatorPatternType) -> Self {
377        self.pattern_type = Some(pt);
378        self
379    }
380
381    pub fn pattern_version(mut self, version: impl Into<String>) -> Self {
382        self.pattern_version = Some(version.into());
383        self
384    }
385
386    pub fn valid_from(mut self, t: DateTime<Utc>) -> Self {
387        self.valid_from = Some(t);
388        self
389    }
390
391    pub fn valid_until(mut self, t: DateTime<Utc>) -> Self {
392        self.valid_until = Some(t);
393        self
394    }
395
396    pub fn kill_chain_phases(mut self, phases: Vec<KillChainPhase>) -> Self {
397        self.kill_chain_phases = Some(phases);
398        self
399    }
400
401    pub fn created_by_ref(mut self, r: impl Into<String>) -> Self {
402        self.created_by_ref = Some(r.into());
403        self
404    }
405
406    /// Enable pattern validation (default: false)
407    pub fn validate_pattern(mut self, validate: bool) -> Self {
408        self.validate_pattern = validate;
409        self
410    }
411
412    pub fn build(self) -> Result<Indicator, BuilderError> {
413        let pattern = self.pattern.ok_or(BuilderError::MissingField("pattern"))?;
414        let pattern_type = self.pattern_type.ok_or(BuilderError::MissingField("pattern_type"))?;
415        let valid_from = self.valid_from.ok_or(BuilderError::MissingField("valid_from"))?;
416
417        // Optionally validate STIX patterns
418        if self.validate_pattern && pattern_type == IndicatorPatternType::Stix {
419            validate_pattern(&pattern)
420                .map_err(|_| BuilderError::MissingField("invalid pattern"))?;
421        }
422
423        let common = CommonProperties::new("indicator", self.created_by_ref);
424
425        Ok(Indicator {
426            common,
427            name: self.name,
428            description: self.description,
429            indicator_types: self.indicator_types,
430            pattern,
431            pattern_type,
432            pattern_version: self.pattern_version,
433            valid_from,
434            valid_until: self.valid_until,
435            kill_chain_phases: self.kill_chain_phases,
436        })
437    }
438}
439
440impl StixObject for Indicator {
441    fn id(&self) -> &str {
442        &self.common.id
443    }
444
445    fn type_(&self) -> &str {
446        &self.common.r#type
447    }
448
449    fn created(&self) -> DateTime<Utc> {
450        self.common.created
451    }
452}
453
454impl From<Indicator> for crate::StixObjectEnum {
455    fn from(i: Indicator) -> Self {
456        crate::StixObjectEnum::Indicator(i)
457    }
458}
459
460impl From<Malware> for crate::StixObjectEnum {
461    fn from(m: Malware) -> Self {
462        crate::StixObjectEnum::Malware(m)
463    }
464}
465
466/// Sighting Domain Object
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, )]
468#[serde(rename_all = "snake_case")]
469pub struct Sighting {
470    #[serde(flatten)]
471    pub common: CommonProperties,
472
473    pub count: u32,
474
475    pub sighting_of_ref: String,
476
477    pub where_sighted_refs: Vec<String>,
478}
479
480impl Sighting {
481    pub fn builder() -> SightingBuilder {
482        SightingBuilder::default()
483    }
484}
485
486#[derive(Debug, Default)]
487pub struct SightingBuilder {
488    count: Option<u32>,
489    sighting_of_ref: Option<String>,
490    where_sighted_refs: Option<Vec<String>>,
491    created_by_ref: Option<String>,
492}
493
494impl SightingBuilder {
495    pub fn count(mut self, count: u32) -> Self {
496        self.count = Some(count);
497        self
498    }
499
500    pub fn sighting_of_ref(mut self, r: impl Into<String>) -> Self {
501        self.sighting_of_ref = Some(r.into());
502        self
503    }
504
505    pub fn where_sighted_refs(mut self, refs: Vec<String>) -> Self {
506        self.where_sighted_refs = Some(refs);
507        self
508    }
509
510    pub fn created_by_ref(mut self, r: impl Into<String>) -> Self {
511        self.created_by_ref = Some(r.into());
512        self
513    }
514
515    pub fn build(self) -> Result<Sighting, BuilderError> {
516        let count = self.count.ok_or(BuilderError::MissingField("count"))?;
517        let sighting_of_ref = self.sighting_of_ref.ok_or(BuilderError::MissingField("sighting_of_ref"))?;
518        let where_sighted_refs = self.where_sighted_refs.ok_or(BuilderError::MissingField("where_sighted_refs"))?;
519
520        let common = CommonProperties::new("sighting", self.created_by_ref);
521
522        Ok(Sighting {
523            common,
524            count,
525            sighting_of_ref,
526            where_sighted_refs,
527        })
528    }
529}
530
531impl StixObject for Sighting {
532    fn id(&self) -> &str {
533        &self.common.id
534    }
535
536    fn type_(&self) -> &str {
537        &self.common.r#type
538    }
539
540    fn created(&self) -> DateTime<Utc> {
541        self.common.created
542    }
543}
544
545impl From<Sighting> for crate::StixObjectEnum {
546    fn from(s: Sighting) -> Self {
547        crate::StixObjectEnum::Sighting(s)
548    }
549}
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554    use crate::vocab::{IdentityClass, IndicatorPatternType};
555    use serde_json::Value;
556
557    #[test]
558    fn identity_builder_and_serialize() {
559        let idty = Identity::builder()
560            .name("ACME")
561            .class(IdentityClass::Organization)
562            .sectors(vec!["technology".into()])
563            .build()
564            .unwrap();
565
566        let s = serde_json::to_string(&idty).unwrap();
567        let v: Value = serde_json::from_str(&s).unwrap();
568        assert_eq!(v.get("type").and_then(Value::as_str).unwrap(), "identity");
569        assert_eq!(v.get("identity_class").and_then(Value::as_str).unwrap(), "organization");
570        let id_field = v.get("id").and_then(Value::as_str).unwrap();
571        assert!(id_field.starts_with("identity--"));
572    }
573
574    #[test]
575    fn malware_builder_and_serialize() {
576        let mw = Malware::builder()
577            .name("BadWare")
578            .is_family(true)
579            .malware_types(vec!["ransomware".into()])
580            .build()
581            .unwrap();
582
583        let s = serde_json::to_string(&mw).unwrap();
584        let v: Value = serde_json::from_str(&s).unwrap();
585        assert_eq!(v.get("type").and_then(Value::as_str).unwrap(), "malware");
586        assert_eq!(v.get("is_family").and_then(Value::as_bool).unwrap(), true);
587    }
588
589    #[test]
590    fn indicator_builder_and_serialize() {
591        let ind = Indicator::builder()
592            .name("Test")
593            .pattern("[file:hashes.'SHA-256' = '...']")
594            .pattern_type(IndicatorPatternType::Stix)
595            .valid_from(Utc::now())
596            .build()
597            .unwrap();
598
599        let s = serde_json::to_string(&ind).unwrap();
600        let v: Value = serde_json::from_str(&s).unwrap();
601        assert_eq!(v.get("type").and_then(Value::as_str).unwrap(), "indicator");
602        assert_eq!(v.get("pattern_type").and_then(Value::as_str).unwrap(), "stix");
603    }
604
605    #[test]
606    fn sighting_builder_and_serialize() {
607        let s = Sighting::builder()
608            .count(2)
609            .sighting_of_ref("malware--1111")
610            .where_sighted_refs(vec!["sensor--1".into()])
611            .build()
612            .unwrap();
613
614        let j = serde_json::to_string(&s).unwrap();
615        let v: Value = serde_json::from_str(&j).unwrap();
616        assert_eq!(v.get("type").and_then(Value::as_str).unwrap(), "sighting");
617        assert_eq!(v.get("sighting_of_ref").and_then(Value::as_str).unwrap(), "malware--1111");
618    }
619
620    #[test]
621    fn missing_required_field_errors() {
622        // identity_class is now optional — builder should succeed
623        let r = Identity::builder().name("No Class").build();
624        assert!(r.is_ok());
625
626        // name is required for Malware — builder should still fail when missing
627        let r2 = Malware::builder().is_family(false).build();
628        assert!(r2.is_err());
629    }
630}