1use super::DiffResult;
9use crate::model::{NormalizedSbom, VulnerabilityCounts};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct SbomInfo {
19 pub name: String,
21 pub file_path: String,
23 pub format: String,
25 pub component_count: usize,
27 pub dependency_count: usize,
29 pub vulnerability_counts: VulnerabilityCounts,
31 pub timestamp: Option<String>,
33}
34
35impl SbomInfo {
36 #[must_use]
37 pub fn from_sbom(sbom: &NormalizedSbom, name: String, file_path: String) -> Self {
38 Self {
39 name,
40 file_path,
41 format: sbom.document.format.to_string(),
42 component_count: sbom.component_count(),
43 dependency_count: sbom.edges.len(),
44 vulnerability_counts: sbom.vulnerability_counts(),
45 timestamp: Some(sbom.document.created.to_rfc3339()),
46 }
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct MultiDiffResult {
57 pub baseline: SbomInfo,
59 pub comparisons: Vec<ComparisonResult>,
61 pub summary: MultiDiffSummary,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ComparisonResult {
68 pub target: SbomInfo,
70 pub diff: DiffResult,
72 pub unique_components: Vec<String>,
74 pub divergent_components: Vec<DivergentComponent>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct DivergentComponent {
81 pub id: String,
82 pub name: String,
83 pub baseline_version: Option<String>,
84 pub target_version: String,
85 pub versions_across_targets: std::collections::BTreeMap<String, String>,
87 pub divergence_type: DivergenceType,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub enum DivergenceType {
92 VersionMismatch,
94 Added,
96 Removed,
98 LicenseMismatch,
100 SupplierMismatch,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct MultiDiffSummary {
111 pub baseline_component_count: usize,
113 pub universal_components: Vec<String>,
115 pub variable_components: Vec<VariableComponent>,
117 pub inconsistent_components: Vec<InconsistentComponent>,
119 pub deviation_scores: std::collections::BTreeMap<String, f64>,
122 pub max_deviation: f64,
124 pub vulnerability_matrix: VulnerabilityMatrix,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct VariableComponent {
131 pub id: String,
132 pub name: String,
133 pub ecosystem: Option<String>,
134 pub version_spread: VersionSpread,
135 pub targets_with_component: Vec<String>,
136 pub security_impact: SecurityImpact,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct VersionSpread {
142 pub baseline: Option<String>,
144 pub min_version: Option<String>,
146 pub max_version: Option<String>,
148 pub unique_versions: Vec<String>,
150 pub is_consistent: bool,
152 pub major_version_spread: u32,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub enum SecurityImpact {
158 Critical,
160 High,
162 Medium,
164 Low,
166}
167
168impl SecurityImpact {
169 #[must_use]
170 pub const fn label(&self) -> &'static str {
171 match self {
172 Self::Critical => "CRITICAL",
173 Self::High => "high",
174 Self::Medium => "medium",
175 Self::Low => "low",
176 }
177 }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct InconsistentComponent {
183 pub id: String,
184 pub name: String,
185 pub in_baseline: bool,
187 pub present_in: Vec<String>,
189 pub missing_from: Vec<String>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct VulnerabilityMatrix {
196 pub per_sbom: std::collections::BTreeMap<String, VulnerabilityCounts>,
198 pub unique_vulnerabilities: std::collections::BTreeMap<String, Vec<String>>,
200 pub common_vulnerabilities: Vec<String>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct TimelineResult {
211 pub sboms: Vec<SbomInfo>,
213 pub incremental_diffs: Vec<DiffResult>,
215 #[serde(default)]
221 pub incremental_pairs: Vec<TimelinePair>,
222 pub cumulative_from_first: Vec<DiffResult>,
224 #[serde(default)]
226 pub cumulative_pairs: Vec<TimelinePair>,
227 pub evolution_summary: EvolutionSummary,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct TimelinePair {
234 pub from_index: usize,
236 pub to_index: usize,
238 pub from_name: String,
240 pub to_name: String,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct EvolutionSummary {
247 pub components_added: Vec<ComponentEvolution>,
249 pub components_removed: Vec<ComponentEvolution>,
251 pub version_history: std::collections::BTreeMap<String, Vec<VersionAtPoint>>,
253 pub vulnerability_trend: Vec<VulnerabilitySnapshot>,
255 pub license_changes: Vec<LicenseChange>,
257 pub dependency_trend: Vec<DependencySnapshot>,
259 pub compliance_trend: Vec<ComplianceSnapshot>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct ComponentEvolution {
266 pub id: String,
267 pub name: String,
268 pub first_seen_index: usize,
270 pub first_seen_version: String,
271 pub last_seen_index: Option<usize>,
273 pub current_version: Option<String>,
275 pub version_change_count: usize,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct VersionAtPoint {
282 pub sbom_index: usize,
283 pub sbom_name: String,
284 pub version: Option<String>,
285 pub change_type: VersionChangeType,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289pub enum VersionChangeType {
290 Initial,
291 MajorUpgrade,
292 MinorUpgrade,
293 PatchUpgrade,
294 Downgrade,
295 Changed,
299 Unchanged,
300 Removed,
301 Absent,
302}
303
304impl VersionChangeType {
305 #[must_use]
306 pub const fn symbol(&self) -> &'static str {
307 match self {
308 Self::Initial => "●",
309 Self::MajorUpgrade => "⬆",
310 Self::MinorUpgrade => "↑",
311 Self::PatchUpgrade => "↗",
312 Self::Downgrade => "⬇",
313 Self::Changed => "~",
314 Self::Unchanged => "─",
315 Self::Removed => "✗",
316 Self::Absent => " ",
317 }
318 }
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct ComplianceSnapshot {
324 pub sbom_index: usize,
325 pub sbom_name: String,
326 pub scores: Vec<ComplianceScoreEntry>,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct ComplianceScoreEntry {
333 pub standard: String,
334 pub error_count: usize,
335 pub warning_count: usize,
336 pub info_count: usize,
337 pub is_compliant: bool,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct VulnerabilitySnapshot {
343 pub sbom_index: usize,
344 pub sbom_name: String,
345 pub counts: VulnerabilityCounts,
346 pub new_vulnerabilities: Vec<String>,
347 pub resolved_vulnerabilities: Vec<String>,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct LicenseChange {
353 pub sbom_index: usize,
354 pub component_id: String,
355 pub component_name: String,
356 pub old_license: Vec<String>,
357 pub new_license: Vec<String>,
358 pub change_type: LicenseChangeType,
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362pub enum LicenseChangeType {
363 MorePermissive,
364 MoreRestrictive,
365 Incompatible,
366 Equivalent,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct DependencySnapshot {
372 pub sbom_index: usize,
373 pub sbom_name: String,
374 pub direct_dependencies: usize,
375 pub transitive_dependencies: usize,
376 pub total_edges: usize,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct MatrixResult {
386 pub sboms: Vec<SbomInfo>,
388 pub diffs: Vec<Option<DiffResult>>,
391 pub similarity_scores: Vec<f64>,
397 pub clustering: Option<SbomClustering>,
399}
400
401impl MatrixResult {
402 #[must_use]
404 pub fn get_diff(&self, i: usize, j: usize) -> Option<&DiffResult> {
405 if i == j {
406 return None;
407 }
408 let (a, b) = if i < j { (i, j) } else { (j, i) };
409 let idx = self.matrix_index(a, b);
410 self.diffs.get(idx).and_then(|d| d.as_ref())
411 }
412
413 #[must_use]
415 pub fn get_similarity(&self, i: usize, j: usize) -> f64 {
416 if i == j {
417 return 1.0;
418 }
419 let (a, b) = if i < j { (i, j) } else { (j, i) };
420 let idx = self.matrix_index(a, b);
421 self.similarity_scores.get(idx).copied().unwrap_or(0.0)
422 }
423
424 fn matrix_index(&self, i: usize, j: usize) -> usize {
426 let n = self.sboms.len();
427 i * (2 * n - i - 1) / 2 + (j - i - 1)
429 }
430
431 #[must_use]
433 pub fn num_pairs(&self) -> usize {
434 let n = self.sboms.len();
435 n * (n - 1) / 2
436 }
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct SbomClustering {
442 pub clusters: Vec<SbomCluster>,
444 pub outliers: Vec<usize>,
446 pub algorithm: String,
448 pub threshold: f64,
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct SbomCluster {
455 pub members: Vec<usize>,
457 pub centroid_index: usize,
459 pub internal_similarity: f64,
461 pub label: Option<String>,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
471pub struct IncrementalChange {
472 pub from_index: usize,
473 pub to_index: usize,
474 pub from_name: String,
475 pub to_name: String,
476 pub components_added: usize,
477 pub components_removed: usize,
478 pub components_modified: usize,
479 pub vulnerabilities_introduced: usize,
480 pub vulnerabilities_resolved: usize,
481}
482
483impl IncrementalChange {
484 #[must_use]
485 pub fn from_diff(
486 from_idx: usize,
487 to_idx: usize,
488 from_name: &str,
489 to_name: &str,
490 diff: &DiffResult,
491 ) -> Self {
492 Self {
493 from_index: from_idx,
494 to_index: to_idx,
495 from_name: from_name.to_string(),
496 to_name: to_name.to_string(),
497 components_added: diff.summary.components_added,
498 components_removed: diff.summary.components_removed,
499 components_modified: diff.summary.components_modified,
500 vulnerabilities_introduced: diff.summary.vulnerabilities_introduced,
501 vulnerabilities_resolved: diff.summary.vulnerabilities_resolved,
502 }
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn test_security_impact_label() {
512 assert_eq!(SecurityImpact::Critical.label(), "CRITICAL");
513 assert_eq!(SecurityImpact::High.label(), "high");
514 assert_eq!(SecurityImpact::Medium.label(), "medium");
515 assert_eq!(SecurityImpact::Low.label(), "low");
516 }
517
518 #[test]
519 fn test_version_change_type_symbol() {
520 assert_eq!(VersionChangeType::Initial.symbol(), "●");
521 assert_eq!(VersionChangeType::MajorUpgrade.symbol(), "⬆");
522 assert_eq!(VersionChangeType::MinorUpgrade.symbol(), "↑");
523 assert_eq!(VersionChangeType::PatchUpgrade.symbol(), "↗");
524 assert_eq!(VersionChangeType::Downgrade.symbol(), "⬇");
525 assert_eq!(VersionChangeType::Unchanged.symbol(), "─");
526 assert_eq!(VersionChangeType::Removed.symbol(), "✗");
527 assert_eq!(VersionChangeType::Absent.symbol(), " ");
528 }
529
530 fn make_matrix(n: usize) -> MatrixResult {
531 let sboms = (0..n)
532 .map(|i| SbomInfo {
533 name: format!("sbom-{i}"),
534 file_path: format!("sbom-{i}.json"),
535 format: "CycloneDX".into(),
536 component_count: 10,
537 dependency_count: 5,
538 vulnerability_counts: VulnerabilityCounts::default(),
539 timestamp: None,
540 })
541 .collect::<Vec<_>>();
542 let num_pairs = n * (n - 1) / 2;
543 MatrixResult {
544 sboms,
545 diffs: vec![None; num_pairs],
546 similarity_scores: vec![0.5; num_pairs],
547 clustering: None,
548 }
549 }
550
551 #[test]
552 fn test_matrix_result_get_diff_self() {
553 let matrix = make_matrix(3);
554 assert!(matrix.get_diff(0, 0).is_none());
555 assert!(matrix.get_diff(1, 1).is_none());
556 }
557
558 #[test]
559 fn test_matrix_result_get_similarity_self() {
560 let matrix = make_matrix(3);
561 assert_eq!(matrix.get_similarity(0, 0), 1.0);
562 assert_eq!(matrix.get_similarity(2, 2), 1.0);
563 }
564
565 #[test]
566 fn test_matrix_result_get_similarity_symmetric() {
567 let matrix = make_matrix(3);
568 assert_eq!(matrix.get_similarity(0, 1), matrix.get_similarity(1, 0));
569 assert_eq!(matrix.get_similarity(0, 2), matrix.get_similarity(2, 0));
570 }
571
572 #[test]
573 fn test_matrix_result_num_pairs() {
574 assert_eq!(make_matrix(3).num_pairs(), 3);
575 assert_eq!(make_matrix(4).num_pairs(), 6);
576 assert_eq!(make_matrix(5).num_pairs(), 10);
577 }
578
579 #[test]
580 fn test_incremental_change_from_diff() {
581 let mut diff = DiffResult::new();
582 diff.summary.components_added = 5;
583 diff.summary.components_removed = 2;
584 diff.summary.components_modified = 3;
585 diff.summary.vulnerabilities_introduced = 1;
586 diff.summary.vulnerabilities_resolved = 4;
587
588 let change = IncrementalChange::from_diff(0, 1, "v1.0", "v2.0", &diff);
589 assert_eq!(change.from_index, 0);
590 assert_eq!(change.to_index, 1);
591 assert_eq!(change.from_name, "v1.0");
592 assert_eq!(change.to_name, "v2.0");
593 assert_eq!(change.components_added, 5);
594 assert_eq!(change.components_removed, 2);
595 assert_eq!(change.components_modified, 3);
596 assert_eq!(change.vulnerabilities_introduced, 1);
597 assert_eq!(change.vulnerabilities_resolved, 4);
598 }
599
600 #[test]
601 fn test_divergence_type_variants() {
602 let variants = [
604 DivergenceType::VersionMismatch,
605 DivergenceType::Added,
606 DivergenceType::Removed,
607 DivergenceType::LicenseMismatch,
608 DivergenceType::SupplierMismatch,
609 ];
610 for (i, a) in variants.iter().enumerate() {
611 for (j, b) in variants.iter().enumerate() {
612 if i == j {
613 assert_eq!(a, b);
614 } else {
615 assert_ne!(a, b);
616 }
617 }
618 }
619 }
620
621 #[test]
622 fn test_license_change_type_variants() {
623 let variants = [
624 LicenseChangeType::MorePermissive,
625 LicenseChangeType::MoreRestrictive,
626 LicenseChangeType::Incompatible,
627 LicenseChangeType::Equivalent,
628 ];
629 for (i, a) in variants.iter().enumerate() {
630 for (j, b) in variants.iter().enumerate() {
631 if i == j {
632 assert_eq!(a, b);
633 } else {
634 assert_ne!(a, b);
635 }
636 }
637 }
638 }
639}