use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
fn default_now() -> DateTime<Utc> {
Utc::now()
}
fn default_version() -> Option<String> {
Some("5.0".to_string())
}
pub trait MaecObject {
fn id(&self) -> &str;
fn type_(&self) -> &str;
fn created(&self) -> DateTime<Utc>;
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct CommonProperties {
#[serde(rename = "type")]
pub r#type: String,
pub id: String,
#[serde(default = "default_version", skip_serializing_if = "Option::is_none")]
pub schema_version: Option<String>,
#[serde(default = "default_now")]
pub created: DateTime<Utc>,
#[serde(default = "default_now")]
pub modified: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_by_ref: Option<String>,
#[serde(flatten)]
pub custom_properties: HashMap<String, serde_json::Value>,
}
impl Default for CommonProperties {
fn default() -> Self {
let now = Utc::now();
Self {
r#type: String::new(),
id: generate_maec_id("object"),
schema_version: Some("5.0".to_string()),
created: now,
modified: now,
created_by_ref: None,
custom_properties: HashMap::new(),
}
}
}
impl CommonProperties {
pub fn new(object_type: impl Into<String>, created_by_ref: Option<String>) -> Self {
let object_type = object_type.into();
let now = Utc::now();
Self {
r#type: object_type.clone(),
id: generate_maec_id(&object_type),
schema_version: Some("5.0".to_string()),
created: now,
modified: now,
created_by_ref,
custom_properties: HashMap::new(),
}
}
pub fn new_version(&mut self) {
self.modified = Utc::now();
}
}
impl MaecObject for CommonProperties {
fn id(&self) -> &str {
&self.id
}
fn type_(&self) -> &str {
&self.r#type
}
fn created(&self) -> DateTime<Utc> {
self.created
}
}
pub fn generate_maec_id(object_type: &str) -> String {
format!("{}--{}", object_type, Uuid::new_v4())
}
pub fn is_valid_maec_id(id: &str) -> bool {
let parts: Vec<&str> = id.split("--").collect();
if parts.len() != 2 {
return false;
}
Uuid::parse_str(parts[1]).is_ok()
}
pub fn extract_type_from_id(id: &str) -> Option<&str> {
let parts: Vec<&str> = id.split("--").collect();
if parts.len() == 2 && Uuid::parse_str(parts[1]).is_ok() {
Some(parts[0])
} else {
None
}
}
pub fn is_valid_ref_for_type(id: &str, expected_type: &str) -> bool {
extract_type_from_id(id)
.map(|t| t == expected_type)
.unwrap_or(false)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ExternalReference {
pub source_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_id: Option<String>,
}
impl ExternalReference {
pub fn new(source_name: impl Into<String>) -> Self {
Self {
source_name: source_name.into(),
description: None,
url: None,
external_id: None,
}
}
pub fn attack_technique(technique_id: impl Into<String>, name: impl Into<String>) -> Self {
let technique_id = technique_id.into();
Self {
source_name: "mitre-attack".to_string(),
description: Some(name.into()),
url: Some(format!(
"https://attack.mitre.org/techniques/{}",
technique_id
)),
external_id: Some(technique_id),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_maec_id() {
let id = generate_maec_id("malware-family");
assert!(id.starts_with("malware-family--"));
assert!(is_valid_maec_id(&id));
}
#[test]
fn test_is_valid_maec_id() {
assert!(is_valid_maec_id(
"malware-family--550e8400-e29b-41d4-a716-446655440000"
));
assert!(is_valid_maec_id(
"package--12345678-1234-1234-1234-123456789abc"
));
assert!(!is_valid_maec_id("invalid"));
assert!(!is_valid_maec_id("malware-family"));
assert!(!is_valid_maec_id("malware-family-no-uuid"));
}
#[test]
fn test_extract_type_from_id() {
assert_eq!(
extract_type_from_id("malware-family--550e8400-e29b-41d4-a716-446655440000"),
Some("malware-family")
);
assert_eq!(
extract_type_from_id("package--12345678-1234-1234-1234-123456789abc"),
Some("package")
);
assert_eq!(extract_type_from_id("invalid"), None);
}
#[test]
fn test_is_valid_ref_for_type() {
assert!(is_valid_ref_for_type(
"malware-family--550e8400-e29b-41d4-a716-446655440000",
"malware-family"
));
assert!(!is_valid_ref_for_type(
"package--550e8400-e29b-41d4-a716-446655440000",
"malware-family"
));
}
#[test]
fn test_common_properties_new() {
let common = CommonProperties::new("malware-family", None);
assert_eq!(common.r#type, "malware-family");
assert_eq!(common.schema_version, Some("5.0".to_string()));
assert!(common.id.starts_with("malware-family--"));
}
#[test]
fn test_new_version() {
let mut common = CommonProperties::new("malware-family", None);
let original_created = common.created;
let original_modified = common.modified;
std::thread::sleep(std::time::Duration::from_millis(10));
common.new_version();
assert_eq!(common.created, original_created);
assert!(common.modified > original_modified);
}
#[test]
fn test_external_reference_attack() {
let ref_obj = ExternalReference::attack_technique("T1055", "Process Injection");
assert_eq!(ref_obj.source_name, "mitre-attack");
assert_eq!(ref_obj.external_id, Some("T1055".to_string()));
assert!(ref_obj.url.unwrap().contains("T1055"));
}
}