Skip to main content

sbom_tools/model/
metadata.rs

1//! Metadata structures for SBOM documents and components.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// SBOM format type
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum SbomFormat {
9    CycloneDx,
10    Spdx,
11}
12
13impl std::fmt::Display for SbomFormat {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        match self {
16            Self::CycloneDx => write!(f, "CycloneDX"),
17            Self::Spdx => write!(f, "SPDX"),
18        }
19    }
20}
21
22/// Document-level metadata
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct DocumentMetadata {
25    /// SBOM format type
26    pub format: SbomFormat,
27    /// Format version (e.g., "1.5" for `CycloneDX`)
28    pub format_version: String,
29    /// Specification version
30    pub spec_version: String,
31    /// Serial number or document namespace
32    pub serial_number: Option<String>,
33    /// Document revision counter (CycloneDX top-level `version`, bumped on
34    /// each BOM revision of the same `serialNumber`). `None` for formats
35    /// without a revision counter (SPDX) and for documents that omit it.
36    /// Additive: skipped in JSON when absent.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub doc_version: Option<u32>,
39    /// Creation timestamp
40    pub created: DateTime<Utc>,
41    /// Creators/authors
42    pub creators: Vec<Creator>,
43    /// Document name
44    pub name: Option<String>,
45    /// Security contact for vulnerability disclosure (CRA requirement)
46    pub security_contact: Option<String>,
47    /// URL for vulnerability disclosure policy/portal
48    pub vulnerability_disclosure_url: Option<String>,
49    /// Support/end-of-life date for security updates
50    pub support_end_date: Option<DateTime<Utc>>,
51    /// SBOM lifecycle phase (e.g., "build", "pre-build", "operations")
52    pub lifecycle_phase: Option<String>,
53    /// Self-declared completeness level (from CycloneDX compositions)
54    pub completeness_declaration: CompletenessDeclaration,
55    /// Digital signature information (from CycloneDX signature field)
56    pub signature: Option<SignatureInfo>,
57    /// Distribution classification (e.g., TLP: CLEAR, GREEN, AMBER, RED)
58    pub distribution_classification: Option<String>,
59    /// Number of data provenance citations (CycloneDX 1.7+)
60    pub citations_count: usize,
61}
62
63/// Self-declared completeness level of the SBOM
64///
65/// Derived from CycloneDX compositions aggregate field, which declares
66/// whether the SBOM inventory is complete, incomplete, or unknown.
67#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
68#[non_exhaustive]
69pub enum CompletenessDeclaration {
70    /// SBOM author declares the inventory is complete
71    Complete,
72    /// SBOM author declares the inventory includes only first-party components
73    IncompleteFirstPartyOnly,
74    /// SBOM author declares the inventory includes only third-party components
75    IncompleteThirdPartyOnly,
76    /// SBOM author declares the inventory is incomplete
77    Incomplete,
78    /// No completeness declaration or explicitly unknown
79    #[default]
80    Unknown,
81    /// Completeness was declared but with an unrecognized value
82    NotSpecified,
83}
84
85impl std::fmt::Display for CompletenessDeclaration {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        match self {
88            Self::Complete => write!(f, "complete"),
89            Self::IncompleteFirstPartyOnly => write!(f, "incomplete (first-party only)"),
90            Self::IncompleteThirdPartyOnly => write!(f, "incomplete (third-party only)"),
91            Self::Incomplete => write!(f, "incomplete"),
92            Self::Unknown => write!(f, "unknown"),
93            Self::NotSpecified => write!(f, "not specified"),
94        }
95    }
96}
97
98/// Digital signature information for the SBOM document
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct SignatureInfo {
101    /// Signature algorithm (e.g., "ES256", "RS256", "Ed25519")
102    pub algorithm: String,
103    /// Whether the signature appears structurally valid (has algorithm + value)
104    pub has_value: bool,
105}
106
107impl DocumentMetadata {
108    /// Whether the document carries a real creation timestamp.
109    ///
110    /// Parsers substitute [`DateTime::UNIX_EPOCH`] when the source document
111    /// has no (or an unparseable) timestamp, so the content hash stays
112    /// deterministic. Consumers that compute an AGE or gate on freshness
113    /// must use this to distinguish "no timestamp" from a document that
114    /// genuinely claims 1970 — otherwise a missing timestamp reads as a
115    /// ~55-year-old SBOM.
116    #[must_use]
117    pub fn has_known_timestamp(&self) -> bool {
118        self.created.timestamp() > 0
119    }
120}
121
122impl Default for DocumentMetadata {
123    fn default() -> Self {
124        Self {
125            format: SbomFormat::CycloneDx,
126            format_version: String::new(),
127            spec_version: String::new(),
128            serial_number: None,
129            doc_version: None,
130            created: Utc::now(),
131            creators: Vec::new(),
132            name: None,
133            security_contact: None,
134            vulnerability_disclosure_url: None,
135            support_end_date: None,
136            lifecycle_phase: None,
137            completeness_declaration: CompletenessDeclaration::default(),
138            signature: None,
139            distribution_classification: None,
140            citations_count: 0,
141        }
142    }
143}
144
145/// Creator information
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct Creator {
148    /// Creator type
149    pub creator_type: CreatorType,
150    /// Creator name or identifier
151    pub name: String,
152    /// Optional email
153    pub email: Option<String>,
154}
155
156/// Type of creator
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158pub enum CreatorType {
159    Person,
160    Organization,
161    Tool,
162}
163
164/// Organization/supplier information
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct Organization {
167    /// Organization name
168    pub name: String,
169    /// Contact URLs
170    pub urls: Vec<String>,
171    /// Contact emails
172    pub contacts: Vec<Contact>,
173}
174
175impl Organization {
176    /// Create a new organization with just a name
177    #[must_use]
178    pub const fn new(name: String) -> Self {
179        Self {
180            name,
181            urls: Vec::new(),
182            contacts: Vec::new(),
183        }
184    }
185}
186
187/// Contact information
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct Contact {
190    /// Contact name
191    pub name: Option<String>,
192    /// Email address
193    pub email: Option<String>,
194    /// Phone number
195    pub phone: Option<String>,
196}
197
198/// Component type classification
199#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
200#[non_exhaustive]
201pub enum ComponentType {
202    Application,
203    Framework,
204    #[default]
205    Library,
206    Container,
207    OperatingSystem,
208    Device,
209    Firmware,
210    File,
211    Data,
212    MachineLearningModel,
213    Platform,
214    DeviceDriver,
215    Cryptographic,
216    Other(String),
217}
218
219impl std::fmt::Display for ComponentType {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        match self {
222            Self::Application => write!(f, "application"),
223            Self::Framework => write!(f, "framework"),
224            Self::Library => write!(f, "library"),
225            Self::Container => write!(f, "container"),
226            Self::OperatingSystem => write!(f, "operating-system"),
227            Self::Device => write!(f, "device"),
228            Self::Firmware => write!(f, "firmware"),
229            Self::File => write!(f, "file"),
230            Self::Data => write!(f, "data"),
231            Self::MachineLearningModel => write!(f, "machine-learning-model"),
232            Self::Platform => write!(f, "platform"),
233            Self::DeviceDriver => write!(f, "device-driver"),
234            Self::Cryptographic => write!(f, "cryptographic"),
235            Self::Other(s) => write!(f, "{s}"),
236        }
237    }
238}
239
240/// Where a hash came from — determines whether integrity verification trusts
241/// it as an EXPECTED baseline. Runtime-only, never serialized: a hash parsed
242/// from an SBOM is author-attested (`Authored`); one this tool added during
243/// enrichment (fetched from a registry / served from cache) is `Enriched` and
244/// must NOT be used as the baseline to verify local files against — that
245/// would be circular (the tool checks a file against a hash it fetched from
246/// the same source that could host the file).
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
248pub enum HashProvenance {
249    /// Present in the parsed SBOM — author-attested, trusted as a baseline.
250    #[default]
251    Authored,
252    /// Added by this tool's enrichment — informational, not a verify baseline.
253    Enriched,
254}
255
256/// Cryptographic hash.
257///
258/// `PartialEq`/`Eq`/`Hash` intentionally ignore [`provenance`](Self::provenance)
259/// so dedup, content-hashing, and diff identity depend only on the
260/// algorithm+value (provenance is a runtime trust marker, not part of the
261/// hash's identity).
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct Hash {
264    /// Hash algorithm
265    pub algorithm: HashAlgorithm,
266    /// Hash value (hex encoded)
267    pub value: String,
268    /// Trust provenance (runtime only — never serialized).
269    #[serde(skip)]
270    pub provenance: HashProvenance,
271}
272
273impl PartialEq for Hash {
274    fn eq(&self, other: &Self) -> bool {
275        self.algorithm == other.algorithm && self.value == other.value
276    }
277}
278
279impl Eq for Hash {}
280
281impl std::hash::Hash for Hash {
282    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
283        self.algorithm.hash(state);
284        self.value.hash(state);
285    }
286}
287
288impl Hash {
289    /// Create an author-attested hash (parsed from an SBOM).
290    #[must_use]
291    pub const fn new(algorithm: HashAlgorithm, value: String) -> Self {
292        Self {
293            algorithm,
294            value,
295            provenance: HashProvenance::Authored,
296        }
297    }
298
299    /// Create an enrichment-sourced hash (fetched from a registry / cache).
300    /// Not trusted as an integrity baseline by `verify`.
301    #[must_use]
302    pub const fn enriched(algorithm: HashAlgorithm, value: String) -> Self {
303        Self {
304            algorithm,
305            value,
306            provenance: HashProvenance::Enriched,
307        }
308    }
309}
310
311/// Hash algorithm types
312#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
313pub enum HashAlgorithm {
314    Md5,
315    Sha1,
316    Sha256,
317    Sha384,
318    Sha512,
319    Sha3_256,
320    Sha3_384,
321    Sha3_512,
322    Blake2b256,
323    Blake2b384,
324    Blake2b512,
325    Blake3,
326    Streebog256,
327    Streebog512,
328    Other(String),
329}
330
331impl std::fmt::Display for HashAlgorithm {
332    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333        match self {
334            Self::Md5 => write!(f, "MD5"),
335            Self::Sha1 => write!(f, "SHA-1"),
336            Self::Sha256 => write!(f, "SHA-256"),
337            Self::Sha384 => write!(f, "SHA-384"),
338            Self::Sha512 => write!(f, "SHA-512"),
339            Self::Sha3_256 => write!(f, "SHA3-256"),
340            Self::Sha3_384 => write!(f, "SHA3-384"),
341            Self::Sha3_512 => write!(f, "SHA3-512"),
342            Self::Blake2b256 => write!(f, "BLAKE2b-256"),
343            Self::Blake2b384 => write!(f, "BLAKE2b-384"),
344            Self::Blake2b512 => write!(f, "BLAKE2b-512"),
345            Self::Blake3 => write!(f, "BLAKE3"),
346            Self::Streebog256 => write!(f, "Streebog-256"),
347            Self::Streebog512 => write!(f, "Streebog-512"),
348            Self::Other(s) => write!(f, "{s}"),
349        }
350    }
351}
352
353/// External reference
354#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
355pub struct ExternalReference {
356    /// Reference type
357    pub ref_type: ExternalRefType,
358    /// URL or locator
359    pub url: String,
360    /// Comment or description
361    pub comment: Option<String>,
362    /// Hash of the referenced content
363    pub hashes: Vec<Hash>,
364}
365
366/// External reference types
367#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
368pub enum ExternalRefType {
369    Vcs,
370    IssueTracker,
371    Website,
372    Advisories,
373    Bom,
374    MailingList,
375    Social,
376    Chat,
377    Documentation,
378    Support,
379    SourceDistribution,
380    BinaryDistribution,
381    License,
382    BuildMeta,
383    BuildSystem,
384    ReleaseNotes,
385    SecurityContact,
386    ModelCard,
387    Log,
388    Configuration,
389    Evidence,
390    Formulation,
391    Attestation,
392    ThreatModel,
393    AdversaryModel,
394    RiskAssessment,
395    VulnerabilityAssertion,
396    ExploitabilityStatement,
397    Pentest,
398    StaticAnalysis,
399    DynamicAnalysis,
400    RuntimeAnalysis,
401    ComponentAnalysis,
402    Maturity,
403    Certification,
404    QualityMetrics,
405    Codified,
406    Citation,
407    Patent,
408    PatentAssertion,
409    PatentFamily,
410    Other(String),
411}
412
413impl std::fmt::Display for ExternalRefType {
414    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
415        match self {
416            Self::Vcs => write!(f, "vcs"),
417            Self::IssueTracker => write!(f, "issue-tracker"),
418            Self::Website => write!(f, "website"),
419            Self::Advisories => write!(f, "advisories"),
420            Self::Bom => write!(f, "bom"),
421            Self::MailingList => write!(f, "mailing-list"),
422            Self::Social => write!(f, "social"),
423            Self::Chat => write!(f, "chat"),
424            Self::Documentation => write!(f, "documentation"),
425            Self::Support => write!(f, "support"),
426            Self::SourceDistribution => write!(f, "distribution"),
427            Self::BinaryDistribution => write!(f, "distribution-intake"),
428            Self::License => write!(f, "license"),
429            Self::BuildMeta => write!(f, "build-meta"),
430            Self::BuildSystem => write!(f, "build-system"),
431            Self::ReleaseNotes => write!(f, "release-notes"),
432            Self::SecurityContact => write!(f, "security-contact"),
433            Self::ModelCard => write!(f, "model-card"),
434            Self::Log => write!(f, "log"),
435            Self::Configuration => write!(f, "configuration"),
436            Self::Evidence => write!(f, "evidence"),
437            Self::Formulation => write!(f, "formulation"),
438            Self::Attestation => write!(f, "attestation"),
439            Self::ThreatModel => write!(f, "threat-model"),
440            Self::AdversaryModel => write!(f, "adversary-model"),
441            Self::RiskAssessment => write!(f, "risk-assessment"),
442            Self::VulnerabilityAssertion => write!(f, "vulnerability-assertion"),
443            Self::ExploitabilityStatement => write!(f, "exploitability-statement"),
444            Self::Pentest => write!(f, "pentest-report"),
445            Self::StaticAnalysis => write!(f, "static-analysis-report"),
446            Self::DynamicAnalysis => write!(f, "dynamic-analysis-report"),
447            Self::RuntimeAnalysis => write!(f, "runtime-analysis-report"),
448            Self::ComponentAnalysis => write!(f, "component-analysis-report"),
449            Self::Maturity => write!(f, "maturity-report"),
450            Self::Certification => write!(f, "certification-report"),
451            Self::QualityMetrics => write!(f, "quality-metrics"),
452            Self::Codified => write!(f, "codified"),
453            Self::Citation => write!(f, "citation"),
454            Self::Patent => write!(f, "patent"),
455            Self::PatentAssertion => write!(f, "patent-assertion"),
456            Self::PatentFamily => write!(f, "patent-family"),
457            Self::Other(s) => write!(f, "{s}"),
458        }
459    }
460}
461
462/// Dependency relationship type
463#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
464#[non_exhaustive]
465pub enum DependencyType {
466    /// Direct dependency
467    DependsOn,
468    /// Optional dependency
469    OptionalDependsOn,
470    /// Development dependency
471    DevDependsOn,
472    /// Build dependency
473    BuildDependsOn,
474    /// Test dependency
475    TestDependsOn,
476    /// Runtime dependency
477    RuntimeDependsOn,
478    /// Provided dependency (e.g., Java provided scope)
479    ProvidedDependsOn,
480    /// Describes relationship (SPDX)
481    Describes,
482    /// Generates relationship
483    Generates,
484    /// Contains relationship
485    Contains,
486    /// Ancestor of
487    AncestorOf,
488    /// Variant of
489    VariantOf,
490    /// Distribution artifact
491    DistributionArtifact,
492    /// Patch for
493    PatchFor,
494    /// Copy of
495    CopyOf,
496    /// File added
497    FileAdded,
498    /// File deleted
499    FileDeleted,
500    /// File modified
501    FileModified,
502    /// Dynamic link
503    DynamicLink,
504    /// Static link
505    StaticLink,
506    /// Provides (CycloneDX 1.7: library provides/implements a crypto asset)
507    Provides,
508    /// Other relationship
509    Other(String),
510}
511
512impl std::fmt::Display for DependencyType {
513    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
514        match self {
515            Self::DependsOn => write!(f, "depends-on"),
516            Self::OptionalDependsOn => write!(f, "optional-depends-on"),
517            Self::DevDependsOn => write!(f, "dev-depends-on"),
518            Self::BuildDependsOn => write!(f, "build-depends-on"),
519            Self::TestDependsOn => write!(f, "test-depends-on"),
520            Self::RuntimeDependsOn => write!(f, "runtime-depends-on"),
521            Self::ProvidedDependsOn => write!(f, "provided-depends-on"),
522            Self::Describes => write!(f, "describes"),
523            Self::Generates => write!(f, "generates"),
524            Self::Contains => write!(f, "contains"),
525            Self::AncestorOf => write!(f, "ancestor-of"),
526            Self::VariantOf => write!(f, "variant-of"),
527            Self::DistributionArtifact => write!(f, "distribution-artifact"),
528            Self::PatchFor => write!(f, "patch-for"),
529            Self::CopyOf => write!(f, "copy-of"),
530            Self::FileAdded => write!(f, "file-added"),
531            Self::FileDeleted => write!(f, "file-deleted"),
532            Self::FileModified => write!(f, "file-modified"),
533            Self::DynamicLink => write!(f, "dynamic-link"),
534            Self::StaticLink => write!(f, "static-link"),
535            Self::Provides => write!(f, "provides"),
536            Self::Other(s) => write!(f, "{s}"),
537        }
538    }
539}
540
541/// Dependency scope
542#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
543pub enum DependencyScope {
544    #[default]
545    Required,
546    Optional,
547    Excluded,
548}
549
550impl std::fmt::Display for DependencyScope {
551    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552        match self {
553            Self::Required => write!(f, "required"),
554            Self::Optional => write!(f, "optional"),
555            Self::Excluded => write!(f, "excluded"),
556        }
557    }
558}
559
560/// Format-specific extensions that don't map to the canonical model
561#[derive(Debug, Clone, Default, Serialize, Deserialize)]
562pub struct FormatExtensions {
563    /// CycloneDX-specific extensions
564    pub cyclonedx: Option<serde_json::Value>,
565    /// SPDX-specific extensions
566    pub spdx: Option<serde_json::Value>,
567    /// CycloneDX 1.6 Attestations (CDXA): normalized `declarations` plus
568    /// `definitions.standards`. Populated only by the CycloneDX JSON parser
569    /// for `specVersion >= 1.6` documents that carry these sections; `None`
570    /// otherwise (SPDX, older CycloneDX, XML input). Additive: skipped in
571    /// serialized output when absent, so documents without declarations
572    /// serialize byte-identically to previous releases. Prefer the
573    /// [`crate::model::NormalizedSbom::declarations`] accessor.
574    #[serde(default, skip_serializing_if = "Option::is_none")]
575    pub declarations: Option<crate::model::AttestationDeclarations>,
576}
577
578/// Component-level extensions
579#[derive(Debug, Clone, Default, Serialize, Deserialize)]
580pub struct ComponentExtensions {
581    /// Properties from `CycloneDX`
582    pub properties: Vec<Property>,
583    /// Annotations from SPDX
584    pub annotations: Vec<Annotation>,
585    /// Raw extension data.
586    ///
587    /// Occupied by the SPDX-3 AI-profile bridge (`parsers::spdx3`), which mirrors
588    /// non-typed AI signals here in CycloneDX `mlModel.modelCard` layout so the
589    /// AI-readiness scorer can read them. Do NOT repurpose this for round-trip
590    /// preservation — use [`source_json`](Self::source_json) instead.
591    pub raw: Option<serde_json::Value>,
592    /// Verbatim source JSON object for this component, captured for cross-format
593    /// conversion fidelity.
594    ///
595    /// Opt-in and convert-only: populated solely when the `convert`/`--preserve`
596    /// path is active (see [`crate::serialization::emit`]), keeping it out of the
597    /// normal parse hot path so memory stays bounded. Boxed to keep
598    /// [`ComponentExtensions`] small when the slot is empty (the common case),
599    /// and skipped on serialization when absent.
600    #[serde(default, skip_serializing_if = "Option::is_none")]
601    pub source_json: Option<Box<serde_json::Value>>,
602}
603
604/// Key-value property
605#[derive(Debug, Clone, Serialize, Deserialize)]
606pub struct Property {
607    pub name: String,
608    pub value: String,
609}
610
611/// Annotation/comment
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct Annotation {
614    pub annotator: String,
615    pub annotation_date: DateTime<Utc>,
616    pub annotation_type: String,
617    pub comment: String,
618}
619
620/// Machine learning model metadata (CycloneDX 1.5+)
621///
622/// Structured information about trained ML models, including architecture,
623/// approach, quantization, and environmental impact.
624#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
625#[non_exhaustive]
626pub struct MlModelInfo {
627    /// ML approach type: "supervised", "unsupervised", "reinforcement-learning", "semi-supervised"
628    pub approach: Option<String>,
629    /// Architecture family: "transformer", "cnn", "rnn", "llm", "gan", etc.
630    pub architecture_family: Option<String>,
631    /// Architecture name: "bert", "gpt", "resnet", etc.
632    pub architecture_name: Option<String>,
633    /// ML task: "nlp", "computer-vision", "audio", "tabular", etc.
634    pub task: Option<String>,
635    /// Quantization mode: "int4", "int8", "fp16", "bf16", "fp32", "mixed", etc.
636    pub quantization: Option<String>,
637    /// Limitations or known constraints of the model
638    pub limitations: Option<String>,
639    /// Training datasets used for this model
640    pub training_datasets: Vec<DatasetRef>,
641    /// Energy consumed during training in kWh (approximate)
642    pub energy_kwh_training: Option<f64>,
643    /// URL to detailed model card (from ExternalRefType::ModelCard)
644    pub model_card_url: Option<String>,
645    /// Fairness assessments (CycloneDX 1.5+ `considerations.fairnessAssessments`).
646    /// SPDX 3.0 has no direct analogue; the nearest AI-profile signals are
647    /// normalized into this shape so cross-format scoring is symmetric.
648    pub fairness: Vec<FairnessAssessment>,
649    /// Ethical considerations. CycloneDX emits structured objects
650    /// (`considerations.ethicalConsiderations[]`); SPDX emits free strings.
651    /// Both are normalized into this single shape.
652    pub ethical_considerations: Vec<EthicalConsideration>,
653    /// Intended use-cases (CycloneDX `considerations.useCases`, SPDX `ai_domain` /
654    /// `ai_informationAboutApplication`).
655    pub use_cases: Vec<String>,
656    /// Quantitative performance metrics (CycloneDX
657    /// `modelCard.quantitativeAnalysis.performanceMetrics`, SPDX `ai_metric`).
658    pub performance_metrics: Vec<MetricEntry>,
659    /// Data-preprocessing steps applied to the model's inputs (SPDX 3.0 AI
660    /// profile `ai_modelDataPreprocessing`). Surfaced for the BSI/G7
661    /// SBOM-for-AI "Models" cluster; CycloneDX has no direct analogue.
662    pub data_preprocessing: Vec<String>,
663}
664
665/// A fairness assessment for an ML model (CycloneDX 1.5+
666/// `considerations.fairnessAssessments[]`).
667#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
668#[non_exhaustive]
669pub struct FairnessAssessment {
670    /// The group(s) potentially at risk for the identified fairness concern.
671    pub group_at_risk: Option<String>,
672    /// Expected benefits to the group at risk.
673    pub benefits: Option<String>,
674    /// Potential harms to the group at risk.
675    pub harms: Option<String>,
676    /// Strategy used to mitigate the identified harms.
677    pub mitigation_strategy: Option<String>,
678}
679
680/// An ethical consideration for an ML model. Normalized from CycloneDX structured
681/// objects (`{ name, mitigationStrategy }`) and SPDX free-text strings alike.
682#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
683#[non_exhaustive]
684pub struct EthicalConsideration {
685    /// Name / description of the ethical risk.
686    pub name: Option<String>,
687    /// Strategy used to mitigate the ethical risk (CycloneDX only).
688    pub mitigation_strategy: Option<String>,
689}
690
691/// A single quantitative performance metric (CycloneDX
692/// `quantitativeAnalysis.performanceMetrics[]`).
693#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
694#[non_exhaustive]
695pub struct MetricEntry {
696    /// Metric type (e.g. "accuracy", "F1", "precision").
697    pub metric_type: Option<String>,
698    /// Metric value, retained verbatim (string form is spec-conformant and
699    /// preserves precision / non-numeric values such as confidence intervals).
700    pub value: Option<String>,
701    /// Data slice the metric was computed over (CycloneDX `slice`).
702    pub slice: Option<String>,
703}
704
705/// Reference to a dataset used for training or evaluation
706#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
707#[non_exhaustive]
708pub struct DatasetRef {
709    /// BOM-ref / BOM-Link to a dataset component (CycloneDX `modelParameters.datasets` `{ref}` form)
710    pub reference: Option<String>,
711    /// Dataset name (from an inline `componentData` dataset)
712    pub name: Option<String>,
713    /// Package URL (PURL). Not part of the CycloneDX spec for datasets; retained for
714    /// non-spec emitters and is `None` for spec-conformant input.
715    pub purl: Option<String>,
716}
717
718/// Dataset component metadata (CycloneDX 1.5+ data type)
719///
720/// Structured information about datasets, including type, sensitivity,
721/// governance, and content properties.
722#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
723#[non_exhaustive]
724pub struct DatasetInfo {
725    /// Dataset type: "training", "testing", "validation", "evaluation"
726    pub dataset_type: Option<String>,
727    /// Sensitivity classifications: "sensitive", "confidential", "pii", etc.
728    pub sensitivity_classifications: Vec<String>,
729    /// Data governance owners/custodians
730    pub governance_owners: Vec<String>,
731    /// Intended use of the dataset (SPDX 3.0 Dataset profile
732    /// `dataset_intendedUse`). Part of the BSI/G7 SBOM-for-AI "Datasets"
733    /// cluster provenance / intended-use element.
734    pub intended_use: Option<String>,
735    /// Confidentiality level of the dataset (SPDX 3.0 Dataset profile
736    /// `dataset_confidentialityLevel`). Distinct from the sensitivity
737    /// classifications above, which already fold this in for AI-Act scoring;
738    /// retained verbatim here for the BSI sensitivity-classification element.
739    pub confidentiality_level: Option<String>,
740    /// Data-preprocessing steps applied to the dataset (SPDX 3.0 Dataset
741    /// profile `dataset_dataPreprocessing`). Surfaced for the BSI/G7
742    /// SBOM-for-AI "Datasets" cluster provenance element.
743    pub preprocessing: Vec<String>,
744    /// Anonymization methods applied to the dataset (SPDX 3.0 Dataset profile
745    /// `dataset_anonymizationMethodUsed`). Surfaced for the BSI/G7
746    /// SBOM-for-AI "Datasets" cluster provenance element.
747    pub anonymization: Vec<String>,
748}