1use 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 pub signature: Option<Signature>,
75 pub model_card: Option<ModelCard>,
77 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
178pub 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 Platform = 9,
209 DeviceDriver = 10,
211 MachineLearningModel = 11,
213 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
269pub 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 pub occurrences: Option<Occurrences>,
367 pub callstack: Option<Callstack>,
369 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#[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#[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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
552pub struct Identity {
553 pub field: IdentityField,
554 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#[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}