1use base64::Engine;
4use base64::engine::general_purpose::STANDARD as BASE64;
5use chrono::SecondsFormat;
6use md5::Md5;
7use serde_json::{Map, Value, json};
8use sha1::Sha1;
9use sha2::{Digest, Sha256, Sha512};
10use uuid::Uuid;
11
12use crate::error::{Result, ValidationError, XarfError};
13use crate::model::{Contact, Evidence};
14use crate::parser::{ParseOptions, ParseResult, parse_value};
15
16pub const SPEC_VERSION: &str = "4.2.0";
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum HashAlgorithm {
22 #[default]
23 Sha256,
24 Sha512,
25 Sha1,
26 Md5,
27}
28
29impl HashAlgorithm {
30 pub fn prefix(self) -> &'static str {
31 match self {
32 Self::Sha256 => "sha256",
33 Self::Sha512 => "sha512",
34 Self::Sha1 => "sha1",
35 Self::Md5 => "md5",
36 }
37 }
38}
39
40pub fn create_evidence(content_type: impl Into<String>, payload: &[u8]) -> Evidence {
44 create_evidence_with_options(content_type, payload, EvidenceOptions::default())
45}
46
47#[derive(Debug, Clone, Default)]
49pub struct EvidenceOptions {
50 pub description: Option<String>,
51 pub hash_algorithm: HashAlgorithm,
52}
53
54pub fn create_evidence_with_options(
57 content_type: impl Into<String>,
58 payload: &[u8],
59 options: EvidenceOptions,
60) -> Evidence {
61 let hex_digest = match options.hash_algorithm {
62 HashAlgorithm::Sha256 => crate::hex::encode(Sha256::digest(payload).as_slice()),
63 HashAlgorithm::Sha512 => crate::hex::encode(Sha512::digest(payload).as_slice()),
64 HashAlgorithm::Sha1 => crate::hex::encode(Sha1::digest(payload).as_slice()),
65 HashAlgorithm::Md5 => crate::hex::encode(Md5::digest(payload).as_slice()),
66 };
67 let hash = format!("{}:{hex_digest}", options.hash_algorithm.prefix());
68 let encoded = BASE64.encode(payload);
69 Evidence {
70 content_type: content_type.into(),
71 payload: encoded,
72 description: options.description,
73 hash: Some(hash),
74 size: Some(payload.len() as u64),
75 }
76}
77
78#[derive(Debug, Clone)]
91pub struct ReportBuilder {
92 category: String,
93 type_name: String,
94 source_identifier: String,
95 reporter: Option<Contact>,
96 sender: Option<Contact>,
97 timestamp: Option<String>,
98 report_id: Option<String>,
99 xarf_version: Option<String>,
100 evidence_source: Option<String>,
101 evidence: Vec<Evidence>,
102 tags: Vec<String>,
103 confidence: Option<f64>,
104 description: Option<String>,
105 source_port: Option<u16>,
106 internal: Option<Map<String, Value>>,
107 extras: Map<String, Value>,
108}
109
110impl ReportBuilder {
111 pub fn new(
112 category: impl Into<String>,
113 type_name: impl Into<String>,
114 source_identifier: impl Into<String>,
115 ) -> Self {
116 Self {
117 category: category.into(),
118 type_name: type_name.into(),
119 source_identifier: source_identifier.into(),
120 reporter: None,
121 sender: None,
122 timestamp: None,
123 report_id: None,
124 xarf_version: None,
125 evidence_source: None,
126 evidence: Vec::new(),
127 tags: Vec::new(),
128 confidence: None,
129 description: None,
130 source_port: None,
131 internal: None,
132 extras: Map::new(),
133 }
134 }
135
136 pub fn reporter(mut self, contact: Contact) -> Self {
137 self.reporter = Some(contact);
138 self
139 }
140
141 pub fn sender(mut self, contact: Contact) -> Self {
142 self.sender = Some(contact);
143 self
144 }
145
146 pub fn timestamp(mut self, ts: impl Into<String>) -> Self {
147 self.timestamp = Some(ts.into());
148 self
149 }
150
151 pub fn report_id(mut self, id: impl Into<String>) -> Self {
152 self.report_id = Some(id.into());
153 self
154 }
155
156 pub fn xarf_version(mut self, v: impl Into<String>) -> Self {
157 self.xarf_version = Some(v.into());
158 self
159 }
160
161 pub fn evidence_source(mut self, s: impl Into<String>) -> Self {
162 self.evidence_source = Some(s.into());
163 self
164 }
165
166 pub fn add_evidence(mut self, evidence: Evidence) -> Self {
167 self.evidence.push(evidence);
168 self
169 }
170
171 pub fn tags<I, S>(mut self, tags: I) -> Self
172 where
173 I: IntoIterator<Item = S>,
174 S: Into<String>,
175 {
176 self.tags = tags.into_iter().map(Into::into).collect();
177 self
178 }
179
180 pub fn confidence(mut self, c: f64) -> Self {
181 self.confidence = Some(c);
182 self
183 }
184
185 pub fn description(mut self, d: impl Into<String>) -> Self {
186 self.description = Some(d.into());
187 self
188 }
189
190 pub fn source_port(mut self, p: u16) -> Self {
191 self.source_port = Some(p);
192 self
193 }
194
195 pub fn extra(mut self, key: impl Into<String>, value: Value) -> Self {
196 self.extras.insert(key.into(), value);
197 self
198 }
199
200 pub fn internal(mut self, internal: Map<String, Value>) -> Self {
201 self.internal = Some(internal);
202 self
203 }
204
205 pub fn build(self) -> Result<ParseResult> {
208 self.build_with_options(ParseOptions::default())
209 }
210
211 pub fn build_with_options(self, options: ParseOptions) -> Result<ParseResult> {
214 let reporter = self.reporter.ok_or_else(|| {
215 XarfError::Validation(vec![ValidationError::new(
216 "reporter",
217 "reporter contact is required",
218 )])
219 })?;
220 let sender = self.sender.ok_or_else(|| {
221 XarfError::Validation(vec![ValidationError::new(
222 "sender",
223 "sender contact is required",
224 )])
225 })?;
226
227 let timestamp = self
228 .timestamp
229 .unwrap_or_else(|| chrono::Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true));
230 let report_id = self.report_id.unwrap_or_else(|| Uuid::new_v4().to_string());
231 let xarf_version = self
232 .xarf_version
233 .unwrap_or_else(|| SPEC_VERSION.to_string());
234
235 let mut data = Map::new();
236 data.insert("xarf_version".into(), Value::String(xarf_version));
237 data.insert("report_id".into(), Value::String(report_id));
238 data.insert("timestamp".into(), Value::String(timestamp));
239 data.insert(
240 "reporter".into(),
241 serde_json::to_value(&reporter).expect("Contact serialises"),
242 );
243 data.insert(
244 "sender".into(),
245 serde_json::to_value(&sender).expect("Contact serialises"),
246 );
247 data.insert(
248 "source_identifier".into(),
249 Value::String(self.source_identifier),
250 );
251 data.insert("category".into(), Value::String(self.category));
252 data.insert("type".into(), Value::String(self.type_name));
253
254 if let Some(p) = self.source_port {
255 data.insert("source_port".into(), json!(p));
256 }
257 if let Some(s) = self.evidence_source {
258 data.insert("evidence_source".into(), Value::String(s));
259 }
260 if !self.evidence.is_empty() {
261 data.insert(
262 "evidence".into(),
263 serde_json::to_value(self.evidence).expect("Vec<Evidence> serialises"),
264 );
265 }
266 if !self.tags.is_empty() {
267 data.insert(
268 "tags".into(),
269 Value::Array(self.tags.into_iter().map(Value::String).collect()),
270 );
271 }
272 if let Some(c) = self.confidence {
273 data.insert("confidence".into(), json!(c));
274 }
275 if let Some(d) = self.description {
276 data.insert("description".into(), Value::String(d));
277 }
278 if let Some(internal) = self.internal {
279 data.insert("_internal".into(), Value::Object(internal));
280 }
281 for (k, v) in self.extras {
282 data.insert(k, v);
283 }
284
285 parse_value(Value::Object(data), options)
286 }
287}
288
289#[allow(clippy::too_many_arguments)]
292pub fn create_report(
293 category: &str,
294 type_name: &str,
295 source_identifier: &str,
296 reporter: Contact,
297 sender: Contact,
298 extras: Map<String, Value>,
299 options: CreateReportOptions,
300) -> Result<ParseResult> {
301 let mut builder = ReportBuilder::new(category, type_name, source_identifier)
302 .reporter(reporter)
303 .sender(sender);
304 for (k, v) in extras {
305 match k.as_str() {
309 "timestamp" => {
310 if let Some(s) = v.as_str() {
311 builder = builder.timestamp(s);
312 }
313 }
314 "report_id" => {
315 if let Some(s) = v.as_str() {
316 builder = builder.report_id(s);
317 }
318 }
319 "xarf_version" => {
320 if let Some(s) = v.as_str() {
321 builder = builder.xarf_version(s);
322 }
323 }
324 "evidence_source" => {
325 if let Some(s) = v.as_str() {
326 builder = builder.evidence_source(s);
327 }
328 }
329 "evidence" => {
330 if let Value::Array(arr) = &v {
331 for item in arr {
332 if let Ok(ev) = serde_json::from_value::<Evidence>(item.clone()) {
333 builder = builder.add_evidence(ev);
334 } else {
335 builder = builder.extra(k.clone(), v.clone());
336 break;
337 }
338 }
339 }
340 }
341 "tags" => {
342 if let Value::Array(arr) = &v {
343 let tags: Vec<String> = arr
344 .iter()
345 .filter_map(|t| t.as_str().map(String::from))
346 .collect();
347 builder = builder.tags(tags);
348 }
349 }
350 "confidence" => {
351 if let Some(c) = v.as_f64() {
352 builder = builder.confidence(c);
353 }
354 }
355 "description" => {
356 if let Some(s) = v.as_str() {
357 builder = builder.description(s);
358 }
359 }
360 "source_port" => {
361 if let Some(n) = v.as_u64() {
362 if n <= u16::MAX as u64 {
363 builder = builder.source_port(n as u16);
364 }
365 }
366 }
367 "_internal" => {
368 if let Value::Object(obj) = v {
369 builder = builder.internal(obj);
370 }
371 }
372 _ => {
373 builder = builder.extra(k, v);
374 }
375 }
376 }
377 builder.build_with_options(options.into())
378}
379
380#[derive(Debug, Clone, Copy, Default)]
382pub struct CreateReportOptions {
383 pub strict: bool,
384 pub show_missing_optional: bool,
385}
386
387impl From<CreateReportOptions> for ParseOptions {
388 fn from(o: CreateReportOptions) -> Self {
389 Self {
390 strict: o.strict,
391 show_missing_optional: o.show_missing_optional,
392 }
393 }
394}