cyclonedx_bom/models/
component.rs

1/*
2 * This file is part of CycloneDX Rust Cargo.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 * SPDX-License-Identifier: Apache-2.0
17 */
18
19use once_cell::sync::Lazy;
20use ordered_float::OrderedFloat;
21use regex::Regex;
22use std::fmt::Formatter;
23
24use crate::external_models::normalized_string::validate_normalized_string;
25use crate::external_models::uri::{validate_purl, validate_uri as validate_url};
26use crate::models::attached_text::AttachedText;
27use crate::models::bom::BomReference;
28use crate::models::code::{Commits, Patches};
29use crate::models::external_reference::ExternalReferences;
30use crate::models::hash::Hashes;
31use crate::models::license::Licenses;
32use crate::models::organization::OrganizationalEntity;
33use crate::models::property::Properties;
34use crate::validation::ValidationError;
35use crate::{
36    external_models::{
37        normalized_string::NormalizedString,
38        uri::{Purl, Uri as Url},
39    },
40    validation::{Validate, ValidationContext, ValidationResult},
41};
42
43use super::bom::{validate_bom_ref, SpecVersion};
44use super::component_data::ComponentData;
45use super::modelcard::ModelCard;
46use super::signature::Signature;
47
48#[derive(Clone, Debug, PartialEq, Eq, Hash)]
49pub struct Component {
50    pub component_type: Classification,
51    pub mime_type: Option<MimeType>,
52    pub bom_ref: Option<String>,
53    pub supplier: Option<OrganizationalEntity>,
54    pub author: Option<NormalizedString>,
55    pub publisher: Option<NormalizedString>,
56    pub group: Option<NormalizedString>,
57    pub name: NormalizedString,
58    pub version: Option<NormalizedString>,
59    pub description: Option<NormalizedString>,
60    pub scope: Option<Scope>,
61    pub hashes: Option<Hashes>,
62    pub licenses: Option<Licenses>,
63    pub copyright: Option<NormalizedString>,
64    pub cpe: Option<Cpe>,
65    pub purl: Option<Purl>,
66    pub swid: Option<Swid>,
67    pub modified: Option<bool>,
68    pub pedigree: Option<Pedigree>,
69    pub external_references: Option<ExternalReferences>,
70    pub properties: Option<Properties>,
71    pub components: Option<Components>,
72    pub evidence: Option<ComponentEvidence>,
73    /// Added in version 1.4
74    pub signature: Option<Signature>,
75    /// Added in version 1.5
76    pub model_card: Option<ModelCard>,
77    /// Added in version 1.5
78    pub data: Option<ComponentData>,
79}
80
81impl Component {
82    pub fn new(
83        component_type: Classification,
84        name: &str,
85        version: &str,
86        bom_ref: Option<String>,
87    ) -> Self {
88        Self {
89            component_type,
90            name: NormalizedString::new(name),
91            version: Some(NormalizedString::new(version)),
92            bom_ref,
93            mime_type: None,
94            supplier: None,
95            author: None,
96            publisher: None,
97            group: None,
98            description: None,
99            scope: None,
100            hashes: None,
101            licenses: None,
102            copyright: None,
103            cpe: None,
104            purl: None,
105            swid: None,
106            modified: None,
107            pedigree: None,
108            external_references: None,
109            properties: None,
110            components: None,
111            evidence: None,
112            signature: None,
113            model_card: None,
114            data: None,
115        }
116    }
117}
118
119impl Validate for Component {
120    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
121        let mut ctx = ValidationContext::new();
122        ctx.add_field("component_type", &self.component_type, |ct| {
123            validate_classification(ct, version)
124        });
125        ctx.add_field_option("mime_type", self.mime_type.as_ref(), validate_mime_type);
126        ctx.add_struct_option("supplier", self.supplier.as_ref(), version);
127        ctx.add_field_option("author", self.author.as_ref(), validate_normalized_string);
128        ctx.add_field_option(
129            "publisher",
130            self.publisher.as_ref(),
131            validate_normalized_string,
132        );
133        ctx.add_field_option("group", self.group.as_ref(), validate_normalized_string);
134        ctx.add_field("name", &self.name, validate_normalized_string);
135        ctx.add_field_option("version", self.version.as_ref(), validate_normalized_string);
136        ctx.add_field_option(
137            "description",
138            self.description.as_ref(),
139            validate_normalized_string,
140        );
141        ctx.add_enum_option("scope", self.scope.as_ref(), validate_scope);
142        ctx.add_struct_option("hashes", self.hashes.as_ref(), version);
143        ctx.add_struct_option("licenses", self.licenses.as_ref(), version);
144        ctx.add_field_option(
145            "copyright",
146            self.copyright.as_ref(),
147            validate_normalized_string,
148        );
149        ctx.add_field_option("cpe", self.cpe.as_ref(), validate_cpe);
150        ctx.add_field_option("purl", self.purl.as_ref(), validate_purl);
151        ctx.add_struct_option("swid", self.swid.as_ref(), version);
152        ctx.add_struct_option("pedigree", self.pedigree.as_ref(), version);
153        ctx.add_struct_option(
154            "external_references",
155            self.external_references.as_ref(),
156            version,
157        );
158        ctx.add_struct_option("properties", self.properties.as_ref(), version);
159        ctx.add_struct_option("components", self.components.as_ref(), version);
160        ctx.add_struct_option("evidence", self.evidence.as_ref(), version);
161        ctx.into()
162    }
163}
164
165#[derive(Clone, Debug, PartialEq, Eq, Hash)]
166pub struct Components(pub Vec<Component>);
167
168impl Validate for Components {
169    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
170        ValidationContext::new()
171            .add_list("inner", &self.0, |component| {
172                component.validate_version(version)
173            })
174            .into()
175    }
176}
177
178/// Checks the given [`Classification`] is valid.
179pub fn validate_classification(
180    classification: &Classification,
181    version: SpecVersion,
182) -> Result<(), ValidationError> {
183    if SpecVersion::V1_3 <= version && version <= SpecVersion::V1_4 {
184        if Classification::File < *classification {
185            return Err(ValidationError::new("Unknown classification"));
186        }
187    } else if SpecVersion::V1_5 <= version
188        && matches!(classification, Classification::UnknownClassification(_))
189    {
190        return Err(ValidationError::new("Unknown classification"));
191    }
192    Ok(())
193}
194
195#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, strum::Display, Hash)]
196#[strum(serialize_all = "kebab-case")]
197#[repr(u16)]
198pub enum Classification {
199    Application = 1,
200    Framework = 2,
201    Library = 3,
202    Container = 4,
203    OperatingSystem = 5,
204    Device = 6,
205    Firmware = 7,
206    File = 8,
207    /// Added in 1.5
208    Platform = 9,
209    /// Added in 1.5
210    DeviceDriver = 10,
211    /// Added in 1.5
212    MachineLearningModel = 11,
213    /// Added in 1.5
214    Data = 12,
215    #[doc(hidden)]
216    #[strum(default)]
217    UnknownClassification(String),
218}
219
220impl Classification {
221    pub fn new_unchecked<A: AsRef<str>>(value: A) -> Self {
222        match value.as_ref() {
223            "application" => Self::Application,
224            "framework" => Self::Framework,
225            "library" => Self::Library,
226            "container" => Self::Container,
227            "operating-system" => Self::OperatingSystem,
228            "device" => Self::Device,
229            "firmware" => Self::Firmware,
230            "file" => Self::File,
231            "platform" => Self::Platform,
232            "device-driver" => Self::DeviceDriver,
233            "machine-learning-model" => Self::MachineLearningModel,
234            "data" => Self::Data,
235            unknown => Self::UnknownClassification(unknown.to_string()),
236        }
237    }
238}
239
240pub fn validate_scope(scope: &Scope) -> Result<(), ValidationError> {
241    if matches!(scope, Scope::UnknownScope(_)) {
242        return Err(ValidationError::new("Unknown scope"));
243    }
244    Ok(())
245}
246
247#[derive(Clone, Debug, PartialEq, Eq, strum::Display, Hash)]
248#[strum(serialize_all = "kebab-case")]
249pub enum Scope {
250    Required,
251    Optional,
252    Excluded,
253    #[doc(hidden)]
254    #[strum(default)]
255    UnknownScope(String),
256}
257
258impl Scope {
259    pub fn new_unchecked<A: AsRef<str>>(value: A) -> Self {
260        match value.as_ref() {
261            "required" => Self::Required,
262            "optional" => Self::Optional,
263            "excluded" => Self::Excluded,
264            unknown => Self::UnknownScope(unknown.to_string()),
265        }
266    }
267}
268
269/// Checks if given [`MimeType`] is valid / supported.
270pub fn validate_mime_type(mime_type: &MimeType) -> Result<(), ValidationError> {
271    static UUID_REGEX: Lazy<Regex> =
272        Lazy::new(|| Regex::new(r"^[-+a-z0-9.]+/[-+a-z0-9.]+$").expect("Failed to compile regex."));
273
274    if !UUID_REGEX.is_match(&mime_type.0) {
275        return Err(ValidationError::new(
276            "MimeType does not match regular expression",
277        ));
278    }
279
280    Ok(())
281}
282
283#[derive(Clone, Debug, PartialEq, Eq, Hash)]
284pub struct MimeType(pub String);
285
286#[derive(Clone, Debug, PartialEq, Eq, Hash)]
287pub struct Swid {
288    pub tag_id: String,
289    pub name: String,
290    pub version: Option<String>,
291    pub tag_version: Option<u32>,
292    pub patch: Option<bool>,
293    pub text: Option<AttachedText>,
294    pub url: Option<Url>,
295}
296
297impl Validate for Swid {
298    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
299        ValidationContext::new()
300            .add_struct_option("text", self.text.as_ref(), version)
301            .add_field_option("url", self.url.as_ref(), validate_url)
302            .into()
303    }
304}
305
306pub fn validate_cpe(cpe: &Cpe) -> Result<(), ValidationError> {
307    static UUID_REGEX: Lazy<Regex> = Lazy::new(|| {
308        Regex::new(
309            r##"([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6})|(cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&'\(\)\+,/:;<=>@\[\]\^`\{\|}~]))+(\?*|\*?))|[\*\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\*\-]))(:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&'\(\)\+,/:;<=>@\[\]\^`\{\|}~]))+(\?*|\*?))|[\*\-])){4})"##,
310        ).expect("Failed to compile regex.")
311    });
312
313    if !UUID_REGEX.is_match(&cpe.0) {
314        return Err(ValidationError::new(
315            "Cpe does not match regular expression",
316        ));
317    }
318
319    Ok(())
320}
321
322#[derive(Clone, Debug, PartialEq, Eq, Hash)]
323pub struct Cpe(pub(crate) String);
324
325impl Cpe {
326    pub fn new(inner: &str) -> Self {
327        Self(inner.to_string())
328    }
329}
330
331impl From<String> for Cpe {
332    fn from(value: String) -> Self {
333        Self(value)
334    }
335}
336
337impl AsRef<String> for Cpe {
338    fn as_ref(&self) -> &String {
339        &self.0
340    }
341}
342
343impl AsRef<str> for Cpe {
344    fn as_ref(&self) -> &str {
345        &self.0
346    }
347}
348
349impl From<Cpe> for String {
350    fn from(value: Cpe) -> Self {
351        value.0
352    }
353}
354
355impl std::fmt::Display for Cpe {
356    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
357        f.write_str(&self.0)
358    }
359}
360
361#[derive(Clone, Debug, PartialEq, Eq, Hash)]
362pub struct ComponentEvidence {
363    pub licenses: Option<Licenses>,
364    pub copyright: Option<CopyrightTexts>,
365    /// Added in version 1.5
366    pub occurrences: Option<Occurrences>,
367    /// Added in version 1.5
368    pub callstack: Option<Callstack>,
369    /// Added in version 1.5
370    pub identity: Option<Identity>,
371}
372
373impl Validate for ComponentEvidence {
374    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
375        ValidationContext::new()
376            .add_struct_option("licenses", self.licenses.as_ref(), version)
377            .add_struct_option("copyright", self.copyright.as_ref(), version)
378            .add_struct_option("occurrences", self.occurrences.as_ref(), version)
379            .add_struct_option("callstack", self.callstack.as_ref(), version)
380            .add_struct_option("identity", self.identity.as_ref(), version)
381            .into()
382    }
383}
384
385/// For more details see
386/// https://cyclonedx.org/docs/1.5/json/#components_items_evidence_occurrences
387/// Added in version 1.5
388#[derive(Clone, Debug, PartialEq, Eq, Hash)]
389pub struct Occurrences(pub Vec<Occurrence>);
390
391impl Validate for Occurrences {
392    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
393        ValidationContext::new()
394            .add_list("inner", &self.0, |occurrence| {
395                occurrence.validate_version(version)
396            })
397            .into()
398    }
399}
400
401#[derive(Clone, Debug, PartialEq, Eq, Hash)]
402pub struct Occurrence {
403    pub bom_ref: Option<BomReference>,
404    pub location: String,
405}
406
407impl Occurrence {
408    pub fn new(location: &str) -> Self {
409        Self {
410            bom_ref: None,
411            location: location.to_string(),
412        }
413    }
414}
415
416impl Validate for Occurrence {
417    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
418        ValidationContext::new()
419            .add_field_option("bom-ref", self.bom_ref.as_ref(), |bom_ref| {
420                validate_bom_ref(bom_ref, version)
421            })
422            .into()
423    }
424}
425
426#[derive(Clone, Debug, PartialEq, Eq, Hash)]
427pub struct Callstack {
428    pub frames: Frames,
429}
430
431impl Callstack {
432    pub fn new(frames: Frames) -> Self {
433        Self { frames }
434    }
435}
436
437impl Validate for Callstack {
438    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
439        self.frames.validate_version(version)
440    }
441}
442
443#[derive(Clone, Debug, PartialEq, Eq, Hash)]
444pub struct Frames(pub Vec<Frame>);
445
446impl Validate for Frames {
447    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
448        ValidationContext::new()
449            .add_list("frames", &self.0, |frame| frame.validate_version(version))
450            .into()
451    }
452}
453
454/// For more information see
455/// https://cyclonedx.org/docs/1.5/json/#components_items_evidence_callstack
456/// Added in version 1.5
457#[derive(Clone, Debug, PartialEq, Eq, Hash)]
458pub struct Frame {
459    pub package: Option<NormalizedString>,
460    pub module: NormalizedString,
461    pub function: Option<NormalizedString>,
462    pub parameters: Option<Vec<NormalizedString>>,
463    pub line: Option<u32>,
464    pub column: Option<u32>,
465    pub full_filename: Option<NormalizedString>,
466}
467
468impl Validate for Frame {
469    fn validate_version(&self, _version: SpecVersion) -> ValidationResult {
470        ValidationContext::new()
471            .add_field_option("package", self.package.as_ref(), validate_normalized_string)
472            .add_field("module", self.module.as_ref(), validate_normalized_string)
473            .add_field_option(
474                "function",
475                self.function.as_ref(),
476                validate_normalized_string,
477            )
478            .add_list_option(
479                "parameters",
480                self.parameters.as_ref(),
481                validate_normalized_string,
482            )
483            .add_field_option(
484                "full_filename",
485                self.full_filename.as_ref(),
486                validate_normalized_string,
487            )
488            .into()
489    }
490}
491
492pub fn validate_confidence(confidence: &ConfidenceScore) -> Result<(), ValidationError> {
493    if confidence.get() < 0.0 && 1.0 > confidence.get() {
494        return Err("Confidence score outside range 0.0 - 1.0".into());
495    }
496    Ok(())
497}
498
499#[derive(Clone, Debug, PartialEq, Eq, Hash)]
500pub struct ConfidenceScore(pub OrderedFloat<f32>);
501
502impl ConfidenceScore {
503    pub fn new(value: f32) -> Self {
504        Self(OrderedFloat(value))
505    }
506
507    pub fn get(&self) -> f32 {
508        self.0 .0
509    }
510}
511
512pub fn validate_identity_field(field: &IdentityField) -> Result<(), ValidationError> {
513    if let IdentityField::Unknown(unknown) = field {
514        return Err(format!("Unknown identity found '{}' given", unknown).into());
515    }
516    Ok(())
517}
518
519#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, strum::Display, Hash)]
520#[strum(serialize_all = "kebab-case")]
521#[repr(u16)]
522pub enum IdentityField {
523    Group,
524    Name,
525    Version,
526    Purl,
527    Cpe,
528    Swid,
529    Hash,
530    Unknown(String),
531}
532
533impl IdentityField {
534    pub fn new_unchecked<A: AsRef<str>>(value: A) -> Self {
535        match value.as_ref() {
536            "group" => Self::Group,
537            "name" => Self::Name,
538            "version" => Self::Version,
539            "purl" => Self::Purl,
540            "cpe" => Self::Cpe,
541            "swid" => Self::Swid,
542            "hash" => Self::Hash,
543            unknown => Self::Unknown(unknown.to_string()),
544        }
545    }
546}
547
548/// For more information see
549/// https://cyclonedx.org/docs/1.5/json/#components_items_evidence_identity
550/// Added in version 1.5
551#[derive(Clone, Debug, PartialEq, Eq, Hash)]
552pub struct Identity {
553    pub field: IdentityField,
554    /// Level between 0.0-1.0 (where 1.0 is highest confidence)
555    pub confidence: Option<ConfidenceScore>,
556    pub methods: Option<Methods>,
557    pub tools: Option<ToolsReferences>,
558}
559
560impl Validate for Identity {
561    fn validate_version(&self, _version: SpecVersion) -> ValidationResult {
562        ValidationContext::new()
563            .add_field("field", &self.field, validate_identity_field)
564            .add_field_option("confidence", self.confidence.as_ref(), validate_confidence)
565            .into()
566    }
567}
568
569/// For more information see
570/// https://cyclonedx.org/docs/1.5/json/#components_items_evidence_identity_methods
571#[derive(Clone, Debug, PartialEq, Eq, Hash)]
572pub struct Methods(pub Vec<Method>);
573
574#[derive(Clone, Debug, PartialEq, Eq, Hash)]
575pub struct Method {
576    pub technique: String,
577    pub confidence: ConfidenceScore,
578    pub value: Option<String>,
579}
580
581#[derive(Clone, Debug, PartialEq, Eq, Hash)]
582pub struct ToolsReferences(pub Vec<String>);
583
584#[derive(Clone, Debug, PartialEq, Eq, Hash)]
585pub struct Pedigree {
586    pub ancestors: Option<Components>,
587    pub descendants: Option<Components>,
588    pub variants: Option<Components>,
589    pub commits: Option<Commits>,
590    pub patches: Option<Patches>,
591    pub notes: Option<String>,
592}
593
594impl Validate for Pedigree {
595    fn validate_version(&self, version: SpecVersion) -> ValidationResult {
596        let mut context = ValidationContext::new();
597        context.add_struct_option("ancestors", self.ancestors.as_ref(), version);
598        context.add_struct_option("descendants", self.descendants.as_ref(), version);
599        context.add_struct_option("variants", self.variants.as_ref(), version);
600        context.add_struct_option("commits", self.commits.as_ref(), version);
601        context.add_struct_option("patches", self.patches.as_ref(), version);
602        context.into()
603    }
604}
605
606pub fn validate_copyright(_copyright: &Copyright) -> Result<(), ValidationError> {
607    Ok(())
608}
609
610#[derive(Clone, Debug, PartialEq, Eq, Hash)]
611pub struct Copyright(pub String);
612
613#[derive(Clone, Debug, PartialEq, Eq, Hash)]
614pub struct CopyrightTexts(pub Vec<Copyright>);
615
616impl Validate for CopyrightTexts {
617    fn validate_version(&self, _version: SpecVersion) -> ValidationResult {
618        ValidationContext::new()
619            .add_list("inner", &self.0, validate_copyright)
620            .into()
621    }
622}
623
624#[cfg(test)]
625mod test {
626    use pretty_assertions::assert_eq;
627
628    use crate::{
629        external_models::spdx::SpdxExpression,
630        models::{
631            attachment::Attachment,
632            bom::BomReference,
633            code::{Commit, Patch, PatchClassification},
634            component_data::{
635                ComponentData, ComponentDataType, DataContents, Graphic, GraphicsCollection,
636            },
637            data_governance::{DataGovernance, DataGovernanceResponsibleParty},
638            external_reference::{ExternalReference, ExternalReferenceType, Uri},
639            hash::{Hash, HashAlgorithm, HashValue},
640            license::LicenseChoice,
641            modelcard::{
642                ApproachType, ConfidenceInterval, Considerations, Dataset, Datasets, Inputs,
643                MLParameter, ModelParameters, ModelParametersApproach, Outputs, PerformanceMetric,
644                PerformanceMetrics, QuantitativeAnalysis,
645            },
646            organization::OrganizationalContact,
647            property::Property,
648            signature::Algorithm,
649        },
650        validation,
651    };
652
653    use super::*;
654
655    #[test]
656    fn valid_components_should_pass_validation() {
657        let vec = vec![Component {
658            component_type: Classification::Application,
659            mime_type: Some(MimeType("text/text".to_string())),
660            bom_ref: Some("bom ref".to_string()),
661            supplier: Some(OrganizationalEntity {
662                bom_ref: Some(BomReference::new("Supplier 1")),
663                name: Some(NormalizedString::new("name")),
664                url: None,
665                contact: None,
666            }),
667            author: Some(NormalizedString::new("author")),
668            publisher: Some(NormalizedString::new("publisher")),
669            group: Some(NormalizedString::new("group")),
670            name: NormalizedString::new("name"),
671            version: Some(NormalizedString::new("version")),
672            description: Some(NormalizedString::new("description")),
673            scope: Some(Scope::Required),
674            hashes: Some(Hashes(vec![Hash {
675                alg: HashAlgorithm::MD5,
676                content: HashValue("a3bf1f3d584747e2569483783ddee45b".to_string()),
677            }])),
678            licenses: Some(Licenses(vec![LicenseChoice::Expression(
679                SpdxExpression::new("MIT"),
680            )])),
681            copyright: Some(NormalizedString::new("copyright")),
682            cpe: Some(Cpe("cpe:/a:example:mylibrary:1.0.0".to_string())),
683            purl: Some(Purl("pkg:cargo/cyclonedx-bom@0.3.1".to_string())),
684            swid: Some(Swid {
685                tag_id: "tag ID".to_string(),
686                name: "name".to_string(),
687                version: Some("version".to_string()),
688                tag_version: Some(1),
689                patch: Some(true),
690                text: Some(AttachedText {
691                    content_type: None,
692                    encoding: None,
693                    content: "content".to_string(),
694                }),
695                url: Some(Url("https://example.com".to_string())),
696            }),
697            modified: Some(true),
698            pedigree: Some(Pedigree {
699                ancestors: Some(Components(vec![])),
700                descendants: Some(Components(vec![])),
701                variants: Some(Components(vec![])),
702                commits: Some(Commits(vec![Commit {
703                    uid: Some(NormalizedString::new("uid")),
704                    url: None,
705                    author: None,
706                    committer: None,
707                    message: None,
708                }])),
709                patches: Some(Patches(vec![Patch {
710                    patch_type: PatchClassification::Backport,
711                    diff: None,
712                    resolves: None,
713                }])),
714                notes: Some("notes".to_string()),
715            }),
716            external_references: Some(ExternalReferences(vec![ExternalReference {
717                external_reference_type: ExternalReferenceType::Bom,
718                url: Uri::Url(Url("https://www.example.com".to_string())),
719                comment: None,
720                hashes: None,
721            }])),
722            properties: Some(Properties(vec![Property {
723                name: "name".to_string(),
724                value: NormalizedString::new("value"),
725            }])),
726            components: Some(Components(vec![])),
727            evidence: Some(ComponentEvidence {
728                licenses: Some(Licenses(vec![LicenseChoice::Expression(
729                    SpdxExpression::new("MIT"),
730                )])),
731                copyright: Some(CopyrightTexts(vec![Copyright("copyright".to_string())])),
732                occurrences: Some(Occurrences(vec![Occurrence {
733                    bom_ref: None,
734                    location: "location".to_string(),
735                }])),
736                callstack: Some(Callstack::new(Frames(vec![Frame {
737                    package: Some("package".into()),
738                    module: "module".into(),
739                    function: Some("function".into()),
740                    parameters: None,
741                    line: Some(10),
742                    column: Some(20),
743                    full_filename: Some("full_filename".into()),
744                }]))),
745                identity: Some(Identity {
746                    field: IdentityField::Group,
747                    confidence: Some(ConfidenceScore::new(0.8)),
748                    methods: Some(Methods(vec![Method {
749                        technique: "technique".to_string(),
750                        confidence: ConfidenceScore::new(0.5),
751                        value: Some("help".to_string()),
752                    }])),
753                    tools: None,
754                }),
755            }),
756            signature: Some(Signature::single(Algorithm::HS512, "abcdefgh")),
757            model_card: Some(ModelCard {
758                bom_ref: None,
759                model_parameters: Some(ModelParameters {
760                    approach: Some(ModelParametersApproach {
761                        approach_type: Some(ApproachType::Supervised),
762                    }),
763                    task: Some("task".to_string()),
764                    architecture_family: Some("architecture family".to_string()),
765                    model_architecture: Some("model architecture".to_string()),
766                    datasets: Some(Datasets(vec![Dataset::Component(ComponentData {
767                        bom_ref: None,
768                        data_type: ComponentDataType::SourceCode,
769                        name: Some("dataset".to_string()),
770                        contents: Some(DataContents {
771                            attachment: Some(Attachment {
772                                content: "data content".to_string(),
773                                content_type: Some("text/plain".to_string()),
774                                encoding: Some("base64".to_string()),
775                            }),
776                            url: Some(Url("https://example.com".to_string())),
777                            properties: Some(Properties(vec![])),
778                        }),
779                        classification: Some("data classification".to_string()),
780                        sensitive_data: Some("sensitive".to_string()),
781                        graphics: Some(GraphicsCollection {
782                            description: Some("All graphics".to_string()),
783                            collection: Some(vec![Graphic {
784                                name: Some("graphic-1".to_string()),
785                                image: Some(Attachment {
786                                    content_type: Some("image/jpeg".to_string()),
787                                    encoding: Some("base64".to_string()),
788                                    content: "imagebytes".to_string(),
789                                }),
790                            }]),
791                        }),
792                        description: Some("Component data description".to_string()),
793                        governance: Some(DataGovernance {
794                            custodians: Some(vec![DataGovernanceResponsibleParty::Contact(
795                                OrganizationalContact {
796                                    bom_ref: Some(BomReference::new("custodian-1")),
797                                    name: Some("custodian".into()),
798                                    email: None,
799                                    phone: None,
800                                },
801                            )]),
802                            stewards: None,
803                            owners: None,
804                        }),
805                    })])),
806                    inputs: Some(Inputs(vec![MLParameter::new("string")])),
807                    outputs: Some(Outputs(vec![MLParameter::new("image")])),
808                }),
809                quantitative_analysis: Some(QuantitativeAnalysis {
810                    performance_metrics: Some(PerformanceMetrics(vec![PerformanceMetric {
811                        metric_type: Some("performance".to_string()),
812                        value: Some("metric value".to_string()),
813                        slice: None,
814                        confidence_interval: Some(ConfidenceInterval {
815                            lower_bound: Some("low".to_string()),
816                            upper_bound: Some("high".to_string()),
817                        }),
818                    }])),
819                    graphics: Some(GraphicsCollection {
820                        description: Some("graphics".to_string()),
821                        collection: None,
822                    }),
823                }),
824                considerations: Some(Considerations {}),
825                properties: Some(Properties(vec![Property {
826                    name: "property".to_string(),
827                    value: NormalizedString("value".to_string()),
828                }])),
829            }),
830            data: Some(ComponentData {
831                bom_ref: None,
832                data_type: ComponentDataType::SourceCode,
833                name: Some("github".into()),
834                contents: Some(DataContents {
835                    attachment: Some(Attachment {
836                        content: "some pic".into(),
837                        content_type: None,
838                        encoding: Some("base64".into()),
839                    }),
840                    url: None,
841                    properties: None,
842                }),
843                classification: None,
844                sensitive_data: None,
845                graphics: None,
846                description: None,
847                governance: None,
848            }),
849        }];
850        let validation_result = Components(vec).validate();
851
852        assert!(validation_result.passed());
853    }
854
855    #[test]
856    fn invalid_components_should_fail_validation() {
857        let validation_result = Components(vec![Component {
858            component_type: Classification::UnknownClassification("unknown".to_string()),
859            mime_type: Some(MimeType("invalid mime type".to_string())),
860            bom_ref: Some("bom ref".to_string()),
861            supplier: Some(OrganizationalEntity {
862                bom_ref: Some(BomReference::new("Supplier 1")),
863                name: Some(NormalizedString("invalid\tname".to_string())),
864                url: None,
865                contact: None,
866            }),
867            author: Some(NormalizedString("invalid\tauthor".to_string())),
868            publisher: Some(NormalizedString("invalid\tpublisher".to_string())),
869            group: Some(NormalizedString("invalid\tgroup".to_string())),
870            name: NormalizedString("invalid\tname".to_string()),
871            version: Some(NormalizedString("invalid\tversion".to_string())),
872            description: Some(NormalizedString("invalid\tdescription".to_string())),
873            scope: Some(Scope::UnknownScope("unknown".to_string())),
874            hashes: Some(Hashes(vec![Hash {
875                alg: HashAlgorithm::MD5,
876                content: HashValue("invalid hash content".to_string()),
877            }])),
878            licenses: Some(Licenses(vec![LicenseChoice::Expression(
879                SpdxExpression::new("invalid license"),
880            )])),
881            copyright: Some(NormalizedString("invalid\tcopyright".to_string())),
882            cpe: Some(Cpe("invalid cpe".to_string())),
883            purl: Some(Purl("invalid purl".to_string())),
884            swid: Some(Swid {
885                tag_id: "tag ID".to_string(),
886                name: "name".to_string(),
887                version: Some("version".to_string()),
888                tag_version: Some(1),
889                patch: Some(true),
890                text: Some(AttachedText {
891                    content_type: Some(NormalizedString("invalid\tcontent_type".to_string())),
892                    encoding: None,
893                    content: "content".to_string(),
894                }),
895                url: Some(Url("invalid url".to_string())),
896            }),
897            modified: Some(true),
898            pedigree: Some(Pedigree {
899                ancestors: Some(Components(vec![invalid_component()])),
900                descendants: Some(Components(vec![invalid_component()])),
901                variants: Some(Components(vec![invalid_component()])),
902                commits: Some(Commits(vec![Commit {
903                    uid: Some(NormalizedString("invalid\tuid".to_string())),
904                    url: None,
905                    author: None,
906                    committer: None,
907                    message: None,
908                }])),
909                patches: Some(Patches(vec![Patch {
910                    patch_type: PatchClassification::UnknownPatchClassification(
911                        "unknown".to_string(),
912                    ),
913                    diff: None,
914                    resolves: None,
915                }])),
916                notes: Some("notes".to_string()),
917            }),
918            external_references: Some(ExternalReferences(vec![ExternalReference {
919                external_reference_type: ExternalReferenceType::UnknownExternalReferenceType(
920                    "unknown".to_string(),
921                ),
922                url: Uri::Url(Url("https://www.example.com".to_string())),
923                comment: None,
924                hashes: None,
925            }])),
926            properties: Some(Properties(vec![Property {
927                name: "name".to_string(),
928                value: NormalizedString("invalid\tvalue".to_string()),
929            }])),
930            components: Some(Components(vec![invalid_component()])),
931            evidence: Some(ComponentEvidence {
932                licenses: Some(Licenses(vec![LicenseChoice::Expression(
933                    SpdxExpression::new("invalid license"),
934                )])),
935                copyright: Some(CopyrightTexts(vec![Copyright("copyright".to_string())])),
936                occurrences: None,
937                callstack: None,
938                identity: None,
939            }),
940            signature: Some(Signature::single(Algorithm::HS512, "abcdefgh")),
941            model_card: None,
942            data: None,
943        }])
944        .validate();
945
946        assert_eq!(
947            validation_result,
948            validation::list(
949                "inner",
950                [(
951                    0,
952                    vec![
953                        validation::field("component_type", "Unknown classification"),
954                        validation::field(
955                            "mime_type",
956                            "MimeType does not match regular expression"
957                        ),
958                        validation::r#struct(
959                            "supplier",
960                            validation::field(
961                                "name",
962                                "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
963                            )
964                        ),
965                        validation::field(
966                            "author",
967                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
968                        ),
969                        validation::field(
970                            "publisher",
971                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
972                        ),
973                        validation::field(
974                            "group",
975                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
976                        ),
977                        validation::field(
978                            "name",
979                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
980                        ),
981                        validation::field(
982                            "version",
983                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
984                        ),
985                        validation::field(
986                            "description",
987                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
988                        ),
989                        validation::r#enum(
990                            "scope",
991                            "Unknown scope"
992                        ),
993                        validation::r#struct(
994                            "hashes",
995                            validation::list(
996                                "inner",
997                                [(
998                                    0,
999                                    validation::field(
1000                                        "content",
1001                                        "HashValue does not match regular expression"
1002                                    )
1003                                )]
1004                            )
1005                        ),
1006                        validation::r#struct(
1007                            "licenses",
1008                            validation::list(
1009                                "inner",
1010                                [(
1011                                    0,
1012                                    validation::r#enum(
1013                                        "expression",
1014                                        "SPDX expression is not valid"
1015                                    )
1016                                )]
1017                            )
1018                        ),
1019                        validation::field(
1020                            "copyright",
1021                            "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
1022                        ),
1023                        validation::field(
1024                            "cpe",
1025                            "Cpe does not match regular expression"
1026                        ),
1027                        validation::field(
1028                            "purl",
1029                            "Purl does not conform to Package URL spec: URL scheme must be pkg"
1030                        ),
1031                        validation::r#struct(
1032                            "swid",
1033                            vec![
1034                                validation::r#struct(
1035                                    "text",
1036                                    validation::field(
1037                                        "content_type",
1038                                        "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
1039                                    )
1040                                ),
1041                                validation::field(
1042                                    "url",
1043                                    "Uri does not conform to RFC 3986"
1044                                )
1045                            ]
1046                        ),
1047                        validation::r#struct(
1048                            "pedigree",
1049                            vec![
1050                                validation::r#struct(
1051                                    "ancestors",
1052                                    validation::list(
1053                                        "inner",
1054                                        [(
1055                                            0,
1056                                            validation::field("component_type", "Unknown classification")
1057                                        )]
1058                                    )
1059                                ),
1060                                validation::r#struct(
1061                                    "descendants",
1062                                    validation::list(
1063                                        "inner",
1064                                        [(
1065                                            0,
1066                                            validation::field("component_type", "Unknown classification")
1067                                        )]
1068                                    )
1069                                ),
1070                                validation::r#struct(
1071                                    "variants",
1072                                    validation::list(
1073                                        "inner",
1074                                        [(
1075                                            0,
1076                                            validation::field("component_type", "Unknown classification")
1077                                        )]
1078                                    )
1079                                ),
1080                                validation::r#struct(
1081                                    "commits",
1082                                    validation::list(
1083                                        "inner",
1084                                        [(
1085                                            0,
1086                                            validation::field(
1087                                                "uid",
1088                                                "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
1089                                            )
1090                                        )]
1091                                    )
1092                                ),
1093                                validation::r#struct(
1094                                    "patches",
1095                                    validation::list(
1096                                        "inner",
1097                                        [(
1098                                            0,
1099                                            validation::r#enum("patch_type", "Unknown patch classification")
1100                                        )]
1101                                    )
1102                                )
1103                            ]
1104                        ),
1105                        validation::r#struct(
1106                            "external_references",
1107                            validation::list(
1108                                "inner",
1109                                [(
1110                                    0,
1111                                    validation::field(
1112                                        "external_reference_type",
1113                                        "Unknown external reference type"
1114                                    )
1115                                )]
1116                            )
1117                        ),
1118                        validation::r#struct(
1119                            "properties",
1120                            validation::list(
1121                                "inner",
1122                                [(
1123                                    0,
1124                                    validation::field(
1125                                        "value",
1126                                        "NormalizedString contains invalid characters \\r \\n \\t or \\r\\n"
1127                                    )
1128                                )]
1129                            )
1130                        ),
1131                        validation::r#struct(
1132                            "components",
1133                            validation::list(
1134                                "inner",
1135                                [(
1136                                    0,
1137                                    validation::field("component_type", "Unknown classification")
1138                                )]
1139                            )
1140                        ),
1141                        validation::r#struct(
1142                            "evidence",
1143                            validation::r#struct(
1144                                "licenses",
1145                                validation::list(
1146                                    "inner",
1147                                    [(
1148                                        0,
1149                                        validation::r#enum("expression", "SPDX expression is not valid")
1150                                    )]
1151                                )
1152                            )
1153                        )
1154                    ]
1155                )]
1156            )
1157        );
1158    }
1159
1160    fn invalid_component() -> Component {
1161        Component {
1162            component_type: Classification::UnknownClassification("unknown".to_string()),
1163            mime_type: None,
1164            bom_ref: None,
1165            supplier: None,
1166            author: None,
1167            publisher: None,
1168            group: None,
1169            name: NormalizedString::new("name"),
1170            version: Some(NormalizedString::new("version")),
1171            description: None,
1172            scope: None,
1173            hashes: None,
1174            licenses: None,
1175            copyright: None,
1176            cpe: None,
1177            purl: None,
1178            swid: None,
1179            modified: None,
1180            pedigree: None,
1181            external_references: None,
1182            properties: None,
1183            components: None,
1184            evidence: None,
1185            signature: None,
1186            model_card: None,
1187            data: None,
1188        }
1189    }
1190
1191    #[test]
1192    fn test_validate_classification() {
1193        assert!(validate_classification(&Classification::Library, SpecVersion::V1_4).is_ok());
1194        assert!(validate_classification(&Classification::Library, SpecVersion::V1_5).is_ok());
1195        assert!(validate_classification(&Classification::Platform, SpecVersion::V1_5).is_ok());
1196
1197        assert!(validate_classification(&Classification::Platform, SpecVersion::V1_4).is_err());
1198        assert!(validate_classification(
1199            &Classification::UnknownClassification("test".to_string()),
1200            SpecVersion::V1_4
1201        )
1202        .is_err());
1203        assert!(validate_classification(
1204            &Classification::UnknownClassification("foo".to_string()),
1205            SpecVersion::V1_5
1206        )
1207        .is_err());
1208    }
1209}