Skip to main content

stix_rs/common/
mod.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::HashMap;
5use uuid::Uuid;
6fn default_now() -> DateTime<Utc> { Utc::now() }
7
8/// Trait implemented by STIX objects for basic accessors
9pub trait StixObject {
10    fn id(&self) -> &str;
11    fn type_(&self) -> &str;
12    fn created(&self) -> DateTime<Utc>;
13}
14
15/// Granular Marking - for marking specific portions of objects
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, )]
17#[serde(rename_all = "snake_case")]
18pub struct GranularMarking {
19    pub marking_ref: Option<String>,
20    pub selectors: Vec<String>,
21    pub lang: Option<String>,
22}
23
24/// Common STIX properties shared by STIX domain objects
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, )]
26#[serde(rename_all = "snake_case")]
27pub struct CommonProperties {
28    #[serde(rename = "type")]
29    pub r#type: String,
30
31    pub id: String,
32
33    #[serde(rename = "spec_version")]
34    pub spec_version: Option<String>,
35
36    #[serde(default = "default_now")]
37    pub created: DateTime<Utc>,
38
39    #[serde(default = "default_now")]
40    pub modified: DateTime<Utc>,
41
42    pub created_by_ref: Option<String>,
43
44    pub revoked: Option<bool>,
45
46    pub labels: Option<Vec<String>>,
47
48    pub confidence: Option<u8>,
49
50    pub lang: Option<String>,
51
52    pub external_references: Option<Vec<ExternalReference>>,
53
54    pub object_marking_refs: Option<Vec<String>>,
55
56    pub granular_markings: Option<Vec<GranularMarking>>,
57
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub extensions: Option<HashMap<String, Value>>,
60
61    #[serde(flatten)]
62    pub custom_properties: HashMap<String, Value>,
63}
64
65impl Default for CommonProperties {
66    fn default() -> Self {
67        let now = Utc::now();
68        Self {
69            r#type: String::new(),
70            id: generate_stix_id("object"),
71            spec_version: Some("2.1".to_string()),
72            created: now,
73            modified: now,
74            created_by_ref: None,
75            revoked: None,
76            labels: None,
77            confidence: None,
78            lang: None,
79            external_references: None,
80            object_marking_refs: None,
81            granular_markings: None,
82            extensions: None,
83            custom_properties: HashMap::new(),
84        }
85    }
86}
87
88impl CommonProperties {
89    pub fn new(object_type: impl Into<String>, created_by_ref: Option<String>) -> Self {
90        let object_type = object_type.into();
91        let mut cp = Self::default();
92        cp.r#type = object_type.clone();
93        cp.id = generate_stix_id(&object_type);
94        cp.created_by_ref = created_by_ref;
95        cp
96    }
97
98    /// Creates a new version of this object by updating the modified timestamp
99    ///
100    /// In STIX, when you update an object, you keep the same ID and created timestamp
101    /// but update the modified timestamp to indicate a new version.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use stix_rs::Malware;
107    /// use chrono::Utc;
108    ///
109    /// let mut malware = Malware::builder()
110    ///     .name("BadWare")
111    ///     .malware_types(vec!["trojan".into()])
112    ///     .build()
113    ///     .unwrap();
114    ///
115    /// let original_modified = malware.common.modified;
116    /// std::thread::sleep(std::time::Duration::from_millis(10));
117    ///
118    /// malware.common.new_version();
119    /// assert!(malware.common.modified > original_modified);
120    /// ```
121    pub fn new_version(&mut self) {
122        self.modified = Utc::now();
123    }
124}
125
126impl StixObject for CommonProperties {
127    fn id(&self) -> &str {
128        &self.id
129    }
130
131    fn type_(&self) -> &str {
132        &self.r#type
133    }
134
135    fn created(&self) -> DateTime<Utc> {
136        self.created
137    }
138}
139
140pub fn generate_stix_id(object_type: &str) -> String {
141    format!("{}--{}", object_type, Uuid::new_v4())
142}
143
144/// Validates that a string is a valid STIX identifier
145///
146/// STIX IDs must follow the format: `object-type--<uuid>`
147///
148/// # Examples
149///
150/// ```
151/// use stix_rs::common::is_valid_stix_id;
152///
153/// assert!(is_valid_stix_id("malware--12345678-1234-1234-1234-123456789abc"));
154/// assert!(is_valid_stix_id("indicator--550e8400-e29b-41d4-a716-446655440000"));
155/// assert!(!is_valid_stix_id("invalid"));
156/// assert!(!is_valid_stix_id("malware-bad-uuid"));
157/// ```
158pub fn is_valid_stix_id(id: &str) -> bool {
159    let parts: Vec<&str> = id.split("--").collect();
160    if parts.len() != 2 {
161        return false;
162    }
163
164    // Validate the UUID part
165    Uuid::parse_str(parts[1]).is_ok()
166}
167
168/// Extracts the object type from a STIX ID
169///
170/// # Examples
171///
172/// ```
173/// use stix_rs::common::extract_type_from_id;
174///
175/// assert_eq!(
176///     extract_type_from_id("malware--12345678-1234-1234-1234-123456789abc"),
177///     Some("malware")
178/// );
179/// assert_eq!(extract_type_from_id("invalid"), None);
180/// ```
181pub fn extract_type_from_id(id: &str) -> Option<&str> {
182    let parts: Vec<&str> = id.split("--").collect();
183    if parts.len() == 2 && Uuid::parse_str(parts[1]).is_ok() {
184        Some(parts[0])
185    } else {
186        None
187    }
188}
189
190/// Validates that a reference ID matches the expected object type
191///
192/// # Examples
193///
194/// ```
195/// use stix_rs::common::is_valid_ref_for_type;
196///
197/// assert!(is_valid_ref_for_type(
198///     "malware--12345678-1234-1234-1234-123456789abc",
199///     "malware"
200/// ));
201/// assert!(!is_valid_ref_for_type(
202///     "indicator--12345678-1234-1234-1234-123456789abc",
203///     "malware"
204/// ));
205/// ```
206pub fn is_valid_ref_for_type(id: &str, expected_type: &str) -> bool {
207    extract_type_from_id(id).map(|t| t == expected_type).unwrap_or(false)
208}
209
210// Common objects: ExternalReference, MarkingDefinition, ExtensionDefinition, LanguageContent
211
212/// External Reference - Links to external resources (CVEs, ATT&CK, etc.)
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, )]
214#[serde(rename_all = "snake_case")]
215pub struct ExternalReference {
216    pub source_name: String,
217    pub description: Option<String>,
218    pub url: Option<String>,
219    pub external_id: Option<String>,
220    pub hashes: Option<HashMap<String, String>>,
221}
222
223impl ExternalReference {
224    pub fn new(source_name: impl Into<String>) -> Self {
225        Self {
226            source_name: source_name.into(),
227            description: None,
228            url: None,
229            external_id: None,
230            hashes: None,
231        }
232    }
233
234    pub fn builder() -> ExternalReferenceBuilder {
235        ExternalReferenceBuilder::default()
236    }
237}
238
239#[derive(Debug, Default)]
240pub struct ExternalReferenceBuilder {
241    source_name: Option<String>,
242    description: Option<String>,
243    url: Option<String>,
244    external_id: Option<String>,
245    hashes: Option<HashMap<String, String>>,
246}
247
248impl ExternalReferenceBuilder {
249    pub fn source_name(mut self, name: impl Into<String>) -> Self {
250        self.source_name = Some(name.into());
251        self
252    }
253
254    pub fn description(mut self, desc: impl Into<String>) -> Self {
255        self.description = Some(desc.into());
256        self
257    }
258
259    pub fn url(mut self, url: impl Into<String>) -> Self {
260        self.url = Some(url.into());
261        self
262    }
263
264    pub fn external_id(mut self, id: impl Into<String>) -> Self {
265        self.external_id = Some(id.into());
266        self
267    }
268
269    pub fn hashes(mut self, hashes: HashMap<String, String>) -> Self {
270        self.hashes = Some(hashes);
271        self
272    }
273
274    pub fn build(self) -> Result<ExternalReference, &'static str> {
275        let source_name = self.source_name.ok_or("missing source_name")?;
276        Ok(ExternalReference {
277            source_name,
278            description: self.description,
279            url: self.url,
280            external_id: self.external_id,
281            hashes: self.hashes,
282        })
283    }
284}
285
286/// Marking Definition - For data markings like TLP (Traffic Light Protocol)
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, )]
288#[serde(rename_all = "snake_case")]
289pub struct MarkingDefinition {
290    #[serde(flatten)]
291    pub common: CommonProperties,
292
293    pub definition_type: String,
294    pub definition: serde_json::Value,
295    pub name: Option<String>,
296}
297
298impl MarkingDefinition {
299    pub fn new(definition_type: impl Into<String>, definition: serde_json::Value) -> Self {
300        Self {
301            common: CommonProperties::new("marking-definition", None),
302            definition_type: definition_type.into(),
303            definition,
304            name: None,
305        }
306    }
307
308    pub fn builder() -> MarkingDefinitionBuilder {
309        MarkingDefinitionBuilder::default()
310    }
311
312    /// Create a TLP marking definition
313    pub fn tlp(level: impl Into<String>) -> Self {
314        let level = level.into();
315        let definition = serde_json::json!({
316            "tlp": level.to_lowercase()
317        });
318        Self {
319            common: CommonProperties::new("marking-definition", None),
320            definition_type: "tlp".to_string(),
321            definition,
322            name: Some(format!("TLP:{}", level.to_uppercase())),
323        }
324    }
325}
326
327#[derive(Debug, Default)]
328pub struct MarkingDefinitionBuilder {
329    definition_type: Option<String>,
330    definition: Option<serde_json::Value>,
331    name: Option<String>,
332    created_by_ref: Option<String>,
333}
334
335impl MarkingDefinitionBuilder {
336    pub fn definition_type(mut self, dt: impl Into<String>) -> Self {
337        self.definition_type = Some(dt.into());
338        self
339    }
340
341    pub fn definition(mut self, def: serde_json::Value) -> Self {
342        self.definition = Some(def);
343        self
344    }
345
346    pub fn name(mut self, name: impl Into<String>) -> Self {
347        self.name = Some(name.into());
348        self
349    }
350
351    pub fn created_by_ref(mut self, r: impl Into<String>) -> Self {
352        self.created_by_ref = Some(r.into());
353        self
354    }
355
356    pub fn build(self) -> Result<MarkingDefinition, &'static str> {
357        let definition_type = self.definition_type.ok_or("missing definition_type")?;
358        let definition = self.definition.ok_or("missing definition")?;
359        Ok(MarkingDefinition {
360            common: CommonProperties::new("marking-definition", self.created_by_ref),
361            definition_type,
362            definition,
363            name: self.name,
364        })
365    }
366}
367
368impl StixObject for MarkingDefinition {
369    fn id(&self) -> &str {
370        &self.common.id
371    }
372
373    fn type_(&self) -> &str {
374        &self.common.r#type
375    }
376
377    fn created(&self) -> DateTime<Utc> {
378        self.common.created
379    }
380}
381
382/// Language Content - For internationalization support
383#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, )]
384#[serde(rename_all = "snake_case")]
385pub struct LanguageContent {
386    #[serde(flatten)]
387    pub common: CommonProperties,
388
389    pub object_ref: String,
390    pub object_modified: DateTime<Utc>,
391    pub contents: HashMap<String, HashMap<String, String>>,
392}
393
394impl LanguageContent {
395    pub fn builder() -> LanguageContentBuilder {
396        LanguageContentBuilder::default()
397    }
398}
399
400#[derive(Debug, Default)]
401pub struct LanguageContentBuilder {
402    object_ref: Option<String>,
403    object_modified: Option<DateTime<Utc>>,
404    contents: Option<HashMap<String, HashMap<String, String>>>,
405    created_by_ref: Option<String>,
406}
407
408impl LanguageContentBuilder {
409    pub fn object_ref(mut self, r: impl Into<String>) -> Self {
410        self.object_ref = Some(r.into());
411        self
412    }
413
414    pub fn object_modified(mut self, t: DateTime<Utc>) -> Self {
415        self.object_modified = Some(t);
416        self
417    }
418
419    pub fn contents(mut self, c: HashMap<String, HashMap<String, String>>) -> Self {
420        self.contents = Some(c);
421        self
422    }
423
424    pub fn created_by_ref(mut self, r: impl Into<String>) -> Self {
425        self.created_by_ref = Some(r.into());
426        self
427    }
428
429    pub fn build(self) -> Result<LanguageContent, &'static str> {
430        let object_ref = self.object_ref.ok_or("missing object_ref")?;
431        let object_modified = self.object_modified.ok_or("missing object_modified")?;
432        let contents = self.contents.ok_or("missing contents")?;
433        Ok(LanguageContent {
434            common: CommonProperties::new("language-content", self.created_by_ref),
435            object_ref,
436            object_modified,
437            contents,
438        })
439    }
440}
441
442impl StixObject for LanguageContent {
443    fn id(&self) -> &str {
444        &self.common.id
445    }
446
447    fn type_(&self) -> &str {
448        &self.common.r#type
449    }
450
451    fn created(&self) -> DateTime<Utc> {
452        self.common.created
453    }
454}
455
456/// Extension Definition - For custom STIX extensions
457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, )]
458#[serde(rename_all = "snake_case")]
459pub struct ExtensionDefinition {
460    #[serde(flatten)]
461    pub common: CommonProperties,
462
463    pub name: String,
464    pub description: Option<String>,
465    pub schema: String,
466    pub version: String,
467    pub extension_types: Vec<String>,
468}
469
470impl ExtensionDefinition {
471    pub fn builder() -> ExtensionDefinitionBuilder {
472        ExtensionDefinitionBuilder::default()
473    }
474}
475
476#[derive(Debug, Default)]
477pub struct ExtensionDefinitionBuilder {
478    name: Option<String>,
479    description: Option<String>,
480    schema: Option<String>,
481    version: Option<String>,
482    extension_types: Option<Vec<String>>,
483    created_by_ref: Option<String>,
484}
485
486impl ExtensionDefinitionBuilder {
487    pub fn name(mut self, n: impl Into<String>) -> Self {
488        self.name = Some(n.into());
489        self
490    }
491
492    pub fn description(mut self, d: impl Into<String>) -> Self {
493        self.description = Some(d.into());
494        self
495    }
496
497    pub fn schema(mut self, s: impl Into<String>) -> Self {
498        self.schema = Some(s.into());
499        self
500    }
501
502    pub fn version(mut self, v: impl Into<String>) -> Self {
503        self.version = Some(v.into());
504        self
505    }
506
507    pub fn extension_types(mut self, t: Vec<String>) -> Self {
508        self.extension_types = Some(t);
509        self
510    }
511
512    pub fn created_by_ref(mut self, r: impl Into<String>) -> Self {
513        self.created_by_ref = Some(r.into());
514        self
515    }
516
517    pub fn build(self) -> Result<ExtensionDefinition, &'static str> {
518        let name = self.name.ok_or("missing name")?;
519        let schema = self.schema.ok_or("missing schema")?;
520        let version = self.version.ok_or("missing version")?;
521        let extension_types = self.extension_types.ok_or("missing extension_types")?;
522        Ok(ExtensionDefinition {
523            common: CommonProperties::new("extension-definition", self.created_by_ref),
524            name,
525            description: self.description,
526            schema,
527            version,
528            extension_types,
529        })
530    }
531}
532
533impl StixObject for ExtensionDefinition {
534    fn id(&self) -> &str {
535        &self.common.id
536    }
537
538    fn type_(&self) -> &str {
539        &self.common.r#type
540    }
541
542    fn created(&self) -> DateTime<Utc> {
543        self.common.created
544    }
545}