1use 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
19pub trait MaecObject {
21 fn id(&self) -> &str;
23
24 fn type_(&self) -> &str;
26
27 fn created(&self) -> DateTime<Utc>;
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub struct CommonProperties {
38 #[serde(rename = "type")]
40 pub r#type: String,
41
42 pub id: String,
44
45 #[serde(default = "default_version", skip_serializing_if = "Option::is_none")]
47 pub schema_version: Option<String>,
48
49 #[serde(default = "default_now")]
51 pub created: DateTime<Utc>,
52
53 #[serde(default = "default_now")]
55 pub modified: DateTime<Utc>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub created_by_ref: Option<String>,
60
61 #[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 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 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
153pub fn generate_maec_id(object_type: &str) -> String {
166 format!("{}--{}", object_type, Uuid::new_v4())
167}
168
169pub 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 Uuid::parse_str(parts[1]).is_ok()
191}
192
193pub 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
215pub 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242#[serde(rename_all = "snake_case")]
243pub struct ExternalReference {
244 pub source_name: String,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub description: Option<String>,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub url: Option<String>,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub external_id: Option<String>,
258}
259
260impl ExternalReference {
261 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 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}