sbom_tools/model/
cra_sidecar.rs1use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use std::path::Path;
15
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct CraSidecarMetadata {
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub security_contact: Option<String>,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub vulnerability_disclosure_url: Option<String>,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub support_end_date: Option<DateTime<Utc>>,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub manufacturer_name: Option<String>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub manufacturer_email: Option<String>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub product_name: Option<String>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub product_version: Option<String>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub ce_marking_reference: Option<String>,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub update_mechanism: Option<String>,
55}
56
57impl CraSidecarMetadata {
58 pub fn from_json_file(path: &Path) -> Result<Self, CraSidecarError> {
60 let content = std::fs::read_to_string(path)
61 .map_err(|e| CraSidecarError::IoError(e.to_string()))?;
62 serde_json::from_str(&content)
63 .map_err(|e| CraSidecarError::ParseError(e.to_string()))
64 }
65
66 pub fn from_yaml_file(path: &Path) -> Result<Self, CraSidecarError> {
68 let content = std::fs::read_to_string(path)
69 .map_err(|e| CraSidecarError::IoError(e.to_string()))?;
70 serde_yaml_ng::from_str(&content)
71 .map_err(|e| CraSidecarError::ParseError(e.to_string()))
72 }
73
74 pub fn from_file(path: &Path) -> Result<Self, CraSidecarError> {
76 let extension = path
77 .extension()
78 .and_then(|e| e.to_str())
79 .unwrap_or("")
80 .to_lowercase();
81
82 match extension.as_str() {
83 "json" => Self::from_json_file(path),
84 "yaml" | "yml" => Self::from_yaml_file(path),
85 _ => Err(CraSidecarError::UnsupportedFormat(extension)),
86 }
87 }
88
89 pub fn find_for_sbom(sbom_path: &Path) -> Option<Self> {
92 let parent = sbom_path.parent()?;
93 let stem = sbom_path.file_stem()?.to_str()?;
94
95 let patterns = [
97 format!("{}.cra.json", stem),
98 format!("{}.cra.yaml", stem),
99 format!("{}.cra.yml", stem),
100 format!("{}-cra.json", stem),
101 format!("{}-cra.yaml", stem),
102 ];
103
104 for pattern in &patterns {
105 let sidecar_path = parent.join(pattern);
106 if sidecar_path.exists() {
107 if let Ok(metadata) = Self::from_file(&sidecar_path) {
108 return Some(metadata);
109 }
110 }
111 }
112
113 None
114 }
115
116 pub fn has_cra_data(&self) -> bool {
118 self.security_contact.is_some()
119 || self.vulnerability_disclosure_url.is_some()
120 || self.support_end_date.is_some()
121 || self.manufacturer_name.is_some()
122 || self.ce_marking_reference.is_some()
123 }
124
125 pub fn example_json() -> String {
127 let example = Self {
128 security_contact: Some("security@example.com".to_string()),
129 vulnerability_disclosure_url: Some("https://example.com/security".to_string()),
130 support_end_date: Some(Utc::now() + chrono::Duration::days(365 * 2)),
131 manufacturer_name: Some("Example Corp".to_string()),
132 manufacturer_email: Some("contact@example.com".to_string()),
133 product_name: Some("Example Product".to_string()),
134 product_version: Some("1.0.0".to_string()),
135 ce_marking_reference: Some("EU-DoC-2024-001".to_string()),
136 update_mechanism: Some("Automatic OTA updates via secure channel".to_string()),
137 };
138 serde_json::to_string_pretty(&example).unwrap_or_default()
139 }
140}
141
142#[derive(Debug)]
144pub enum CraSidecarError {
145 IoError(String),
146 ParseError(String),
147 UnsupportedFormat(String),
148}
149
150impl std::fmt::Display for CraSidecarError {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 match self {
153 Self::IoError(e) => write!(f, "IO error reading sidecar file: {}", e),
154 Self::ParseError(e) => write!(f, "Parse error in sidecar file: {}", e),
155 Self::UnsupportedFormat(ext) => {
156 write!(f, "Unsupported sidecar file format: .{}", ext)
157 }
158 }
159 }
160}
161
162impl std::error::Error for CraSidecarError {}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_default_has_no_data() {
170 let sidecar = CraSidecarMetadata::default();
171 assert!(!sidecar.has_cra_data());
172 }
173
174 #[test]
175 fn test_has_cra_data_with_contact() {
176 let sidecar = CraSidecarMetadata {
177 security_contact: Some("security@example.com".to_string()),
178 ..Default::default()
179 };
180 assert!(sidecar.has_cra_data());
181 }
182
183 #[test]
184 fn test_example_json_is_valid() {
185 let json = CraSidecarMetadata::example_json();
186 let parsed: Result<CraSidecarMetadata, _> = serde_json::from_str(&json);
187 assert!(parsed.is_ok());
188 }
189
190 #[test]
191 fn test_json_roundtrip() {
192 let original = CraSidecarMetadata {
193 security_contact: Some("test@example.com".to_string()),
194 support_end_date: Some(Utc::now()),
195 ..Default::default()
196 };
197 let json = serde_json::to_string(&original).unwrap();
198 let parsed: CraSidecarMetadata = serde_json::from_str(&json).unwrap();
199 assert_eq!(original.security_contact, parsed.security_contact);
200 }
201}