Skip to main content

sbom_tools/diff/
multi.rs

1//! Multi-SBOM comparison data structures and engines.
2//!
3//! Supports:
4//! - 1:N diff-multi (baseline vs multiple targets)
5//! - Timeline analysis (incremental version evolution)
6//! - N×N matrix comparison (all pairs)
7
8use super::DiffResult;
9use crate::model::{NormalizedSbom, VulnerabilityCounts};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13// ============================================================================
14// SBOM Info (common metadata)
15// ============================================================================
16
17/// Basic information about an SBOM
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct SbomInfo {
20    /// Display name (user-provided label or filename)
21    pub name: String,
22    /// File path
23    pub file_path: String,
24    /// Format (CycloneDX, SPDX)
25    pub format: String,
26    /// Number of components
27    pub component_count: usize,
28    /// Number of dependencies
29    pub dependency_count: usize,
30    /// Vulnerability counts
31    pub vulnerability_counts: VulnerabilityCounts,
32    /// Timestamp if available
33    pub timestamp: Option<String>,
34}
35
36impl SbomInfo {
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// ============================================================================
51// 1:N MULTI-DIFF RESULT
52// ============================================================================
53
54/// Result of 1:N baseline comparison
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct MultiDiffResult {
57    /// Baseline SBOM information
58    pub baseline: SbomInfo,
59    /// Individual comparison results for each target
60    pub comparisons: Vec<ComparisonResult>,
61    /// Aggregated summary across all comparisons
62    pub summary: MultiDiffSummary,
63}
64
65/// Individual comparison result (baseline vs one target)
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ComparisonResult {
68    /// Target SBOM information
69    pub target: SbomInfo,
70    /// Full diff result (same as 1:1 diff)
71    pub diff: DiffResult,
72    /// Components unique to this target (not in baseline or other targets)
73    pub unique_components: Vec<String>,
74    /// Components shared with baseline but different from other targets
75    pub divergent_components: Vec<DivergentComponent>,
76}
77
78/// Component that differs across targets
79#[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    /// All versions across targets: target_name -> version
86    pub versions_across_targets: HashMap<String, String>,
87    pub divergence_type: DivergenceType,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub enum DivergenceType {
92    /// Version differs from baseline
93    VersionMismatch,
94    /// Component added (not in baseline)
95    Added,
96    /// Component removed (in baseline, not in target)
97    Removed,
98    /// Different license
99    LicenseMismatch,
100    /// Different supplier
101    SupplierMismatch,
102}
103
104// ============================================================================
105// MULTI-DIFF SUMMARY
106// ============================================================================
107
108/// Aggregated summary across all 1:N comparisons
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct MultiDiffSummary {
111    /// Total component count in baseline
112    pub baseline_component_count: usize,
113    /// Components present in ALL targets (including baseline)
114    pub universal_components: Vec<String>,
115    /// Components that have different versions across targets
116    pub variable_components: Vec<VariableComponent>,
117    /// Components missing from one or more targets
118    pub inconsistent_components: Vec<InconsistentComponent>,
119    /// Per-target deviation scores
120    pub deviation_scores: HashMap<String, f64>,
121    /// Maximum deviation from baseline
122    pub max_deviation: f64,
123    /// Aggregate vulnerability exposure across targets
124    pub vulnerability_matrix: VulnerabilityMatrix,
125}
126
127/// Component with version variation across targets
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct VariableComponent {
130    pub id: String,
131    pub name: String,
132    pub ecosystem: Option<String>,
133    pub version_spread: VersionSpread,
134    pub targets_with_component: Vec<String>,
135    pub security_impact: SecurityImpact,
136}
137
138/// Version distribution information
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct VersionSpread {
141    /// Baseline version
142    pub baseline: Option<String>,
143    /// Lowest version seen (as string, parsed if semver)
144    pub min_version: Option<String>,
145    /// Highest version seen
146    pub max_version: Option<String>,
147    /// All unique versions
148    pub unique_versions: Vec<String>,
149    /// True if all targets have same version
150    pub is_consistent: bool,
151    /// Number of major version differences
152    pub major_version_spread: u32,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub enum SecurityImpact {
157    /// Critical security component with version spread (e.g., openssl, curl)
158    Critical,
159    /// Security-relevant component
160    High,
161    /// Standard component
162    Medium,
163    /// Low-risk component
164    Low,
165}
166
167impl SecurityImpact {
168    pub fn label(&self) -> &'static str {
169        match self {
170            SecurityImpact::Critical => "CRITICAL",
171            SecurityImpact::High => "high",
172            SecurityImpact::Medium => "medium",
173            SecurityImpact::Low => "low",
174        }
175    }
176}
177
178/// Component missing from some targets
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct InconsistentComponent {
181    pub id: String,
182    pub name: String,
183    /// True if in baseline
184    pub in_baseline: bool,
185    /// Targets that have this component
186    pub present_in: Vec<String>,
187    /// Targets missing this component
188    pub missing_from: Vec<String>,
189}
190
191/// Vulnerability counts across all SBOMs
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct VulnerabilityMatrix {
194    /// Vulnerability counts per SBOM name
195    pub per_sbom: HashMap<String, VulnerabilityCounts>,
196    /// Vulnerabilities unique to specific targets
197    pub unique_vulnerabilities: HashMap<String, Vec<String>>,
198    /// Vulnerabilities common to all
199    pub common_vulnerabilities: Vec<String>,
200}
201
202// ============================================================================
203// TIMELINE RESULT
204// ============================================================================
205
206/// Timeline analysis result (incremental version evolution)
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct TimelineResult {
209    /// Ordered list of SBOMs in timeline
210    pub sboms: Vec<SbomInfo>,
211    /// Incremental diffs: [0→1, 1→2, 2→3, ...]
212    pub incremental_diffs: Vec<DiffResult>,
213    /// Cumulative diffs from first: [0→1, 0→2, 0→3, ...]
214    pub cumulative_from_first: Vec<DiffResult>,
215    /// High-level evolution summary
216    pub evolution_summary: EvolutionSummary,
217}
218
219/// High-level evolution across the timeline
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct EvolutionSummary {
222    /// Components added over the timeline
223    pub components_added: Vec<ComponentEvolution>,
224    /// Components removed over the timeline
225    pub components_removed: Vec<ComponentEvolution>,
226    /// Version progression for each component: component_id -> versions at each point
227    pub version_history: HashMap<String, Vec<VersionAtPoint>>,
228    /// Vulnerability trend over time
229    pub vulnerability_trend: Vec<VulnerabilitySnapshot>,
230    /// License changes over time
231    pub license_changes: Vec<LicenseChange>,
232    /// Dependency count trend
233    pub dependency_trend: Vec<DependencySnapshot>,
234    /// Compliance score trend across SBOM versions
235    pub compliance_trend: Vec<ComplianceSnapshot>,
236}
237
238/// Component lifecycle in the timeline
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct ComponentEvolution {
241    pub id: String,
242    pub name: String,
243    /// Index in timeline when first seen
244    pub first_seen_index: usize,
245    pub first_seen_version: String,
246    /// Index when last seen (None if still present at end)
247    pub last_seen_index: Option<usize>,
248    /// Current version (at end of timeline)
249    pub current_version: Option<String>,
250    /// Total version changes
251    pub version_change_count: usize,
252}
253
254/// Version of a component at a point in the timeline
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct VersionAtPoint {
257    pub sbom_index: usize,
258    pub sbom_name: String,
259    pub version: Option<String>,
260    pub change_type: VersionChangeType,
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
264pub enum VersionChangeType {
265    Initial,
266    MajorUpgrade,
267    MinorUpgrade,
268    PatchUpgrade,
269    Downgrade,
270    Unchanged,
271    Removed,
272    Absent,
273}
274
275impl VersionChangeType {
276    pub fn symbol(&self) -> &'static str {
277        match self {
278            VersionChangeType::Initial => "●",
279            VersionChangeType::MajorUpgrade => "⬆",
280            VersionChangeType::MinorUpgrade => "↑",
281            VersionChangeType::PatchUpgrade => "↗",
282            VersionChangeType::Downgrade => "⬇",
283            VersionChangeType::Unchanged => "─",
284            VersionChangeType::Removed => "✗",
285            VersionChangeType::Absent => " ",
286        }
287    }
288}
289
290/// Compliance score at a point in timeline
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ComplianceSnapshot {
293    pub sbom_index: usize,
294    pub sbom_name: String,
295    /// Compliance scores per standard: (standard_name, error_count, warning_count, is_compliant)
296    pub scores: Vec<ComplianceScoreEntry>,
297}
298
299/// A single compliance score entry for one standard
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct ComplianceScoreEntry {
302    pub standard: String,
303    pub error_count: usize,
304    pub warning_count: usize,
305    pub info_count: usize,
306    pub is_compliant: bool,
307}
308
309/// Vulnerability counts at a point in timeline
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct VulnerabilitySnapshot {
312    pub sbom_index: usize,
313    pub sbom_name: String,
314    pub counts: VulnerabilityCounts,
315    pub new_vulnerabilities: Vec<String>,
316    pub resolved_vulnerabilities: Vec<String>,
317}
318
319/// License change record
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct LicenseChange {
322    pub sbom_index: usize,
323    pub component_id: String,
324    pub component_name: String,
325    pub old_license: Vec<String>,
326    pub new_license: Vec<String>,
327    pub change_type: LicenseChangeType,
328}
329
330#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
331pub enum LicenseChangeType {
332    MorePermissive,
333    MoreRestrictive,
334    Incompatible,
335    Equivalent,
336}
337
338/// Dependency count at a point
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct DependencySnapshot {
341    pub sbom_index: usize,
342    pub sbom_name: String,
343    pub direct_dependencies: usize,
344    pub transitive_dependencies: usize,
345    pub total_edges: usize,
346}
347
348// ============================================================================
349// MATRIX RESULT
350// ============================================================================
351
352/// N×N comparison matrix result
353#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct MatrixResult {
355    /// All SBOMs in comparison
356    pub sboms: Vec<SbomInfo>,
357    /// Upper-triangle matrix of diff results
358    /// Access with matrix[i * sboms.len() + j] where i < j
359    pub diffs: Vec<Option<DiffResult>>,
360    /// Similarity scores (0.0 = completely different, 1.0 = identical)
361    /// Same indexing as diffs
362    pub similarity_scores: Vec<f64>,
363    /// Optional clustering based on similarity
364    pub clustering: Option<SbomClustering>,
365}
366
367impl MatrixResult {
368    /// Get diff between sboms[i] and sboms[j]
369    pub fn get_diff(&self, i: usize, j: usize) -> Option<&DiffResult> {
370        if i == j {
371            return None;
372        }
373        let (a, b) = if i < j { (i, j) } else { (j, i) };
374        let idx = self.matrix_index(a, b);
375        self.diffs.get(idx).and_then(|d| d.as_ref())
376    }
377
378    /// Get similarity between sboms[i] and sboms[j]
379    pub fn get_similarity(&self, i: usize, j: usize) -> f64 {
380        if i == j {
381            return 1.0;
382        }
383        let (a, b) = if i < j { (i, j) } else { (j, i) };
384        let idx = self.matrix_index(a, b);
385        self.similarity_scores.get(idx).copied().unwrap_or(0.0)
386    }
387
388    /// Calculate index in flattened upper-triangle matrix
389    fn matrix_index(&self, i: usize, j: usize) -> usize {
390        let n = self.sboms.len();
391        // Upper triangle index formula: i * (2n - i - 1) / 2 + (j - i - 1)
392        i * (2 * n - i - 1) / 2 + (j - i - 1)
393    }
394
395    /// Number of pairs (n choose 2)
396    pub fn num_pairs(&self) -> usize {
397        let n = self.sboms.len();
398        n * (n - 1) / 2
399    }
400}
401
402/// Clustering of similar SBOMs
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct SbomClustering {
405    /// Identified clusters of similar SBOMs
406    pub clusters: Vec<SbomCluster>,
407    /// Outliers that don't fit any cluster (indices into sboms)
408    pub outliers: Vec<usize>,
409    /// Clustering algorithm used
410    pub algorithm: String,
411    /// Threshold used for clustering
412    pub threshold: f64,
413}
414
415/// A cluster of similar SBOMs
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct SbomCluster {
418    /// Indices into sboms vec
419    pub members: Vec<usize>,
420    /// Most representative SBOM (centroid)
421    pub centroid_index: usize,
422    /// Average internal similarity
423    pub internal_similarity: f64,
424    /// Cluster label (auto-generated or user-provided)
425    pub label: Option<String>,
426}
427
428// ============================================================================
429// INCREMENTAL CHANGE SUMMARY (for timeline)
430// ============================================================================
431
432/// Summary of changes between two adjacent versions
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct IncrementalChange {
435    pub from_index: usize,
436    pub to_index: usize,
437    pub from_name: String,
438    pub to_name: String,
439    pub components_added: usize,
440    pub components_removed: usize,
441    pub components_modified: usize,
442    pub vulnerabilities_introduced: usize,
443    pub vulnerabilities_resolved: usize,
444}
445
446impl IncrementalChange {
447    pub fn from_diff(
448        from_idx: usize,
449        to_idx: usize,
450        from_name: &str,
451        to_name: &str,
452        diff: &DiffResult,
453    ) -> Self {
454        Self {
455            from_index: from_idx,
456            to_index: to_idx,
457            from_name: from_name.to_string(),
458            to_name: to_name.to_string(),
459            components_added: diff.summary.components_added,
460            components_removed: diff.summary.components_removed,
461            components_modified: diff.summary.components_modified,
462            vulnerabilities_introduced: diff.summary.vulnerabilities_introduced,
463            vulnerabilities_resolved: diff.summary.vulnerabilities_resolved,
464        }
465    }
466}