Skip to main content

maec/objects/
types.rs

1//! Supporting types for MAEC objects
2//!
3//! This module contains common supporting types used across multiple MAEC objects,
4//! such as Name and FieldData.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::common::ExternalReference;
10
11/// Captures the name of a malware instance, family, or alias
12///
13/// Includes the actual name value along with optional source and confidence information.
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15#[serde(rename_all = "snake_case")]
16pub struct Name {
17    /// The actual name value
18    pub value: String,
19
20    /// Source of the name (e.g., AV vendor, researcher)
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub source: Option<ExternalReference>,
23
24    /// Confidence in the accuracy of the assigned name
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub confidence: Option<String>,
27}
28
29impl Name {
30    /// Creates a new Name with just a value
31    pub fn new(value: impl Into<String>) -> Self {
32        Self {
33            value: value.into(),
34            source: None,
35            confidence: None,
36        }
37    }
38
39    /// Creates a Name with a source
40    pub fn with_source(value: impl Into<String>, source: ExternalReference) -> Self {
41        Self {
42            value: value.into(),
43            source: Some(source),
44            confidence: None,
45        }
46    }
47
48    /// Creates a Name with source and confidence
49    pub fn with_confidence(
50        value: impl Into<String>,
51        source: ExternalReference,
52        confidence: impl Into<String>,
53    ) -> Self {
54        Self {
55            value: value.into(),
56            source: Some(source),
57            confidence: Some(confidence.into()),
58        }
59    }
60}
61
62impl From<String> for Name {
63    fn from(value: String) -> Self {
64        Name::new(value)
65    }
66}
67
68impl From<&str> for Name {
69    fn from(value: &str) -> Self {
70        Name::new(value)
71    }
72}
73
74/// Field data associated with a malware instance or family
75///
76/// Captures temporal information and delivery vectors.
77/// At least one field must be present.
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79#[serde(rename_all = "snake_case")]
80pub struct FieldData {
81    /// Vectors used to distribute/deploy the malware
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub delivery_vectors: Option<Vec<String>>,
84
85    /// When the malware was first observed (ISO 8601 format)
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub first_seen: Option<DateTime<Utc>>,
88
89    /// When the malware was last observed (ISO 8601 format)
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub last_seen: Option<DateTime<Utc>>,
92}
93
94impl FieldData {
95    /// Creates a new FieldData builder
96    pub fn builder() -> FieldDataBuilder {
97        FieldDataBuilder::default()
98    }
99
100    /// Creates FieldData with just delivery vectors
101    pub fn with_delivery_vectors(vectors: Vec<String>) -> Self {
102        Self {
103            delivery_vectors: Some(vectors),
104            first_seen: None,
105            last_seen: None,
106        }
107    }
108
109    /// Creates FieldData with first/last seen dates
110    pub fn with_timestamps(first_seen: DateTime<Utc>, last_seen: Option<DateTime<Utc>>) -> Self {
111        Self {
112            delivery_vectors: None,
113            first_seen: Some(first_seen),
114            last_seen,
115        }
116    }
117}
118
119/// Builder for FieldData
120#[derive(Debug, Default)]
121pub struct FieldDataBuilder {
122    delivery_vectors: Option<Vec<String>>,
123    first_seen: Option<DateTime<Utc>>,
124    last_seen: Option<DateTime<Utc>>,
125}
126
127impl FieldDataBuilder {
128    pub fn delivery_vectors(mut self, vectors: Vec<String>) -> Self {
129        self.delivery_vectors = Some(vectors);
130        self
131    }
132
133    pub fn add_delivery_vector(mut self, vector: impl Into<String>) -> Self {
134        self.delivery_vectors
135            .get_or_insert_with(Vec::new)
136            .push(vector.into());
137        self
138    }
139
140    pub fn first_seen(mut self, timestamp: DateTime<Utc>) -> Self {
141        self.first_seen = Some(timestamp);
142        self
143    }
144
145    pub fn last_seen(mut self, timestamp: DateTime<Utc>) -> Self {
146        self.last_seen = Some(timestamp);
147        self
148    }
149
150    pub fn build(self) -> crate::error::Result<FieldData> {
151        // Validate that at least one field is present
152        if self.delivery_vectors.is_none() && self.first_seen.is_none() && self.last_seen.is_none()
153        {
154            return Err(crate::error::MaecError::ValidationError(
155                "FieldData must have at least one of: delivery_vectors, first_seen, or last_seen"
156                    .to_string(),
157            ));
158        }
159
160        Ok(FieldData {
161            delivery_vectors: self.delivery_vectors,
162            first_seen: self.first_seen,
163            last_seen: self.last_seen,
164        })
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_name_new() {
174        let name = Name::new("WannaCry");
175        assert_eq!(name.value, "WannaCry");
176        assert!(name.source.is_none());
177        assert!(name.confidence.is_none());
178    }
179
180    #[test]
181    fn test_name_from_string() {
182        let name: Name = "Emotet".into();
183        assert_eq!(name.value, "Emotet");
184    }
185
186    #[test]
187    fn test_field_data_builder() {
188        let field_data = FieldData::builder()
189            .add_delivery_vector("email")
190            .first_seen(Utc::now())
191            .build()
192            .unwrap();
193
194        assert!(field_data.delivery_vectors.is_some());
195        assert!(field_data.first_seen.is_some());
196    }
197
198    #[test]
199    fn test_field_data_validation() {
200        let result = FieldData::builder().build();
201        assert!(result.is_err());
202
203        let valid = FieldData::builder().add_delivery_vector("email").build();
204        assert!(valid.is_ok());
205    }
206}