Skip to main content

maec/common/
mod.rs

1//! Common MAEC types and utilities
2//!
3//! This module provides core types shared across all MAEC objects, including
4//! common properties, traits, and ID generation/validation helpers.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11fn default_now() -> DateTime<Utc> {
12    Utc::now()
13}
14
15fn default_version() -> Option<String> {
16    Some("5.0".to_string())
17}
18
19/// Trait implemented by all MAEC objects for basic accessors
20pub trait MaecObject {
21    /// Returns the unique identifier of the object
22    fn id(&self) -> &str;
23
24    /// Returns the type of the MAEC object (e.g., "package", "malware-family")
25    fn type_(&self) -> &str;
26
27    /// Returns when the object was created
28    fn created(&self) -> DateTime<Utc>;
29}
30
31/// Common properties shared by MAEC top-level objects
32///
33/// These properties are flattened into each MAEC object type via serde,
34/// providing consistent ID generation, timestamping, and metadata.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub struct CommonProperties {
38    /// The type of MAEC object (e.g., "package", "malware-family")
39    #[serde(rename = "type")]
40    pub r#type: String,
41
42    /// Unique identifier for this object (format: "type--uuid")
43    pub id: String,
44
45    /// MAEC specification version (should be "5.0")
46    #[serde(default = "default_version", skip_serializing_if = "Option::is_none")]
47    pub schema_version: Option<String>,
48
49    /// Timestamp when the object was created
50    #[serde(default = "default_now")]
51    pub created: DateTime<Utc>,
52
53    /// Timestamp when the object was last modified
54    #[serde(default = "default_now")]
55    pub modified: DateTime<Utc>,
56
57    /// Reference to the identity that created this object
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub created_by_ref: Option<String>,
60
61    /// Custom properties for extensions
62    #[serde(flatten)]
63    pub custom_properties: HashMap<String, serde_json::Value>,
64}
65
66impl Default for CommonProperties {
67    fn default() -> Self {
68        let now = Utc::now();
69        Self {
70            r#type: String::new(),
71            id: generate_maec_id("object"),
72            schema_version: Some("5.0".to_string()),
73            created: now,
74            modified: now,
75            created_by_ref: None,
76            custom_properties: HashMap::new(),
77        }
78    }
79}
80
81impl CommonProperties {
82    /// Creates a new CommonProperties instance
83    ///
84    /// # Arguments
85    ///
86    /// * `object_type` - The MAEC object type (e.g., "package", "malware-family")
87    /// * `created_by_ref` - Optional reference to the creating identity
88    ///
89    /// # Examples
90    ///
91    /// ```
92    /// use maec::common::CommonProperties;
93    ///
94    /// let common = CommonProperties::new("malware-family", None);
95    /// assert_eq!(common.r#type, "malware-family");
96    /// assert_eq!(common.schema_version, Some("5.0".to_string()));
97    /// ```
98    pub fn new(object_type: impl Into<String>, created_by_ref: Option<String>) -> Self {
99        let object_type = object_type.into();
100        let now = Utc::now();
101        Self {
102            r#type: object_type.clone(),
103            id: generate_maec_id(&object_type),
104            schema_version: Some("5.0".to_string()),
105            created: now,
106            modified: now,
107            created_by_ref,
108            custom_properties: HashMap::new(),
109        }
110    }
111
112    /// Creates a new version of this object by updating the modified timestamp
113    ///
114    /// In MAEC (like STIX), when you update an object, you keep the same ID
115    /// and created timestamp but update the modified timestamp to indicate
116    /// a new version.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use maec::common::CommonProperties;
122    /// use std::thread;
123    /// use std::time::Duration;
124    ///
125    /// let mut common = CommonProperties::new("malware-family", None);
126    /// let original_modified = common.modified;
127    ///
128    /// thread::sleep(Duration::from_millis(10));
129    /// common.new_version();
130    ///
131    /// assert!(common.modified > original_modified);
132    /// assert_eq!(common.created, original_modified); // created unchanged
133    /// ```
134    pub fn new_version(&mut self) {
135        self.modified = Utc::now();
136    }
137}
138
139impl MaecObject for CommonProperties {
140    fn id(&self) -> &str {
141        &self.id
142    }
143
144    fn type_(&self) -> &str {
145        &self.r#type
146    }
147
148    fn created(&self) -> DateTime<Utc> {
149        self.created
150    }
151}
152
153/// Generates a unique MAEC identifier
154///
155/// MAEC IDs follow the format: `{object-type}--{uuid}`
156///
157/// # Examples
158///
159/// ```
160/// use maec::common::generate_maec_id;
161///
162/// let id = generate_maec_id("malware-family");
163/// assert!(id.starts_with("malware-family--"));
164/// ```
165pub fn generate_maec_id(object_type: &str) -> String {
166    format!("{}--{}", object_type, Uuid::new_v4())
167}
168
169/// Validates that a string is a valid MAEC identifier
170///
171/// MAEC IDs must follow the format: `{object-type}--{uuid}`
172///
173/// # Examples
174///
175/// ```
176/// use maec::common::is_valid_maec_id;
177///
178/// assert!(is_valid_maec_id("malware-family--12345678-1234-1234-1234-123456789abc"));
179/// assert!(is_valid_maec_id("package--550e8400-e29b-41d4-a716-446655440000"));
180/// assert!(!is_valid_maec_id("invalid"));
181/// assert!(!is_valid_maec_id("malware-family-bad-uuid"));
182/// ```
183pub fn is_valid_maec_id(id: &str) -> bool {
184    let parts: Vec<&str> = id.split("--").collect();
185    if parts.len() != 2 {
186        return false;
187    }
188
189    // Validate the UUID part
190    Uuid::parse_str(parts[1]).is_ok()
191}
192
193/// Extracts the object type from a MAEC ID
194///
195/// # Examples
196///
197/// ```
198/// use maec::common::extract_type_from_id;
199///
200/// assert_eq!(
201///     extract_type_from_id("malware-family--12345678-1234-1234-1234-123456789abc"),
202///     Some("malware-family")
203/// );
204/// assert_eq!(extract_type_from_id("invalid"), None);
205/// ```
206pub fn extract_type_from_id(id: &str) -> Option<&str> {
207    let parts: Vec<&str> = id.split("--").collect();
208    if parts.len() == 2 && Uuid::parse_str(parts[1]).is_ok() {
209        Some(parts[0])
210    } else {
211        None
212    }
213}
214
215/// Validates that a reference ID matches the expected object type
216///
217/// # Examples
218///
219/// ```
220/// use maec::common::is_valid_ref_for_type;
221///
222/// assert!(is_valid_ref_for_type(
223///     "malware-family--12345678-1234-1234-1234-123456789abc",
224///     "malware-family"
225/// ));
226/// assert!(!is_valid_ref_for_type(
227///     "package--12345678-1234-1234-1234-123456789abc",
228///     "malware-family"
229/// ));
230/// ```
231pub fn is_valid_ref_for_type(id: &str, expected_type: &str) -> bool {
232    extract_type_from_id(id)
233        .map(|t| t == expected_type)
234        .unwrap_or(false)
235}
236
237/// External Reference - Links to external resources
238///
239/// Used to reference external sources like ATT&CK techniques, CVEs,
240/// or research papers related to MAEC objects.
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242#[serde(rename_all = "snake_case")]
243pub struct ExternalReference {
244    /// Name of the source (e.g., "mitre-attack", "cve")
245    pub source_name: String,
246
247    /// Description of the reference
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub description: Option<String>,
250
251    /// URL to the external resource
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub url: Option<String>,
254
255    /// External identifier (e.g., "T1055" for ATT&CK)
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub external_id: Option<String>,
258}
259
260impl ExternalReference {
261    /// Creates a new external reference with just a source name
262    pub fn new(source_name: impl Into<String>) -> Self {
263        Self {
264            source_name: source_name.into(),
265            description: None,
266            url: None,
267            external_id: None,
268        }
269    }
270
271    /// Creates an ATT&CK technique reference
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// use maec::common::ExternalReference;
277    ///
278    /// let technique = ExternalReference::attack_technique("T1055", "Process Injection");
279    /// assert_eq!(technique.source_name, "mitre-attack");
280    /// assert_eq!(technique.external_id, Some("T1055".to_string()));
281    /// ```
282    pub fn attack_technique(technique_id: impl Into<String>, name: impl Into<String>) -> Self {
283        let technique_id = technique_id.into();
284        Self {
285            source_name: "mitre-attack".to_string(),
286            description: Some(name.into()),
287            url: Some(format!(
288                "https://attack.mitre.org/techniques/{}",
289                technique_id
290            )),
291            external_id: Some(technique_id),
292        }
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_generate_maec_id() {
302        let id = generate_maec_id("malware-family");
303        assert!(id.starts_with("malware-family--"));
304        assert!(is_valid_maec_id(&id));
305    }
306
307    #[test]
308    fn test_is_valid_maec_id() {
309        assert!(is_valid_maec_id(
310            "malware-family--550e8400-e29b-41d4-a716-446655440000"
311        ));
312        assert!(is_valid_maec_id(
313            "package--12345678-1234-1234-1234-123456789abc"
314        ));
315        assert!(!is_valid_maec_id("invalid"));
316        assert!(!is_valid_maec_id("malware-family"));
317        assert!(!is_valid_maec_id("malware-family-no-uuid"));
318    }
319
320    #[test]
321    fn test_extract_type_from_id() {
322        assert_eq!(
323            extract_type_from_id("malware-family--550e8400-e29b-41d4-a716-446655440000"),
324            Some("malware-family")
325        );
326        assert_eq!(
327            extract_type_from_id("package--12345678-1234-1234-1234-123456789abc"),
328            Some("package")
329        );
330        assert_eq!(extract_type_from_id("invalid"), None);
331    }
332
333    #[test]
334    fn test_is_valid_ref_for_type() {
335        assert!(is_valid_ref_for_type(
336            "malware-family--550e8400-e29b-41d4-a716-446655440000",
337            "malware-family"
338        ));
339        assert!(!is_valid_ref_for_type(
340            "package--550e8400-e29b-41d4-a716-446655440000",
341            "malware-family"
342        ));
343    }
344
345    #[test]
346    fn test_common_properties_new() {
347        let common = CommonProperties::new("malware-family", None);
348        assert_eq!(common.r#type, "malware-family");
349        assert_eq!(common.schema_version, Some("5.0".to_string()));
350        assert!(common.id.starts_with("malware-family--"));
351    }
352
353    #[test]
354    fn test_new_version() {
355        let mut common = CommonProperties::new("malware-family", None);
356        let original_created = common.created;
357        let original_modified = common.modified;
358
359        std::thread::sleep(std::time::Duration::from_millis(10));
360        common.new_version();
361
362        assert_eq!(common.created, original_created);
363        assert!(common.modified > original_modified);
364    }
365
366    #[test]
367    fn test_external_reference_attack() {
368        let ref_obj = ExternalReference::attack_technique("T1055", "Process Injection");
369        assert_eq!(ref_obj.source_name, "mitre-attack");
370        assert_eq!(ref_obj.external_id, Some("T1055".to_string()));
371        assert!(ref_obj.url.unwrap().contains("T1055"));
372    }
373}