diff --git a/.gitmodules b/.gitmodules index 5d4cd14..583660a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,7 @@ path = csaf url = https://github.com/oasis-tcs/csaf branch = master +[submodule "ssvc"] + path = ssvc + url = https://github.com/CERTCC/SSVC.git + branch = main diff --git a/csaf b/csaf index 65f402d..62fb87c 160000 --- a/csaf +++ b/csaf @@ -1 +1 @@ -Subproject commit 65f402d7d5298b1957ce61f1e175c755069214ae +Subproject commit 62fb87c6b6d0754bf0a4df20fc9e3d387497d0fe diff --git a/csaf-lib/build.rs b/csaf-lib/build.rs index 6538f4d..48c05e9 100644 --- a/csaf-lib/build.rs +++ b/csaf-lib/build.rs @@ -21,21 +21,35 @@ fn main() -> Result<(), BuildError> { build( "./src/csaf/csaf2_0/csaf_json_schema.json", "csaf/csaf2_0/schema.rs", + true, + )?; + build( + "./src/csaf/csaf2_1/ssvc-1-0-1-merged.schema.json", + "csaf/csaf2_1/ssvc_schema.rs", + false, )?; build( "./src/csaf/csaf2_1/csaf_json_schema.json", "csaf/csaf2_1/schema.rs", + true, + )?; + build( + "../ssvc/data/schema/v1/Decision_Point-1-0-1.schema.json", + "csaf/csaf2_1/ssvc_dp_schema.rs", + false, )?; Ok(()) } -fn build(input: &str, output: &str) -> Result<(), BuildError> { +fn build(input: &str, output: &str, no_date_time: bool) -> Result<(), BuildError> { let content = fs::read_to_string(input)?; let mut schema_value = serde_json::from_str(&content)?; - // Recursively search for "format": "date-time" and replace with something else - remove_datetime_formats(&mut schema_value); - let schema = serde_json::from_value::(schema_value)?; + if no_date_time { + // Recursively search for "format": "date-time" and remove this format + remove_datetime_formats(&mut schema_value); + } + let schema: schemars::schema::RootSchema = serde_json::from_value(schema_value)?; let mut type_space = TypeSpace::new(TypeSpaceSettings::default().with_struct_builder(true)); type_space.add_root_schema(schema)?; diff --git a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs index 4c585c8..185f16b 100644 --- a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs @@ -1,7 +1,9 @@ -use crate::csaf::csaf2_0::schema::{Branch, CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, DocumentGenerator, DocumentLevelMetaData, DocumentStatus, Flag, FullProductNameT, HelperToIdentifyTheProduct, Involvement, LabelOfTlp, ProductGroup, ProductStatus, ProductTree, Relationship, Remediation, Revision, RulesForSharingDocument, Threat, Tracking, TrafficLightProtocolTlp, Vulnerability}; +use crate::csaf::csaf2_0::schema::{Branch, CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, DocumentGenerator, DocumentLevelMetaData, DocumentStatus, Flag, FullProductNameT, HelperToIdentifyTheProduct, Id, Involvement, LabelOfTlp, ProductGroup, ProductStatus, ProductTree, Relationship, Remediation, Revision, RulesForSharingDocument, Threat, Tracking, TrafficLightProtocolTlp, Vulnerability}; use crate::csaf::csaf2_1::schema::{CategoryOfTheRemediation as Remediation21, DocumentStatus as Status21, LabelOfTlp as Tlp21}; -use crate::csaf::getter_traits::{BranchTrait, CsafTrait, DistributionTrait, DocumentTrait, FlagTrait, ProductTrait, GeneratorTrait, InvolvementTrait, MetricTrait, ProductGroupTrait, ProductIdentificationHelperTrait, ProductStatusTrait, ProductTreeTrait, RelationshipTrait, RemediationTrait, RevisionTrait, SharingGroupTrait, ThreatTrait, TlpTrait, TrackingTrait, VulnerabilityTrait}; +use crate::csaf::getter_traits::{BranchTrait, CsafTrait, DistributionTrait, DocumentTrait, FlagTrait, ProductTrait, GeneratorTrait, InvolvementTrait, MetricTrait, ProductGroupTrait, ProductIdentificationHelperTrait, ProductStatusTrait, ProductTreeTrait, RelationshipTrait, RemediationTrait, RevisionTrait, SharingGroupTrait, ThreatTrait, TlpTrait, TrackingTrait, VulnerabilityTrait, ContentTrait, VulnerabilityIdTrait}; use std::ops::Deref; +use serde::de::Error; +use crate::csaf::csaf2_1::ssvc_schema::SsvcV1; use crate::csaf::validation::ValidationError; impl RemediationTrait for Remediation { @@ -73,6 +75,8 @@ impl ProductStatusTrait for ProductStatus { } impl MetricTrait for () { + type ContentType = (); + //noinspection RsConstantConditionIf fn get_products(&self) -> impl Iterator + '_ { // This construction is required to satisfy compiler checks @@ -82,6 +86,16 @@ impl MetricTrait for () { } std::iter::empty() } + + fn get_content(&self) -> &Self::ContentType { + panic!("Metrics are not implemented in CSAF 2.0"); + } +} + +impl ContentTrait for () { + fn get_ssvc_v1(&self) -> Result { + Err(serde_json::Error::custom("Metrics are not implemented in CSAF 2.0")) + } } impl ThreatTrait for Threat { @@ -102,6 +116,7 @@ impl VulnerabilityTrait for Vulnerability { type ThreatType = Threat; type FlagType = Flag; type InvolvementType = Involvement; + type VulnerabilityIdType = Id; fn get_remediations(&self) -> &Vec { &self.remediations @@ -120,7 +135,7 @@ impl VulnerabilityTrait for Vulnerability { &self.threats } - fn get_release_date(&self) -> &Option { + fn get_disclosure_date(&self) -> &Option { &self.release_date } @@ -135,6 +150,23 @@ impl VulnerabilityTrait for Vulnerability { fn get_involvements(&self) -> &Option> { &self.involvements } + + fn get_cve(&self) -> Option<&String> { + self.cve.as_ref().map(|x| x.deref()) + } + fn get_ids(&self) -> &Option> { + &self.ids + } +} + +impl VulnerabilityIdTrait for Id { + fn get_system_name(&self) -> &String { + self.system_name.deref() + } + + fn get_text(&self) -> &String { + self.text.deref() + } } impl FlagTrait for Flag { @@ -274,6 +306,10 @@ impl TrackingTrait for Tracking { DocumentStatus::Interim => Status21::Interim, } } + + fn get_id(&self) -> &String { + self.id.deref() + } } impl GeneratorTrait for DocumentGenerator { diff --git a/csaf-lib/src/csaf/csaf2_1/csaf_json_schema.json b/csaf-lib/src/csaf/csaf2_1/csaf_json_schema.json index 0f22ebb..042a764 100644 --- a/csaf-lib/src/csaf/csaf2_1/csaf_json_schema.json +++ b/csaf-lib/src/csaf/csaf2_1/csaf_json_schema.json @@ -854,7 +854,7 @@ }, "initial_release_date": { "title": "Initial release date", - "description": "The date when this document was first published.", + "description": "The date when this document was first released to the specified target group.", "type": "string", "format": "date-time" }, @@ -1098,6 +1098,12 @@ } } }, + "disclosure_date": { + "title": "Disclosure date", + "description": "Holds the date and time the vulnerability was originally disclosed to the public.", + "type": "string", + "format": "date-time" + }, "discovery_date": { "title": "Discovery date", "description": "Holds the date and time the vulnerability was originally discovered.", @@ -1267,6 +1273,9 @@ }, "cvss_v4": { "type": "object" + }, + "ssvc_v1": { + "type": "object" } } }, @@ -1340,12 +1349,6 @@ "description": "Holds a list of references associated with this vulnerability item.", "$ref": "#/$defs/references_t" }, - "release_date": { - "title": "Release date", - "description": "Holds the date and time the vulnerability was originally released into the wild.", - "type": "string", - "format": "date-time" - }, "remediations": { "title": "List of remediations", "description": "Contains a list of remediations.", diff --git a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs index 1569bd8..d23c189 100644 --- a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs @@ -1,6 +1,8 @@ -use crate::csaf::csaf2_1::schema::{Branch, CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, DocumentGenerator, DocumentLevelMetaData, DocumentStatus, Flag, FullProductNameT, HelperToIdentifyTheProduct, Involvement, LabelOfTlp, Metric, ProductGroup, ProductStatus, ProductTree, Relationship, Remediation, Revision, RulesForSharingDocument, SharingGroup, Threat, Tracking, TrafficLightProtocolTlp, Vulnerability}; -use crate::csaf::getter_traits::{BranchTrait, CsafTrait, DistributionTrait, DocumentTrait, FlagTrait, ProductTrait, GeneratorTrait, InvolvementTrait, MetricTrait, ProductGroupTrait, ProductIdentificationHelperTrait, ProductStatusTrait, ProductTreeTrait, RelationshipTrait, RemediationTrait, RevisionTrait, SharingGroupTrait, ThreatTrait, TlpTrait, TrackingTrait, VulnerabilityTrait}; +use crate::csaf::csaf2_1::schema::{Branch, CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, Content, DocumentGenerator, DocumentLevelMetaData, DocumentStatus, Flag, FullProductNameT, HelperToIdentifyTheProduct, Id, Involvement, LabelOfTlp, Metric, ProductGroup, ProductStatus, ProductTree, Relationship, Remediation, Revision, RulesForSharingDocument, SharingGroup, Threat, Tracking, TrafficLightProtocolTlp, Vulnerability}; +use crate::csaf::getter_traits::{BranchTrait, CsafTrait, DistributionTrait, DocumentTrait, FlagTrait, ProductTrait, GeneratorTrait, InvolvementTrait, MetricTrait, ProductGroupTrait, ProductIdentificationHelperTrait, ProductStatusTrait, ProductTreeTrait, RelationshipTrait, RemediationTrait, RevisionTrait, SharingGroupTrait, ThreatTrait, TlpTrait, TrackingTrait, VulnerabilityTrait, ContentTrait, VulnerabilityIdTrait}; use std::ops::Deref; +use serde_json::Value; +use crate::csaf::csaf2_1::ssvc_schema::SsvcV1; use crate::csaf::validation::ValidationError; impl RemediationTrait for Remediation { @@ -56,9 +58,22 @@ impl ProductStatusTrait for ProductStatus { } impl MetricTrait for Metric { + type ContentType = Content; + fn get_products(&self) -> impl Iterator + '_ { self.products.deref().iter().map(|p| p.deref()) } + + fn get_content(&self) -> &Self::ContentType { + &self.content + } +} + +impl ContentTrait for Content { + fn get_ssvc_v1(&self) -> Result { + let ssvc_value = Value::Object(self.ssvc_v1.clone()); + serde_json::from_value::(ssvc_value) + } } impl ThreatTrait for Threat { @@ -78,6 +93,7 @@ impl VulnerabilityTrait for Vulnerability { type ThreatType = Threat; type FlagType = Flag; type InvolvementType = Involvement; + type VulnerabilityIdType = Id; fn get_remediations(&self) -> &Vec { &self.remediations @@ -95,8 +111,8 @@ impl VulnerabilityTrait for Vulnerability { &self.threats } - fn get_release_date(&self) -> &Option { - &self.release_date + fn get_disclosure_date(&self) -> &Option { + &self.disclosure_date } fn get_discovery_date(&self) -> &Option { @@ -110,6 +126,24 @@ impl VulnerabilityTrait for Vulnerability { fn get_involvements(&self) -> &Option> { &self.involvements } + + fn get_cve(&self) -> Option<&String> { + self.cve.as_ref().map(|x| x.deref()) + } + + fn get_ids(&self) -> &Option> { + &self.ids + } +} + +impl VulnerabilityIdTrait for Id { + fn get_system_name(&self) -> &String { + self.system_name.deref() + } + + fn get_text(&self) -> &String { + self.text.deref() + } } impl FlagTrait for Flag { @@ -219,6 +253,10 @@ impl TrackingTrait for Tracking { fn get_status(&self) -> DocumentStatus { self.status } + + fn get_id(&self) -> &String { + self.id.deref() + } } impl GeneratorTrait for DocumentGenerator { diff --git a/csaf-lib/src/csaf/csaf2_1/mod.rs b/csaf-lib/src/csaf/csaf2_1/mod.rs index 6f7125d..b91c9c1 100644 --- a/csaf-lib/src/csaf/csaf2_1/mod.rs +++ b/csaf-lib/src/csaf/csaf2_1/mod.rs @@ -2,3 +2,5 @@ pub mod loader; pub mod schema; pub mod validation; pub mod getter_implementations; +pub mod ssvc_schema; +pub mod ssvc_dp_schema; diff --git a/csaf-lib/src/csaf/csaf2_1/schema.rs b/csaf-lib/src/csaf/csaf2_1/schema.rs index cd30e5c..09875c9 100644 --- a/csaf-lib/src/csaf/csaf2_1/schema.rs +++ b/csaf-lib/src/csaf/csaf2_1/schema.rs @@ -1896,7 +1896,7 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// }, /// "initial_release_date": { /// "title": "Initial release date", -/// "description": "The date when this document was first published.", +/// "description": "The date when this document was first released to the specified target group.", /// "type": "string" /// }, /// "revision_history": { @@ -2137,6 +2137,11 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "minItems": 1, /// "uniqueItems": true /// }, +/// "disclosure_date": { +/// "title": "Disclosure date", +/// "description": "Holds the date and time the vulnerability was originally disclosed to the public.", +/// "type": "string" +/// }, /// "discovery_date": { /// "title": "Discovery date", /// "description": "Holds the date and time the vulnerability was originally discovered.", @@ -2301,6 +2306,9 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// }, /// "cvss_v4": { /// "type": "object" +/// }, +/// "ssvc_v1": { +/// "type": "object" /// } /// } /// }, @@ -2376,11 +2384,6 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "description": "Holds a list of references associated with this vulnerability item.", /// "$ref": "#/$defs/references_t" /// }, -/// "release_date": { -/// "title": "Release date", -/// "description": "Holds the date and time the vulnerability was originally released into the wild.", -/// "type": "string" -/// }, /// "remediations": { /// "title": "List of remediations", /// "description": "Contains a list of remediations.", @@ -2661,6 +2664,9 @@ impl<'de> ::serde::Deserialize<'de> for ContactDetails { /// }, /// "cvss_v4": { /// "type": "object" +/// }, +/// "ssvc_v1": { +/// "type": "object" /// } /// } ///} @@ -2674,6 +2680,8 @@ pub struct Content { pub cvss_v3: ::serde_json::Map<::std::string::String, ::serde_json::Value>, #[serde(default, skip_serializing_if = "::serde_json::Map::is_empty")] pub cvss_v4: ::serde_json::Map<::std::string::String, ::serde_json::Value>, + #[serde(default, skip_serializing_if = "::serde_json::Map::is_empty")] + pub ssvc_v1: ::serde_json::Map<::std::string::String, ::serde_json::Value>, } impl ::std::convert::From<&Content> for Content { fn from(value: &Content) -> Self { @@ -2686,6 +2694,7 @@ impl ::std::default::Default for Content { cvss_v2: Default::default(), cvss_v3: Default::default(), cvss_v4: Default::default(), + ssvc_v1: Default::default(), } } } @@ -3863,7 +3872,7 @@ impl DocumentGenerator { /// }, /// "initial_release_date": { /// "title": "Initial release date", -/// "description": "The date when this document was first published.", +/// "description": "The date when this document was first released to the specified target group.", /// "type": "string" /// }, /// "revision_history": { @@ -5788,6 +5797,9 @@ impl<'de> ::serde::Deserialize<'de> for LegacyVersionOfTheRevision { /// }, /// "cvss_v4": { /// "type": "object" +/// }, +/// "ssvc_v1": { +/// "type": "object" /// } /// } /// }, @@ -9659,7 +9671,7 @@ impl<'de> ::serde::Deserialize<'de> for TitleOfThisDocument { /// }, /// "initial_release_date": { /// "title": "Initial release date", -/// "description": "The date when this document was first published.", +/// "description": "The date when this document was first released to the specified target group.", /// "type": "string" /// }, /// "revision_history": { @@ -9731,7 +9743,7 @@ pub struct Tracking { pub generator: ::std::option::Option, ///The ID is a simple label that provides for a wide range of numbering values, types, and schemes. Its value SHOULD be assigned and maintained by the original document issuing authority. pub id: UniqueIdentifierForTheDocument, - ///The date when this document was first published. + ///The date when this document was first released to the specified target group. pub initial_release_date: ::std::string::String, ///Holds one revision item for each version of the CSAF document, including the initial one. pub revision_history: ::std::vec::Vec, @@ -10157,6 +10169,11 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "minItems": 1, /// "uniqueItems": true /// }, +/// "disclosure_date": { +/// "title": "Disclosure date", +/// "description": "Holds the date and time the vulnerability was originally disclosed to the public.", +/// "type": "string" +/// }, /// "discovery_date": { /// "title": "Discovery date", /// "description": "Holds the date and time the vulnerability was originally discovered.", @@ -10321,6 +10338,9 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// }, /// "cvss_v4": { /// "type": "object" +/// }, +/// "ssvc_v1": { +/// "type": "object" /// } /// } /// }, @@ -10396,11 +10416,6 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "description": "Holds a list of references associated with this vulnerability item.", /// "$ref": "#/$defs/references_t" /// }, -/// "release_date": { -/// "title": "Release date", -/// "description": "Holds the date and time the vulnerability was originally released into the wild.", -/// "type": "string" -/// }, /// "remediations": { /// "title": "List of remediations", /// "description": "Contains a list of remediations.", @@ -10564,6 +10579,9 @@ pub struct Vulnerability { ///Contains a list of CWEs. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub cwes: ::std::option::Option>, + ///Holds the date and time the vulnerability was originally disclosed to the public. + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub disclosure_date: ::std::option::Option<::std::string::String>, ///Holds the date and time the vulnerability was originally discovered. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub discovery_date: ::std::option::Option<::std::string::String>, @@ -10587,9 +10605,6 @@ pub struct Vulnerability { ///Holds a list of references associated with this vulnerability item. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub references: ::std::option::Option, - ///Holds the date and time the vulnerability was originally released into the wild. - #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] - pub release_date: ::std::option::Option<::std::string::String>, ///Contains a list of remediations. #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] pub remediations: ::std::vec::Vec, @@ -10611,6 +10626,7 @@ impl ::std::default::Default for Vulnerability { acknowledgments: Default::default(), cve: Default::default(), cwes: Default::default(), + disclosure_date: Default::default(), discovery_date: Default::default(), flags: Default::default(), ids: Default::default(), @@ -10619,7 +10635,6 @@ impl ::std::default::Default for Vulnerability { notes: Default::default(), product_status: Default::default(), references: Default::default(), - release_date: Default::default(), remediations: Default::default(), threats: Default::default(), title: Default::default(), @@ -11179,6 +11194,10 @@ pub mod builder { ::serde_json::Map<::std::string::String, ::serde_json::Value>, ::std::string::String, >, + ssvc_v1: ::std::result::Result< + ::serde_json::Map<::std::string::String, ::serde_json::Value>, + ::std::string::String, + >, } impl ::std::default::Default for Content { fn default() -> Self { @@ -11186,6 +11205,7 @@ pub mod builder { cvss_v2: Ok(Default::default()), cvss_v3: Ok(Default::default()), cvss_v4: Ok(Default::default()), + ssvc_v1: Ok(Default::default()), } } } @@ -11232,6 +11252,20 @@ pub mod builder { }); self } + pub fn ssvc_v1(mut self, value: T) -> Self + where + T: ::std::convert::TryInto< + ::serde_json::Map<::std::string::String, ::serde_json::Value>, + >, + T::Error: ::std::fmt::Display, + { + self.ssvc_v1 = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for ssvc_v1: {}", e) + }); + self + } } impl ::std::convert::TryFrom for super::Content { type Error = super::error::ConversionError; @@ -11242,6 +11276,7 @@ pub mod builder { cvss_v2: value.cvss_v2?, cvss_v3: value.cvss_v3?, cvss_v4: value.cvss_v4?, + ssvc_v1: value.ssvc_v1?, }) } } @@ -11251,6 +11286,7 @@ pub mod builder { cvss_v2: Ok(value.cvss_v2), cvss_v3: Ok(value.cvss_v3), cvss_v4: Ok(value.cvss_v4), + ssvc_v1: Ok(value.ssvc_v1), } } } @@ -14076,6 +14112,10 @@ pub mod builder { ::std::option::Option>, ::std::string::String, >, + disclosure_date: ::std::result::Result< + ::std::option::Option<::std::string::String>, + ::std::string::String, + >, discovery_date: ::std::result::Result< ::std::option::Option<::std::string::String>, ::std::string::String, @@ -14108,10 +14148,6 @@ pub mod builder { ::std::option::Option, ::std::string::String, >, - release_date: ::std::result::Result< - ::std::option::Option<::std::string::String>, - ::std::string::String, - >, remediations: ::std::result::Result< ::std::vec::Vec, ::std::string::String, @@ -14131,6 +14167,7 @@ pub mod builder { acknowledgments: Ok(Default::default()), cve: Ok(Default::default()), cwes: Ok(Default::default()), + disclosure_date: Ok(Default::default()), discovery_date: Ok(Default::default()), flags: Ok(Default::default()), ids: Ok(Default::default()), @@ -14139,7 +14176,6 @@ pub mod builder { notes: Ok(Default::default()), product_status: Ok(Default::default()), references: Ok(Default::default()), - release_date: Ok(Default::default()), remediations: Ok(Default::default()), threats: Ok(Default::default()), title: Ok(Default::default()), @@ -14179,6 +14215,18 @@ pub mod builder { .map_err(|e| format!("error converting supplied value for cwes: {}", e)); self } + pub fn disclosure_date(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>, + T::Error: ::std::fmt::Display, + { + self.disclosure_date = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for disclosure_date: {}", e) + }); + self + } pub fn discovery_date(mut self, value: T) -> Self where T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>, @@ -14273,18 +14321,6 @@ pub mod builder { }); self } - pub fn release_date(mut self, value: T) -> Self - where - T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>, - T::Error: ::std::fmt::Display, - { - self.release_date = value - .try_into() - .map_err(|e| { - format!("error converting supplied value for release_date: {}", e) - }); - self - } pub fn remediations(mut self, value: T) -> Self where T: ::std::convert::TryInto<::std::vec::Vec>, @@ -14331,6 +14367,7 @@ pub mod builder { acknowledgments: value.acknowledgments?, cve: value.cve?, cwes: value.cwes?, + disclosure_date: value.disclosure_date?, discovery_date: value.discovery_date?, flags: value.flags?, ids: value.ids?, @@ -14339,7 +14376,6 @@ pub mod builder { notes: value.notes?, product_status: value.product_status?, references: value.references?, - release_date: value.release_date?, remediations: value.remediations?, threats: value.threats?, title: value.title?, @@ -14352,6 +14388,7 @@ pub mod builder { acknowledgments: Ok(value.acknowledgments), cve: Ok(value.cve), cwes: Ok(value.cwes), + disclosure_date: Ok(value.disclosure_date), discovery_date: Ok(value.discovery_date), flags: Ok(value.flags), ids: Ok(value.ids), @@ -14360,7 +14397,6 @@ pub mod builder { notes: Ok(value.notes), product_status: Ok(value.product_status), references: Ok(value.references), - release_date: Ok(value.release_date), remediations: Ok(value.remediations), threats: Ok(value.threats), title: Ok(value.title), diff --git a/csaf-lib/src/csaf/csaf2_1/ssvc-1-0-1-merged.schema.json b/csaf-lib/src/csaf/csaf2_1/ssvc-1-0-1-merged.schema.json new file mode 100644 index 0000000..6fa7c5b --- /dev/null +++ b/csaf-lib/src/csaf/csaf2_1/ssvc-1-0-1-merged.schema.json @@ -0,0 +1,104 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://certcc.github.io/SSVC/data/schema/v1/Decision_Point_Value_Selection-1-0-1.schema.json", + "description": "This schema defines the structure for selecting SSVC Decision Points and their evaluated values for a given vulnerability. Each vulnerability can have multiple Decision Points, and each Decision Point can have multiple selected values when full certainty is not available.", + "title": "SSVC_v1", + "$defs": { + "schemaVersion": { + "description": "Schema version used to represent this Decision Point.", + "type": "string", + "enum": ["1-0-1"] + }, + "id": { + "type": "string", + "description": "Identifier for the vulnerability that was evaluation, such as CVE, CERT/CC VU#, OSV id, Bugtraq, GHSA etc.", + "examples": ["CVE-1900-1234","VU#11111","GHSA-11a1-22b2-33c3"], + "minLength": 1 + }, + "role": { + "type": "string", + "description": "The role of the stakeholder performing the evaluation (e.g., Supplier, Deployer, Coordinator). See SSVC documentation for a currently identified list: https://certcc.github.io/SSVC/topics/enumerating_stakeholders/", + "examples": ["Supplier","Deployer","Coordinator"], + "minLength": 1 + }, + "timestamp" : { + "description": "Date and time when the evaluation of the Vulnerability was performed according to RFC 3339, section 5.6.", + "type": "string", + "format": "date-time" + }, + "SsvcdecisionpointselectionSchema": { + "description": "A down-selection of SSVC Decision Points that represent an evaluation at a specific time of a Vulnerability evaluation.", + "properties": { + "name": { + "type": "string", + "description": "A short label that identifies a Decision Point.", + "minLength": 1, + "examples": ["Exploitation", "Automatable"] + }, + "namespace": { + "type": "string", + "description": "Namespace (a short, unique string): The value must be one of the official namespaces, currenlty \"ssvc\", \"cvss\" OR can start with 'x_' for private namespaces. See SSVC Documentation for details.", + "pattern": "^(?=.{3,100}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,97}$", + "examples": ["ssvc", "cvss", "x_custom","x_custom/extension"] + }, + "values": { + "description": "One or more Decision Point Values that were selected for this Decision Point. If the evaluation is uncertain, multiple values may be listed to reflect the potential range of possibilities.", + "title": "values", + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "description": "A short label that identifies a Decision Point Value", + "minLength": 1, + "examples": ["Public PoC", "Yes"] + } + }, + "version": { + "type": "string", + "description": "Version (a semantic version string) that identifies the version of a Decision Point.", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "examples": ["1.0.1", "1.0.1-alpha"] + } + }, + "type": "object", + "required": [ + "name", + "namespace", + "values", + "version" + ], + "additionalProperties": false + } + }, + "properties": { + "id": { + "$ref": "#/$defs/id" + }, + "role": { + "$ref": "#/$defs/role" + }, + "schemaVersion": { + "$ref": "#/$defs/schemaVersion" + }, + "timestamp": { + "$ref": "#/$defs/timestamp" + }, + "selections": { + "description": "An array of Decision Points and their selected values for the identified Vulnerability. If a clear evaluation is uncertain, multiple values may be listed for a Decision Point instead of waiting for perfect clarity.", + "title": "selections", + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/SsvcdecisionpointselectionSchema" + } + } + }, + "type": "object", + "required": [ + "selections", + "id", + "timestamp", + "schemaVersion" + ], + "additionalProperties": false +} diff --git a/csaf-lib/src/csaf/csaf2_1/ssvc_dp_schema.rs b/csaf-lib/src/csaf/csaf2_1/ssvc_dp_schema.rs new file mode 100644 index 0000000..2fce551 --- /dev/null +++ b/csaf-lib/src/csaf/csaf2_1/ssvc_dp_schema.rs @@ -0,0 +1,1216 @@ +/// Error types. +pub mod error { + /// Error from a TryFrom or FromStr implementation. + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt( + &self, + f: &mut ::std::fmt::Formatter<'_>, + ) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + impl ::std::fmt::Debug for ConversionError { + fn fmt( + &self, + f: &mut ::std::fmt::Formatter<'_>, + ) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } +} +///DecisionPoint +/// +///
JSON schema +/// +/// ```json +///{ +/// "type": "object", +/// "required": [ +/// "description", +/// "key", +/// "name", +/// "namespace", +/// "schemaVersion", +/// "values", +/// "version" +/// ], +/// "properties": { +/// "description": { +/// "description": "A full description of the Decision Point, explaining what it represents and how it is used in SSVC.", +/// "type": "string", +/// "minLength": 1 +/// }, +/// "key": { +/// "description": "A short, unique string (or key) used as a shorthand identifier for a Decision Point.", +/// "examples": [ +/// "E", +/// "A" +/// ], +/// "type": "string", +/// "minLength": 1 +/// }, +/// "name": { +/// "description": "A short label that identifies a Decision Point.", +/// "examples": [ +/// "Exploitation", +/// "Automatable" +/// ], +/// "type": "string", +/// "minLength": 1 +/// }, +/// "namespace": { +/// "description": "Namespace (a short, unique string): The value must be one of the official namespaces, currenlty \"ssvc\", \"cvss\" OR can start with 'x_' for private namespaces. See SSVC Documentation for details.", +/// "examples": [ +/// "ssvc", +/// "cvss", +/// "x_custom", +/// "x_custom/extension" +/// ], +/// "type": "string", +/// "pattern": "^(?=.{3,100}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,97}$" +/// }, +/// "schemaVersion": { +/// "$ref": "#/$defs/schemaVersion" +/// }, +/// "values": { +/// "description": "A set of possible answers for a given Decision Point", +/// "type": "array", +/// "items": { +/// "$ref": "#/$defs/decision_point_value" +/// }, +/// "minItems": 1, +/// "uniqueItems": true +/// }, +/// "version": { +/// "description": "Version (a semantic version string) that identifies the version of a Decision Point.", +/// "examples": [ +/// "1.0.1", +/// "1.0.1-alpha" +/// ], +/// "type": "string", +/// "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" +/// } +/// }, +/// "additionalProperties": false +///} +/// ``` +///
+#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] +pub struct DecisionPoint { + ///A full description of the Decision Point, explaining what it represents and how it is used in SSVC. + pub description: DecisionPointDescription, + ///A short, unique string (or key) used as a shorthand identifier for a Decision Point. + pub key: DecisionPointKey, + ///A short label that identifies a Decision Point. + pub name: DecisionPointName, + ///Namespace (a short, unique string): The value must be one of the official namespaces, currenlty "ssvc", "cvss" OR can start with 'x_' for private namespaces. See SSVC Documentation for details. + pub namespace: DecisionPointNamespace, + #[serde(rename = "schemaVersion")] + pub schema_version: SchemaVersion, + ///A set of possible answers for a given Decision Point + pub values: Vec, + ///Version (a semantic version string) that identifies the version of a Decision Point. + pub version: DecisionPointVersion, +} +impl ::std::convert::From<&DecisionPoint> for DecisionPoint { + fn from(value: &DecisionPoint) -> Self { + value.clone() + } +} +impl DecisionPoint { + pub fn builder() -> builder::DecisionPoint { + Default::default() + } +} +///A full description of the Decision Point, explaining what it represents and how it is used in SSVC. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "A full description of the Decision Point, explaining what it represents and how it is used in SSVC.", +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct DecisionPointDescription(::std::string::String); +impl ::std::ops::Deref for DecisionPointDescription { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: DecisionPointDescription) -> Self { + value.0 + } +} +impl ::std::convert::From<&DecisionPointDescription> for DecisionPointDescription { + fn from(value: &DecisionPointDescription) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for DecisionPointDescription { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if value.len() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for DecisionPointDescription { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for DecisionPointDescription { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for DecisionPointDescription { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for DecisionPointDescription { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///A short, unique string (or key) used as a shorthand identifier for a Decision Point. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "A short, unique string (or key) used as a shorthand identifier for a Decision Point.", +/// "examples": [ +/// "E", +/// "A" +/// ], +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct DecisionPointKey(::std::string::String); +impl ::std::ops::Deref for DecisionPointKey { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: DecisionPointKey) -> Self { + value.0 + } +} +impl ::std::convert::From<&DecisionPointKey> for DecisionPointKey { + fn from(value: &DecisionPointKey) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for DecisionPointKey { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if value.len() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for DecisionPointKey { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for DecisionPointKey { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for DecisionPointKey { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for DecisionPointKey { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///A short label that identifies a Decision Point. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "A short label that identifies a Decision Point.", +/// "examples": [ +/// "Exploitation", +/// "Automatable" +/// ], +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct DecisionPointName(::std::string::String); +impl ::std::ops::Deref for DecisionPointName { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: DecisionPointName) -> Self { + value.0 + } +} +impl ::std::convert::From<&DecisionPointName> for DecisionPointName { + fn from(value: &DecisionPointName) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for DecisionPointName { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if value.len() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for DecisionPointName { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for DecisionPointName { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for DecisionPointName { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for DecisionPointName { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///Namespace (a short, unique string): The value must be one of the official namespaces, currenlty "ssvc", "cvss" OR can start with 'x_' for private namespaces. See SSVC Documentation for details. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "Namespace (a short, unique string): The value must be one of the official namespaces, currenlty \"ssvc\", \"cvss\" OR can start with 'x_' for private namespaces. See SSVC Documentation for details.", +/// "examples": [ +/// "ssvc", +/// "cvss", +/// "x_custom", +/// "x_custom/extension" +/// ], +/// "type": "string", +/// "pattern": "^(?=.{3,100}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,97}$" +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct DecisionPointNamespace(::std::string::String); +impl ::std::ops::Deref for DecisionPointNamespace { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: DecisionPointNamespace) -> Self { + value.0 + } +} +impl ::std::convert::From<&DecisionPointNamespace> for DecisionPointNamespace { + fn from(value: &DecisionPointNamespace) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for DecisionPointNamespace { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if regress::Regex::new("^(?=.{3,100}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,97}$") + .unwrap() + .find(value) + .is_none() + { + return Err( + "doesn't match pattern \"^(?=.{3,100}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,97}$\"" + .into(), + ); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for DecisionPointNamespace { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for DecisionPointNamespace { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for DecisionPointNamespace { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for DecisionPointNamespace { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///Decision points are the basic building blocks of SSVC decision functions. Individual decision points describe a single aspect of the input to a decision function. +/// +///
JSON schema +/// +/// ```json +///{ +/// "$id": "https://certcc.github.io/SSVC/data/schema/v1/Decision_Point-1-0-1.schema.json", +/// "title": "Decision Point schema definition", +/// "description": "Decision points are the basic building blocks of SSVC decision functions. Individual decision points describe a single aspect of the input to a decision function.", +/// "$ref": "#/$defs/decision_point" +///} +/// ``` +///
+#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(transparent)] +pub struct DecisionPointSchemaDefinition(pub DecisionPoint); +impl ::std::ops::Deref for DecisionPointSchemaDefinition { + type Target = DecisionPoint; + fn deref(&self) -> &DecisionPoint { + &self.0 + } +} +impl ::std::convert::From for DecisionPoint { + fn from(value: DecisionPointSchemaDefinition) -> Self { + value.0 + } +} +impl ::std::convert::From<&DecisionPointSchemaDefinition> +for DecisionPointSchemaDefinition { + fn from(value: &DecisionPointSchemaDefinition) -> Self { + value.clone() + } +} +impl ::std::convert::From for DecisionPointSchemaDefinition { + fn from(value: DecisionPoint) -> Self { + Self(value) + } +} +///DecisionPointValue +/// +///
JSON schema +/// +/// ```json +///{ +/// "type": "object", +/// "required": [ +/// "description", +/// "key", +/// "name" +/// ], +/// "properties": { +/// "description": { +/// "description": "A full description of the Decision Point Value.", +/// "examples": [ +/// "One of the following is true: (1) Typical public PoC exists in sources such as Metasploit or websites like ExploitDB; or (2) the vulnerability has a well-known method of exploitation.", +/// "Attackers can reliably automate steps 1-4 of the kill chain." +/// ], +/// "type": "string", +/// "minLength": 1 +/// }, +/// "key": { +/// "description": "A short, unique string (or key) used as a shorthand identifier for a Decision Point Value.", +/// "examples": [ +/// "P", +/// "Y" +/// ], +/// "type": "string", +/// "minLength": 1 +/// }, +/// "name": { +/// "description": "A short label that identifies a Decision Point Value", +/// "examples": [ +/// "Public PoC", +/// "Yes" +/// ], +/// "type": "string", +/// "minLength": 1 +/// } +/// }, +/// "additionalProperties": false +///} +/// ``` +///
+#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] +pub struct DecisionPointValue { + ///A full description of the Decision Point Value. + pub description: DecisionPointValueDescription, + ///A short, unique string (or key) used as a shorthand identifier for a Decision Point Value. + pub key: DecisionPointValueKey, + ///A short label that identifies a Decision Point Value + pub name: DecisionPointValueName, +} +impl ::std::convert::From<&DecisionPointValue> for DecisionPointValue { + fn from(value: &DecisionPointValue) -> Self { + value.clone() + } +} +impl DecisionPointValue { + pub fn builder() -> builder::DecisionPointValue { + Default::default() + } +} +///A full description of the Decision Point Value. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "A full description of the Decision Point Value.", +/// "examples": [ +/// "One of the following is true: (1) Typical public PoC exists in sources such as Metasploit or websites like ExploitDB; or (2) the vulnerability has a well-known method of exploitation.", +/// "Attackers can reliably automate steps 1-4 of the kill chain." +/// ], +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct DecisionPointValueDescription(::std::string::String); +impl ::std::ops::Deref for DecisionPointValueDescription { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: DecisionPointValueDescription) -> Self { + value.0 + } +} +impl ::std::convert::From<&DecisionPointValueDescription> +for DecisionPointValueDescription { + fn from(value: &DecisionPointValueDescription) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for DecisionPointValueDescription { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if value.len() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for DecisionPointValueDescription { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for DecisionPointValueDescription { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for DecisionPointValueDescription { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for DecisionPointValueDescription { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///A short, unique string (or key) used as a shorthand identifier for a Decision Point Value. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "A short, unique string (or key) used as a shorthand identifier for a Decision Point Value.", +/// "examples": [ +/// "P", +/// "Y" +/// ], +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct DecisionPointValueKey(::std::string::String); +impl ::std::ops::Deref for DecisionPointValueKey { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: DecisionPointValueKey) -> Self { + value.0 + } +} +impl ::std::convert::From<&DecisionPointValueKey> for DecisionPointValueKey { + fn from(value: &DecisionPointValueKey) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for DecisionPointValueKey { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if value.len() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for DecisionPointValueKey { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for DecisionPointValueKey { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for DecisionPointValueKey { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for DecisionPointValueKey { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///A short label that identifies a Decision Point Value +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "A short label that identifies a Decision Point Value", +/// "examples": [ +/// "Public PoC", +/// "Yes" +/// ], +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct DecisionPointValueName(::std::string::String); +impl ::std::ops::Deref for DecisionPointValueName { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: DecisionPointValueName) -> Self { + value.0 + } +} +impl ::std::convert::From<&DecisionPointValueName> for DecisionPointValueName { + fn from(value: &DecisionPointValueName) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for DecisionPointValueName { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if value.len() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for DecisionPointValueName { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for DecisionPointValueName { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for DecisionPointValueName { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for DecisionPointValueName { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///Version (a semantic version string) that identifies the version of a Decision Point. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "Version (a semantic version string) that identifies the version of a Decision Point.", +/// "examples": [ +/// "1.0.1", +/// "1.0.1-alpha" +/// ], +/// "type": "string", +/// "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct DecisionPointVersion(::std::string::String); +impl ::std::ops::Deref for DecisionPointVersion { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: DecisionPointVersion) -> Self { + value.0 + } +} +impl ::std::convert::From<&DecisionPointVersion> for DecisionPointVersion { + fn from(value: &DecisionPointVersion) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for DecisionPointVersion { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if regress::Regex::new( + "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + ) + .unwrap() + .find(value) + .is_none() + { + return Err( + "doesn't match pattern \"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$\"" + .into(), + ); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for DecisionPointVersion { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for DecisionPointVersion { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for DecisionPointVersion { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for DecisionPointVersion { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///Schema version used to represent this Decision Point. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "Schema version used to represent this Decision Point.", +/// "type": "string", +/// "enum": [ +/// "1-0-1" +/// ] +///} +/// ``` +///
+#[derive( + ::serde::Deserialize, + ::serde::Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd +)] +pub enum SchemaVersion { + #[serde(rename = "1-0-1")] + _101, +} +impl ::std::convert::From<&Self> for SchemaVersion { + fn from(value: &SchemaVersion) -> Self { + value.clone() + } +} +impl ::std::fmt::Display for SchemaVersion { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::_101 => write!(f, "1-0-1"), + } + } +} +impl ::std::str::FromStr for SchemaVersion { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + match value { + "1-0-1" => Ok(Self::_101), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for SchemaVersion { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for SchemaVersion { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for SchemaVersion { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +/// Types for composing complex structures. +pub mod builder { + #[derive(Clone, Debug)] + pub struct DecisionPoint { + description: ::std::result::Result< + super::DecisionPointDescription, + ::std::string::String, + >, + key: ::std::result::Result, + name: ::std::result::Result, + namespace: ::std::result::Result< + super::DecisionPointNamespace, + ::std::string::String, + >, + schema_version: ::std::result::Result< + super::SchemaVersion, + ::std::string::String, + >, + values: ::std::result::Result< + Vec, + ::std::string::String, + >, + version: ::std::result::Result< + super::DecisionPointVersion, + ::std::string::String, + >, + } + impl ::std::default::Default for DecisionPoint { + fn default() -> Self { + Self { + description: Err("no value supplied for description".to_string()), + key: Err("no value supplied for key".to_string()), + name: Err("no value supplied for name".to_string()), + namespace: Err("no value supplied for namespace".to_string()), + schema_version: Err("no value supplied for schema_version".to_string()), + values: Err("no value supplied for values".to_string()), + version: Err("no value supplied for version".to_string()), + } + } + } + impl DecisionPoint { + pub fn description(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.description = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for description: {}", e) + }); + self + } + pub fn key(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.key = value + .try_into() + .map_err(|e| format!("error converting supplied value for key: {}", e)); + self + } + pub fn name(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.name = value + .try_into() + .map_err(|e| format!("error converting supplied value for name: {}", e)); + self + } + pub fn namespace(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.namespace = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for namespace: {}", e) + }); + self + } + pub fn schema_version(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.schema_version = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for schema_version: {}", e) + }); + self + } + pub fn values(mut self, value: T) -> Self + where + T: ::std::convert::TryInto>, + T::Error: ::std::fmt::Display, + { + self.values = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for values: {}", e) + }); + self + } + pub fn version(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.version = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for version: {}", e) + }); + self + } + } + impl ::std::convert::TryFrom for super::DecisionPoint { + type Error = super::error::ConversionError; + fn try_from( + value: DecisionPoint, + ) -> ::std::result::Result { + Ok(Self { + description: value.description?, + key: value.key?, + name: value.name?, + namespace: value.namespace?, + schema_version: value.schema_version?, + values: value.values?, + version: value.version?, + }) + } + } + impl ::std::convert::From for DecisionPoint { + fn from(value: super::DecisionPoint) -> Self { + Self { + description: Ok(value.description), + key: Ok(value.key), + name: Ok(value.name), + namespace: Ok(value.namespace), + schema_version: Ok(value.schema_version), + values: Ok(value.values), + version: Ok(value.version), + } + } + } + #[derive(Clone, Debug)] + pub struct DecisionPointValue { + description: ::std::result::Result< + super::DecisionPointValueDescription, + ::std::string::String, + >, + key: ::std::result::Result, + name: ::std::result::Result< + super::DecisionPointValueName, + ::std::string::String, + >, + } + impl ::std::default::Default for DecisionPointValue { + fn default() -> Self { + Self { + description: Err("no value supplied for description".to_string()), + key: Err("no value supplied for key".to_string()), + name: Err("no value supplied for name".to_string()), + } + } + } + impl DecisionPointValue { + pub fn description(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.description = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for description: {}", e) + }); + self + } + pub fn key(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.key = value + .try_into() + .map_err(|e| format!("error converting supplied value for key: {}", e)); + self + } + pub fn name(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.name = value + .try_into() + .map_err(|e| format!("error converting supplied value for name: {}", e)); + self + } + } + impl ::std::convert::TryFrom for super::DecisionPointValue { + type Error = super::error::ConversionError; + fn try_from( + value: DecisionPointValue, + ) -> ::std::result::Result { + Ok(Self { + description: value.description?, + key: value.key?, + name: value.name?, + }) + } + } + impl ::std::convert::From for DecisionPointValue { + fn from(value: super::DecisionPointValue) -> Self { + Self { + description: Ok(value.description), + key: Ok(value.key), + name: Ok(value.name), + } + } + } +} diff --git a/csaf-lib/src/csaf/csaf2_1/ssvc_schema.rs b/csaf-lib/src/csaf/csaf2_1/ssvc_schema.rs new file mode 100644 index 0000000..87cad8d --- /dev/null +++ b/csaf-lib/src/csaf/csaf2_1/ssvc_schema.rs @@ -0,0 +1,1068 @@ +/// Error types. +pub mod error { + /// Error from a TryFrom or FromStr implementation. + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt( + &self, + f: &mut ::std::fmt::Formatter<'_>, + ) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + impl ::std::fmt::Debug for ConversionError { + fn fmt( + &self, + f: &mut ::std::fmt::Formatter<'_>, + ) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } +} +///Identifier for the vulnerability that was evaluation, such as CVE, CERT/CC VU#, OSV id, Bugtraq, GHSA etc. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "Identifier for the vulnerability that was evaluation, such as CVE, CERT/CC VU#, OSV id, Bugtraq, GHSA etc.", +/// "examples": [ +/// "CVE-1900-1234", +/// "VU#11111", +/// "GHSA-11a1-22b2-33c3" +/// ], +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct Id(::std::string::String); +impl ::std::ops::Deref for Id { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: Id) -> Self { + value.0 + } +} +impl ::std::convert::From<&Id> for Id { + fn from(value: &Id) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for Id { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if value.len() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for Id { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for Id { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for Id { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for Id { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///The role of the stakeholder performing the evaluation (e.g., Supplier, Deployer, Coordinator). See SSVC documentation for a currently identified list: https://certcc.github.io/SSVC/topics/enumerating_stakeholders/ +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "The role of the stakeholder performing the evaluation (e.g., Supplier, Deployer, Coordinator). See SSVC documentation for a currently identified list: https://certcc.github.io/SSVC/topics/enumerating_stakeholders/", +/// "examples": [ +/// "Supplier", +/// "Deployer", +/// "Coordinator" +/// ], +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct Role(::std::string::String); +impl ::std::ops::Deref for Role { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: Role) -> Self { + value.0 + } +} +impl ::std::convert::From<&Role> for Role { + fn from(value: &Role) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for Role { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if value.len() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for Role { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for Role { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for Role { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for Role { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///Schema version used to represent this Decision Point. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "Schema version used to represent this Decision Point.", +/// "type": "string", +/// "enum": [ +/// "1-0-1" +/// ] +///} +/// ``` +///
+#[derive( + ::serde::Deserialize, + ::serde::Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd +)] +pub enum SchemaVersion { + #[serde(rename = "1-0-1")] + _101, +} +impl ::std::convert::From<&Self> for SchemaVersion { + fn from(value: &SchemaVersion) -> Self { + value.clone() + } +} +impl ::std::fmt::Display for SchemaVersion { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::_101 => write!(f, "1-0-1"), + } + } +} +impl ::std::str::FromStr for SchemaVersion { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + match value { + "1-0-1" => Ok(Self::_101), + _ => Err("invalid value".into()), + } + } +} +impl ::std::convert::TryFrom<&str> for SchemaVersion { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for SchemaVersion { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for SchemaVersion { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +///This schema defines the structure for selecting SSVC Decision Points and their evaluated values for a given vulnerability. Each vulnerability can have multiple Decision Points, and each Decision Point can have multiple selected values when full certainty is not available. +/// +///
JSON schema +/// +/// ```json +///{ +/// "$id": "https://certcc.github.io/SSVC/data/schema/v1/Decision_Point_Value_Selection-1-0-1.schema.json", +/// "title": "SSVC_v1", +/// "description": "This schema defines the structure for selecting SSVC Decision Points and their evaluated values for a given vulnerability. Each vulnerability can have multiple Decision Points, and each Decision Point can have multiple selected values when full certainty is not available.", +/// "type": "object", +/// "required": [ +/// "id", +/// "schemaVersion", +/// "selections", +/// "timestamp" +/// ], +/// "properties": { +/// "id": { +/// "$ref": "#/$defs/id" +/// }, +/// "role": { +/// "$ref": "#/$defs/role" +/// }, +/// "schemaVersion": { +/// "$ref": "#/$defs/schemaVersion" +/// }, +/// "selections": { +/// "title": "selections", +/// "description": "An array of Decision Points and their selected values for the identified Vulnerability. If a clear evaluation is uncertain, multiple values may be listed for a Decision Point instead of waiting for perfect clarity.", +/// "type": "array", +/// "items": { +/// "$ref": "#/$defs/SsvcdecisionpointselectionSchema" +/// }, +/// "minItems": 1 +/// }, +/// "timestamp": { +/// "$ref": "#/$defs/timestamp" +/// } +/// }, +/// "additionalProperties": false +///} +/// ``` +///
+#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] +pub struct SsvcV1 { + pub id: Id, + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub role: ::std::option::Option, + #[serde(rename = "schemaVersion")] + pub schema_version: SchemaVersion, + ///An array of Decision Points and their selected values for the identified Vulnerability. If a clear evaluation is uncertain, multiple values may be listed for a Decision Point instead of waiting for perfect clarity. + pub selections: ::std::vec::Vec, + pub timestamp: Timestamp, +} +impl ::std::convert::From<&SsvcV1> for SsvcV1 { + fn from(value: &SsvcV1) -> Self { + value.clone() + } +} +impl SsvcV1 { + pub fn builder() -> builder::SsvcV1 { + Default::default() + } +} +///A down-selection of SSVC Decision Points that represent an evaluation at a specific time of a Vulnerability evaluation. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "A down-selection of SSVC Decision Points that represent an evaluation at a specific time of a Vulnerability evaluation.", +/// "type": "object", +/// "required": [ +/// "name", +/// "namespace", +/// "values", +/// "version" +/// ], +/// "properties": { +/// "name": { +/// "description": "A short label that identifies a Decision Point.", +/// "examples": [ +/// "Exploitation", +/// "Automatable" +/// ], +/// "type": "string", +/// "minLength": 1 +/// }, +/// "namespace": { +/// "description": "Namespace (a short, unique string): The value must be one of the official namespaces, currenlty \"ssvc\", \"cvss\" OR can start with 'x_' for private namespaces. See SSVC Documentation for details.", +/// "examples": [ +/// "ssvc", +/// "cvss", +/// "x_custom", +/// "x_custom/extension" +/// ], +/// "type": "string", +/// "pattern": "^(?=.{3,100}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,97}$" +/// }, +/// "values": { +/// "title": "values", +/// "description": "One or more Decision Point Values that were selected for this Decision Point. If the evaluation is uncertain, multiple values may be listed to reflect the potential range of possibilities.", +/// "type": "array", +/// "items": { +/// "description": "A short label that identifies a Decision Point Value", +/// "examples": [ +/// "Public PoC", +/// "Yes" +/// ], +/// "type": "string", +/// "minLength": 1 +/// }, +/// "minItems": 1 +/// }, +/// "version": { +/// "description": "Version (a semantic version string) that identifies the version of a Decision Point.", +/// "examples": [ +/// "1.0.1", +/// "1.0.1-alpha" +/// ], +/// "type": "string", +/// "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" +/// } +/// }, +/// "additionalProperties": false +///} +/// ``` +///
+#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] +pub struct SsvcdecisionpointselectionSchema { + ///A short label that identifies a Decision Point. + pub name: SsvcdecisionpointselectionSchemaName, + ///Namespace (a short, unique string): The value must be one of the official namespaces, currenlty "ssvc", "cvss" OR can start with 'x_' for private namespaces. See SSVC Documentation for details. + pub namespace: SsvcdecisionpointselectionSchemaNamespace, + ///One or more Decision Point Values that were selected for this Decision Point. If the evaluation is uncertain, multiple values may be listed to reflect the potential range of possibilities. + pub values: ::std::vec::Vec, + ///Version (a semantic version string) that identifies the version of a Decision Point. + pub version: SsvcdecisionpointselectionSchemaVersion, +} +impl ::std::convert::From<&SsvcdecisionpointselectionSchema> +for SsvcdecisionpointselectionSchema { + fn from(value: &SsvcdecisionpointselectionSchema) -> Self { + value.clone() + } +} +impl SsvcdecisionpointselectionSchema { + pub fn builder() -> builder::SsvcdecisionpointselectionSchema { + Default::default() + } +} +///A short label that identifies a Decision Point. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "A short label that identifies a Decision Point.", +/// "examples": [ +/// "Exploitation", +/// "Automatable" +/// ], +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct SsvcdecisionpointselectionSchemaName(::std::string::String); +impl ::std::ops::Deref for SsvcdecisionpointselectionSchemaName { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From +for ::std::string::String { + fn from(value: SsvcdecisionpointselectionSchemaName) -> Self { + value.0 + } +} +impl ::std::convert::From<&SsvcdecisionpointselectionSchemaName> +for SsvcdecisionpointselectionSchemaName { + fn from(value: &SsvcdecisionpointselectionSchemaName) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for SsvcdecisionpointselectionSchemaName { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if value.len() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for SsvcdecisionpointselectionSchemaName { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> +for SsvcdecisionpointselectionSchemaName { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> +for SsvcdecisionpointselectionSchemaName { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for SsvcdecisionpointselectionSchemaName { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///Namespace (a short, unique string): The value must be one of the official namespaces, currenlty "ssvc", "cvss" OR can start with 'x_' for private namespaces. See SSVC Documentation for details. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "Namespace (a short, unique string): The value must be one of the official namespaces, currenlty \"ssvc\", \"cvss\" OR can start with 'x_' for private namespaces. See SSVC Documentation for details.", +/// "examples": [ +/// "ssvc", +/// "cvss", +/// "x_custom", +/// "x_custom/extension" +/// ], +/// "type": "string", +/// "pattern": "^(?=.{3,100}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,97}$" +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct SsvcdecisionpointselectionSchemaNamespace(::std::string::String); +impl ::std::ops::Deref for SsvcdecisionpointselectionSchemaNamespace { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From +for ::std::string::String { + fn from(value: SsvcdecisionpointselectionSchemaNamespace) -> Self { + value.0 + } +} +impl ::std::convert::From<&SsvcdecisionpointselectionSchemaNamespace> +for SsvcdecisionpointselectionSchemaNamespace { + fn from(value: &SsvcdecisionpointselectionSchemaNamespace) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for SsvcdecisionpointselectionSchemaNamespace { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if regress::Regex::new("^(?=.{3,100}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,97}$") + .unwrap() + .find(value) + .is_none() + { + return Err( + "doesn't match pattern \"^(?=.{3,100}$)(x_)?[a-z0-9]{3}([/.-]?[a-z0-9]+){0,97}$\"" + .into(), + ); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for SsvcdecisionpointselectionSchemaNamespace { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> +for SsvcdecisionpointselectionSchemaNamespace { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> +for SsvcdecisionpointselectionSchemaNamespace { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for SsvcdecisionpointselectionSchemaNamespace { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///Version (a semantic version string) that identifies the version of a Decision Point. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "Version (a semantic version string) that identifies the version of a Decision Point.", +/// "examples": [ +/// "1.0.1", +/// "1.0.1-alpha" +/// ], +/// "type": "string", +/// "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct SsvcdecisionpointselectionSchemaVersion(::std::string::String); +impl ::std::ops::Deref for SsvcdecisionpointselectionSchemaVersion { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From +for ::std::string::String { + fn from(value: SsvcdecisionpointselectionSchemaVersion) -> Self { + value.0 + } +} +impl ::std::convert::From<&SsvcdecisionpointselectionSchemaVersion> +for SsvcdecisionpointselectionSchemaVersion { + fn from(value: &SsvcdecisionpointselectionSchemaVersion) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for SsvcdecisionpointselectionSchemaVersion { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if regress::Regex::new( + "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + ) + .unwrap() + .find(value) + .is_none() + { + return Err( + "doesn't match pattern \"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$\"" + .into(), + ); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for SsvcdecisionpointselectionSchemaVersion { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> +for SsvcdecisionpointselectionSchemaVersion { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> +for SsvcdecisionpointselectionSchemaVersion { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for SsvcdecisionpointselectionSchemaVersion { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///Date and time when the evaluation of the Vulnerability was performed according to RFC 3339, section 5.6. +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "Date and time when the evaluation of the Vulnerability was performed according to RFC 3339, section 5.6.", +/// "type": "string", +/// "format": "date-time" +///} +/// ``` +///
+#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(transparent)] +pub struct Timestamp(pub chrono::DateTime); +impl ::std::ops::Deref for Timestamp { + type Target = chrono::DateTime; + fn deref(&self) -> &chrono::DateTime { + &self.0 + } +} +impl ::std::convert::From for chrono::DateTime { + fn from(value: Timestamp) -> Self { + value.0 + } +} +impl ::std::convert::From<&Timestamp> for Timestamp { + fn from(value: &Timestamp) -> Self { + value.clone() + } +} +impl ::std::convert::From> for Timestamp { + fn from(value: chrono::DateTime) -> Self { + Self(value) + } +} +impl ::std::str::FromStr for Timestamp { + type Err = as ::std::str::FromStr>::Err; + fn from_str(value: &str) -> ::std::result::Result { + Ok(Self(value.parse()?)) + } +} +impl ::std::convert::TryFrom<&str> for Timestamp { + type Error = as ::std::str::FromStr>::Err; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&String> for Timestamp { + type Error = as ::std::str::FromStr>::Err; + fn try_from(value: &String) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom for Timestamp { + type Error = as ::std::str::FromStr>::Err; + fn try_from(value: String) -> ::std::result::Result { + value.parse() + } +} +impl ::std::fmt::Display for Timestamp { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + self.0.fmt(f) + } +} +///A short label that identifies a Decision Point Value +/// +///
JSON schema +/// +/// ```json +///{ +/// "description": "A short label that identifies a Decision Point Value", +/// "examples": [ +/// "Public PoC", +/// "Yes" +/// ], +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct ValuesItem(::std::string::String); +impl ::std::ops::Deref for ValuesItem { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: ValuesItem) -> Self { + value.0 + } +} +impl ::std::convert::From<&ValuesItem> for ValuesItem { + fn from(value: &ValuesItem) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for ValuesItem { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if value.len() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for ValuesItem { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for ValuesItem { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<::std::string::String> for ValuesItem { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for ValuesItem { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + ::std::string::String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +/// Types for composing complex structures. +pub mod builder { + #[derive(Clone, Debug)] + pub struct SsvcV1 { + id: ::std::result::Result, + role: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, + schema_version: ::std::result::Result< + super::SchemaVersion, + ::std::string::String, + >, + selections: ::std::result::Result< + ::std::vec::Vec, + ::std::string::String, + >, + timestamp: ::std::result::Result, + } + impl ::std::default::Default for SsvcV1 { + fn default() -> Self { + Self { + id: Err("no value supplied for id".to_string()), + role: Ok(Default::default()), + schema_version: Err("no value supplied for schema_version".to_string()), + selections: Err("no value supplied for selections".to_string()), + timestamp: Err("no value supplied for timestamp".to_string()), + } + } + } + impl SsvcV1 { + pub fn id(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.id = value + .try_into() + .map_err(|e| format!("error converting supplied value for id: {}", e)); + self + } + pub fn role(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option>, + T::Error: ::std::fmt::Display, + { + self.role = value + .try_into() + .map_err(|e| format!("error converting supplied value for role: {}", e)); + self + } + pub fn schema_version(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.schema_version = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for schema_version: {}", e) + }); + self + } + pub fn selections(mut self, value: T) -> Self + where + T: ::std::convert::TryInto< + ::std::vec::Vec, + >, + T::Error: ::std::fmt::Display, + { + self.selections = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for selections: {}", e) + }); + self + } + pub fn timestamp(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.timestamp = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for timestamp: {}", e) + }); + self + } + } + impl ::std::convert::TryFrom for super::SsvcV1 { + type Error = super::error::ConversionError; + fn try_from( + value: SsvcV1, + ) -> ::std::result::Result { + Ok(Self { + id: value.id?, + role: value.role?, + schema_version: value.schema_version?, + selections: value.selections?, + timestamp: value.timestamp?, + }) + } + } + impl ::std::convert::From for SsvcV1 { + fn from(value: super::SsvcV1) -> Self { + Self { + id: Ok(value.id), + role: Ok(value.role), + schema_version: Ok(value.schema_version), + selections: Ok(value.selections), + timestamp: Ok(value.timestamp), + } + } + } + #[derive(Clone, Debug)] + pub struct SsvcdecisionpointselectionSchema { + name: ::std::result::Result< + super::SsvcdecisionpointselectionSchemaName, + ::std::string::String, + >, + namespace: ::std::result::Result< + super::SsvcdecisionpointselectionSchemaNamespace, + ::std::string::String, + >, + values: ::std::result::Result< + ::std::vec::Vec, + ::std::string::String, + >, + version: ::std::result::Result< + super::SsvcdecisionpointselectionSchemaVersion, + ::std::string::String, + >, + } + impl ::std::default::Default for SsvcdecisionpointselectionSchema { + fn default() -> Self { + Self { + name: Err("no value supplied for name".to_string()), + namespace: Err("no value supplied for namespace".to_string()), + values: Err("no value supplied for values".to_string()), + version: Err("no value supplied for version".to_string()), + } + } + } + impl SsvcdecisionpointselectionSchema { + pub fn name(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.name = value + .try_into() + .map_err(|e| format!("error converting supplied value for name: {}", e)); + self + } + pub fn namespace(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.namespace = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for namespace: {}", e) + }); + self + } + pub fn values(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::vec::Vec>, + T::Error: ::std::fmt::Display, + { + self.values = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for values: {}", e) + }); + self + } + pub fn version(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.version = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for version: {}", e) + }); + self + } + } + impl ::std::convert::TryFrom + for super::SsvcdecisionpointselectionSchema { + type Error = super::error::ConversionError; + fn try_from( + value: SsvcdecisionpointselectionSchema, + ) -> ::std::result::Result { + Ok(Self { + name: value.name?, + namespace: value.namespace?, + values: value.values?, + version: value.version?, + }) + } + } + impl ::std::convert::From + for SsvcdecisionpointselectionSchema { + fn from(value: super::SsvcdecisionpointselectionSchema) -> Self { + Self { + name: Ok(value.name), + namespace: Ok(value.namespace), + values: Ok(value.values), + version: Ok(value.version), + } + } + } +} diff --git a/csaf-lib/src/csaf/getter_traits.rs b/csaf-lib/src/csaf/getter_traits.rs index 368fab7..2c8d0e5 100644 --- a/csaf-lib/src/csaf/getter_traits.rs +++ b/csaf-lib/src/csaf/getter_traits.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeSet, HashSet}; use crate::csaf::csaf2_1::schema::{CategoryOfTheRemediation, DocumentStatus, LabelOfTlp}; +use crate::csaf::csaf2_1::ssvc_schema::SsvcV1; use crate::csaf::helpers::resolve_product_groups; use crate::csaf::validation::ValidationError; @@ -100,6 +101,9 @@ pub trait TrackingTrait { /// Returns the status of this document fn get_status(&self) -> DocumentStatus; + + /// Returns the tracking ID of this document + fn get_id(&self) -> &String; } /// Trait for accessing document generator information @@ -138,12 +142,15 @@ pub trait VulnerabilityTrait { /// The associated type representing the threat information. type ThreatType: ThreatTrait; - /// The type representing a vulnerability flag + /// The associated type representing a vulnerability flag type FlagType: FlagTrait; - /// The type representing a vulnerability involvement + /// The associated type representing a vulnerability involvement type InvolvementType: InvolvementTrait; + /// The associated type representing the vulnerability ID information. + type VulnerabilityIdType: VulnerabilityIdTrait; + /// Retrieves a list of remediations associated with the vulnerability. fn get_remediations(&self) -> &Vec; @@ -157,7 +164,7 @@ pub trait VulnerabilityTrait { fn get_threats(&self) -> &Vec; /// Returns the date when this vulnerability was initially disclosed - fn get_release_date(&self) -> &Option; + fn get_disclosure_date(&self) -> &Option; /// Returns the date when this vulnerability was initially discovered fn get_discovery_date(&self) -> &Option; @@ -167,6 +174,18 @@ pub trait VulnerabilityTrait { /// Returns all involvements associated with this vulnerability fn get_involvements(&self) -> &Option>; + + /// Returns the CVE associated with the vulnerability + fn get_cve(&self) -> Option<&String>; + + /// Returns the vulnerability IDs associated with this vulnerability + fn get_ids(&self) -> &Option>; +} + +pub trait VulnerabilityIdTrait { + fn get_system_name(&self) -> &String; + + fn get_text(&self) -> &String; } /// Trait for accessing vulnerability flags information @@ -310,8 +329,16 @@ pub trait ProductStatusTrait { /// Trait representing an abstract metric in a CSAF document. pub trait MetricTrait { + type ContentType: ContentTrait; + /// Retrieves a vector of product IDs associated with this metric. fn get_products(&self) -> impl Iterator + '_; + + fn get_content(&self) -> &Self::ContentType; +} + +pub trait ContentTrait { + fn get_ssvc_v1(&self) -> Result; } /// Trait representing an abstract threat in a CSAF document. diff --git a/csaf-lib/src/csaf/helpers.rs b/csaf-lib/src/csaf/helpers.rs index cf0ec7f..48f9aee 100644 --- a/csaf-lib/src/csaf/helpers.rs +++ b/csaf-lib/src/csaf/helpers.rs @@ -1,5 +1,10 @@ use crate::csaf::getter_traits::{CsafTrait, ProductGroupTrait, ProductTreeTrait}; -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::fs; +use std::ops::Deref; +use std::sync::LazyLock; +use glob::glob; +use crate::csaf::csaf2_1::ssvc_dp_schema::DecisionPoint; pub fn resolve_product_groups<'a, I>(doc: &impl CsafTrait, product_groups: I) -> Option> where @@ -41,3 +46,71 @@ pub fn count_unescaped_stars(s: &str) -> u32 { } count } + +/// Recursively loads all decision point JSON descriptions from ../ssvc/data/json/decision_points. +/// Entries are stored in a `HashMap` indexed by their respective (name, version) tuple for lookup. +pub static SSVC_DECISION_POINTS: LazyLock> = LazyLock::new(|| { + let mut decision_points = HashMap::new(); + + // Use glob to find all JSON files that might contain decision point data + match glob("../ssvc/data/json/decision_points/**/*.json") { + Ok(paths) => { + for path_res in paths { + match path_res { + Ok(path) => { + match fs::read_to_string(&path) { + Ok(content) => { + match serde_json::from_str::(&content) { + Ok(dp) => { + println!("Loaded SSVC decision point '{}' (version {})", dp.name.deref(), dp.version.deref()); + // Insert using (name, key) tuple as the key + let key = ( + dp.namespace.deref().to_owned(), + dp.name.deref().to_owned(), + dp.version.deref().to_owned(), + ); + decision_points.insert(key, dp); + }, + Err(err) => eprintln!("Warning: Failed to parse decision point from file {:?}: {}", path, err), + } + }, + Err(err) => eprintln!("Warning: Failed to read file {:?}: {}", path, err), + } + }, + Err(ref err) => eprintln!("Warning: Failed to read glob result {:?}: {}", path_res, err), + } + } + }, + Err(err) => eprintln!("Warning: Failed to search for decision point files: {}", err), + } + + decision_points +}); + +/// Derives lookup maps for all observed SSVC decision points that can be used +/// to verify the order of values within the respective decision points. +pub static DP_VAL_LOOKUP: LazyLock>> = LazyLock::new(|| { + let mut lookups = HashMap::new(); + + for (key, dp) in SSVC_DECISION_POINTS.iter() { + let mut lookup_map = HashMap::new(); + for (i, v) in dp.values.iter().enumerate() { + lookup_map.insert(v.name.deref().to_owned(), i as i32); + } + lookups.insert(key.clone(), lookup_map); + } + + lookups +}); + +/// Collects all "registered" namespaces from known decision points. We assume that each namespace +/// that occurs in at least one decision point in the SSVC repository is a "registered" namespace. +pub static REGISTERED_SSVC_NAMESPACES: LazyLock> = LazyLock::new(|| { + let mut namespaces = HashSet::new(); + + for (namespace, _, _) in SSVC_DECISION_POINTS.keys() { + namespaces.insert(namespace.to_owned()); + } + + namespaces +}); \ No newline at end of file diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index 52fd5da..d295fcf 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -9,3 +9,10 @@ pub mod test_6_1_39; pub mod test_6_1_40; pub mod test_6_1_41; pub mod test_6_1_42; +pub mod test_6_1_43; +pub mod test_6_1_44; +pub mod test_6_1_45; +pub mod test_6_1_46; +pub mod test_6_1_47; +pub mod test_6_1_48; +pub mod test_6_1_49; diff --git a/csaf-lib/src/csaf/validations/test_6_1_37.rs b/csaf-lib/src/csaf/validations/test_6_1_37.rs index 840c806..640896b 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_37.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_37.rs @@ -1,15 +1,11 @@ use crate::csaf::getter_traits::{CsafTrait, DocumentTrait, FlagTrait, GeneratorTrait, InvolvementTrait, RemediationTrait, RevisionTrait, ThreatTrait, TrackingTrait, VulnerabilityTrait}; use crate::csaf::validation::ValidationError; use regex::Regex; -use std::sync::OnceLock; +use std::sync::LazyLock; -static RFC3339_REGEX: OnceLock = OnceLock::new(); - -fn get_rfc3339_regex() -> &'static Regex { - RFC3339_REGEX.get_or_init(|| - Regex::new(r"^((\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?)(Z|[+-]\d{2}:\d{2}))$").unwrap() - ) -} +static CSAF_RFC3339_REGEX: LazyLock = LazyLock::new(|| + Regex::new(r"^((\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?)(Z|[+-]\d{2}:\d{2}))$").unwrap() +); /// Validates that all date/time fields in the CSAF document conform to the required format /// (ISO 8601 format with time zone or UTC). @@ -45,8 +41,8 @@ pub fn test_6_1_37_date_and_time( // Check vulnerability related dates for (i_v, vuln) in doc.get_vulnerabilities().iter().enumerate() { // Check disclosure date if present - if let Some(date) = vuln.get_release_date() { - check_datetime(date, &format!("/vulnerabilities/{}/release_date", i_v))?; + if let Some(date) = vuln.get_disclosure_date() { + check_datetime(date, &format!("/vulnerabilities/{}/disclosure_date", i_v))?; } // Check discovery date if present @@ -94,7 +90,7 @@ pub fn test_6_1_37_date_and_time( } fn check_datetime(date_time: &String, instance_path: &str) -> Result<(), ValidationError> { - if get_rfc3339_regex().is_match(date_time) { + if CSAF_RFC3339_REGEX.is_match(date_time) { // Add chrono-based plausibility check match chrono::DateTime::parse_from_rfc3339(date_time) { Ok(_) => Ok(()), // Successfully parsed as a valid RFC3339 datetime @@ -118,6 +114,8 @@ mod tests { use crate::csaf::validations::test_6_1_37::test_6_1_37_date_and_time; use std::collections::HashMap; + /* + Ignored because of https://github.com/oasis-tcs/csaf/issues/963 #[test] fn test_test_6_1_37() { run_csaf21_tests( @@ -136,14 +134,14 @@ mod tests { instance_path: "/vulnerabilities/0/discovery_date".to_string(), }), ("04", &ValidationError { - message: "Date-time string 2023-02-30T00:00:00+01:00 matched RFC3339 regex but failed chrono parsing: input is out of range".to_string(), - instance_path: "/vulnerabilities/0/discovery_date".to_string(), + message: "Date-time string 2023-04-31T00:00:00+01:00 matched RFC3339 regex but failed chrono parsing: input is out of range".to_string(), + instance_path: "/vulnerabilities/0/disclosure_date".to_string(), }), ("05", &ValidationError { - message: "Date-time string 1900-02-29T00:00:00+01:00 matched RFC3339 regex but failed chrono parsing: input is out of range".to_string(), - instance_path: "/vulnerabilities/0/discovery_date".to_string(), + message: "Date-time string 2023-02-29T00:00:00+01:00 matched RFC3339 regex but failed chrono parsing: input is out of range".to_string(), + instance_path: "/vulnerabilities/0/disclosure_date".to_string(), }), ]) ); - } + }*/ } diff --git a/csaf-lib/src/csaf/validations/test_6_1_43.rs b/csaf-lib/src/csaf/validations/test_6_1_43.rs new file mode 100644 index 0000000..6d8c5bb --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_43.rs @@ -0,0 +1,50 @@ +use crate::csaf::getter_traits::{CsafTrait, ProductIdentificationHelperTrait, ProductTrait, ProductTreeTrait}; +use crate::csaf::helpers::count_unescaped_stars; +use crate::csaf::validation::ValidationError; + +pub fn test_6_1_43_multiple_stars_in_model_number( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + if let Some(product_tree) = doc.get_product_tree() { + product_tree.visit_all_products(&mut |product, path| { + if let Some(helper) = product.get_product_identification_helper() { + if let Some(model_numbers) = helper.get_model_numbers() { + for (index, model_number) in model_numbers.enumerate() { + if count_unescaped_stars(model_number) > 1 { + return Err(ValidationError { + message: "Model number must not contain multiple unescaped asterisks (stars)".to_string(), + instance_path: format!("{}/product_identification_helper/model_numbers/{}", path, index), + }); + } + } + } + } + Ok(()) + })?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::csaf::test_helper::run_csaf21_tests; + use crate::csaf::validation::ValidationError; + use crate::csaf::validations::test_6_1_43::test_6_1_43_multiple_stars_in_model_number; + use std::collections::HashMap; + + #[test] + fn test_test_6_1_43() { + let expected_error = ValidationError { + message: "Model number must not contain multiple unescaped asterisks (stars)".to_string(), + instance_path: "/product_tree/full_product_names/0/product_identification_helper/model_numbers/0".to_string(), + }; + + run_csaf21_tests( + "43", + test_6_1_43_multiple_stars_in_model_number, HashMap::from([ + ("01", &expected_error), + ("02", &expected_error), + ]) + ); + } +} diff --git a/csaf-lib/src/csaf/validations/test_6_1_44.rs b/csaf-lib/src/csaf/validations/test_6_1_44.rs new file mode 100644 index 0000000..d0a35f1 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_44.rs @@ -0,0 +1,50 @@ +use crate::csaf::getter_traits::{CsafTrait, ProductIdentificationHelperTrait, ProductTrait, ProductTreeTrait}; +use crate::csaf::helpers::count_unescaped_stars; +use crate::csaf::validation::ValidationError; + +pub fn test_6_1_44_multiple_stars_in_serial_number( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + if let Some(product_tree) = doc.get_product_tree() { + product_tree.visit_all_products(&mut |product, path| { + if let Some(helper) = product.get_product_identification_helper() { + if let Some(serial_numbers) = helper.get_serial_numbers() { + for (index, serial_number) in serial_numbers.enumerate() { + if count_unescaped_stars(serial_number) > 1 { + return Err(ValidationError { + message: "Serial number must not contain multiple unescaped asterisks (stars)".to_string(), + instance_path: format!("{}/product_identification_helper/serial_numbers/{}", path, index), + }); + } + } + } + } + Ok(()) + })?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::csaf::test_helper::run_csaf21_tests; + use crate::csaf::validation::ValidationError; + use std::collections::HashMap; + use crate::csaf::validations::test_6_1_44::test_6_1_44_multiple_stars_in_serial_number; + + #[test] + fn test_test_6_1_44() { + let expected_error = ValidationError { + message: "Serial number must not contain multiple unescaped asterisks (stars)".to_string(), + instance_path: "/product_tree/full_product_names/0/product_identification_helper/serial_numbers/0".to_string(), + }; + + run_csaf21_tests( + "44", + test_6_1_44_multiple_stars_in_serial_number, HashMap::from([ + ("01", &expected_error), + ("02", &expected_error), + ]) + ); + } +} diff --git a/csaf-lib/src/csaf/validations/test_6_1_45.rs b/csaf-lib/src/csaf/validations/test_6_1_45.rs new file mode 100644 index 0000000..ce0edb9 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_45.rs @@ -0,0 +1,105 @@ +use crate::csaf::csaf2_1::schema::{DocumentStatus, LabelOfTlp}; +use crate::csaf::getter_traits::{CsafTrait, DistributionTrait, DocumentTrait, RevisionTrait, TlpTrait, TrackingTrait, VulnerabilityTrait}; +use crate::csaf::validation::ValidationError; +use chrono::{DateTime, FixedOffset}; + +pub fn test_6_1_45_inconsistent_disclosure_date( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + // Only check if document is TLP:CLEAR and status is final or interim + let document = doc.get_document(); + let status = document.get_tracking().get_status(); + + if status != DocumentStatus::Final && status != DocumentStatus::Interim { + return Ok(()); + } + + let is_tlp_clear = match document.get_distribution_21() { + Ok(distribution) => match distribution.get_tlp_21() { + Ok(tlp) => tlp.get_label() == LabelOfTlp::Clear, + Err(_) => false, + }, + Err(_) => false, + }; + + if !is_tlp_clear { + return Ok(()); + } + + // Get the newest revision history date + let mut newest_revision_date: Option> = None; + let revision_history = document.get_tracking().get_revision_history(); + for (i_rev, rev) in revision_history.iter().enumerate() { + chrono::DateTime::parse_from_rfc3339(rev.get_date()) + .map(|rev_datetime| { + println!( + "rev_datetime: {:?}, newest_revision_date: {:?}", + rev_datetime, + newest_revision_date + ); + newest_revision_date = match newest_revision_date { + None => Some(rev_datetime), + Some(prev_max) => Some(prev_max.max(rev_datetime)), + } + }) + .map_err(|_| ValidationError { + message: format!("Invalid date format in revision history: {}", rev.get_date()), + instance_path: format!("/document/tracking/revision_history/{}", i_rev), + })?; + } + + if let Some(newest_date) = newest_revision_date { + // Check each vulnerability's disclosure date + for (i_v, v) in doc.get_vulnerabilities().iter().enumerate() { + if let Some(disclosure_date) = v.get_disclosure_date() { + match chrono::DateTime::parse_from_rfc3339(disclosure_date) { + Ok(disclosure_datetime) => { + println!( + "disclosure_datetime: {:?}, newest_date: {:?}", + disclosure_datetime, newest_date + ); + if disclosure_datetime > newest_date { + return Err(ValidationError { + message: "Disclosure date must not be later than the newest revision history date for TLP:CLEAR documents with final or interim status".to_string(), + instance_path: format!("/vulnerabilities/{}/discovery_date", i_v), + }); + } + }, + Err(_) => { + return Err(ValidationError { + message: format!("Invalid disclosure date format: {}", disclosure_date), + instance_path: format!("/vulnerabilities/{}/discovery_date", i_v), + }); + } + } + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::csaf::test_helper::run_csaf21_tests; + use crate::csaf::validation::ValidationError; + use crate::csaf::validations::test_6_1_45::test_6_1_45_inconsistent_disclosure_date; + use std::collections::HashMap; + + #[test] + fn test_test_6_1_45() { + let expected_error = ValidationError { + message: "Disclosure date must not be later than the newest revision history date for TLP:CLEAR documents with final or interim status".to_string(), + instance_path: "/vulnerabilities/0/discovery_date".to_string(), + }; + + run_csaf21_tests( + "45", + test_6_1_45_inconsistent_disclosure_date, HashMap::from([ + ("01", &expected_error), + ("02", &expected_error), + ("03", &expected_error), + ]) + ); + } +} diff --git a/csaf-lib/src/csaf/validations/test_6_1_46.rs b/csaf-lib/src/csaf/validations/test_6_1_46.rs new file mode 100644 index 0000000..5e4cb75 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_46.rs @@ -0,0 +1,47 @@ +use crate::csaf::getter_traits::{ContentTrait, CsafTrait, MetricTrait, VulnerabilityTrait}; +use crate::csaf::validation::ValidationError; + +pub fn test_6_1_46_invalid_ssvc( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + // /vulnerabilities[]/metrics[]/content/ssvc_v1 + for (i_v, v) in doc.get_vulnerabilities().iter().enumerate() { + if let Some(metrics) = v.get_metrics() { + for (i_m, m) in metrics.iter().enumerate() { + m.get_content().get_ssvc_v1().map_err(|e| { + ValidationError { + message: format!("Invalid SSVC object: {}", e), + instance_path: format!("/vulnerabilities/{}/metrics/{}/content/ssvc_v1", i_v, i_m), + } + })?; + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::csaf::test_helper::run_csaf21_tests; + use crate::csaf::validation::ValidationError; + use crate::csaf::validations::test_6_1_46::test_6_1_46_invalid_ssvc; + use std::collections::HashMap; + + #[test] + fn test_test_6_1_46() { + run_csaf21_tests( + "46", + test_6_1_46_invalid_ssvc, HashMap::from([ + ("01", &ValidationError { + message: "Invalid SSVC object: missing field `selections`".to_string(), + instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1".to_string(), + }), + ("02", &ValidationError { + message: "Invalid SSVC object: unknown field `value`, expected one of `name`, `namespace`, `values`, `version`".to_string(), + instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1".to_string(), + }), + ]) + ); + } +} diff --git a/csaf-lib/src/csaf/validations/test_6_1_47.rs b/csaf-lib/src/csaf/validations/test_6_1_47.rs new file mode 100644 index 0000000..89df8ae --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_47.rs @@ -0,0 +1,100 @@ +use std::ops::Deref; +use crate::csaf::getter_traits::{ContentTrait, CsafTrait, DocumentTrait, MetricTrait, TrackingTrait, VulnerabilityIdTrait, VulnerabilityTrait}; +use crate::csaf::validation::ValidationError; + +pub fn test_6_1_47_inconsistent_ssvc_id( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + let vulnerabilities = doc.get_vulnerabilities(); + + for (i_v, v) in vulnerabilities.iter().enumerate() { + if let Some(metrics) = v.get_metrics() { + for (i_m, m) in metrics.iter().enumerate() { + return match m.get_content().get_ssvc_v1() { + Ok(ssvc) => { + // Get the SSVC ID + let ssvc_id = ssvc.id.deref(); + + // Check if SSVC ID equals document ID + let document_id = doc.get_document().get_tracking().get_id(); + if ssvc_id == document_id { + // If there are multiple vulnerabilities, the validation must fail here. + if vulnerabilities.len() > 1 { + return Err(ValidationError { + message: format!("The SSVC ID equals the document ID '{}' and the document contains multiple vulnerabilities", document_id), + instance_path: format!("/vulnerabilities/{}/metrics/{}/content/ssvc_v1/id", i_v, i_m), + }); + } + // SSVC ID is valid, go to next metrics object + continue; + } + + // Check if it matches CVE + if let Some(cve) = v.get_cve() { + if ssvc_id == cve { + // SSVC ID is valid, go to next metrics object + continue; + } + } + + // Check if it matches any ID in ids array + if let Some(ids) = v.get_ids() { + if ids.iter().any(|id| id.get_text() == ssvc_id) { + // SSVC ID is valid, go to next metrics object + continue; + } + } + + // Return error if SSVC ID is not valid + Err(ValidationError { + message: format!("The SSVC ID '{}' does not match the document ID, the CVE ID or any ID in the IDs array of the vulnerability", ssvc_id), + instance_path: format!("/vulnerabilities/{}/metrics/{}/content/ssvc_v1/id", i_v, i_m), + }) + }, + Err(err) => Err(ValidationError { + message: format!("Invalid SSVC object: {}", err), + instance_path: format!("/vulnerabilities/{}/metrics/{}/content/ssvc_v1", i_v, i_m), + }), + } + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::csaf::test_helper::run_csaf21_tests; + use crate::csaf::validation::ValidationError; + use crate::csaf::validations::test_6_1_47::test_6_1_47_inconsistent_ssvc_id; + use std::collections::HashMap; + + #[test] + fn test_test_6_1_47() { + let instance_path = "/vulnerabilities/0/metrics/0/content/ssvc_v1/id".to_string(); + + run_csaf21_tests( + "47", + test_6_1_47_inconsistent_ssvc_id, + HashMap::from([ + ("01", &ValidationError { + message: "The SSVC ID 'CVE-1900-0002' does not match the document ID, the CVE ID or any ID in the IDs array of the vulnerability".to_string(), + instance_path: instance_path.clone(), + }), + ("02", &ValidationError { + message: "The SSVC ID 'CVE-1900-0001' does not match the document ID, the CVE ID or any ID in the IDs array of the vulnerability".to_string(), + instance_path: instance_path.clone(), + }), + ("03", &ValidationError { + message: "The SSVC ID '2723' does not match the document ID, the CVE ID or any ID in the IDs array of the vulnerability".to_string(), + instance_path: instance_path.clone(), + }), + ("04", &ValidationError { + message: "The SSVC ID 'Bug#2723' does not match the document ID, the CVE ID or any ID in the IDs array of the vulnerability".to_string(), + instance_path: instance_path.clone(), + }), + ]) + ); + } +} \ No newline at end of file diff --git a/csaf-lib/src/csaf/validations/test_6_1_48.rs b/csaf-lib/src/csaf/validations/test_6_1_48.rs new file mode 100644 index 0000000..225f6a9 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_48.rs @@ -0,0 +1,138 @@ +use crate::csaf::getter_traits::{ContentTrait, CsafTrait, MetricTrait, VulnerabilityTrait}; +use crate::csaf::validation::ValidationError; +use std::ops::Deref; +use crate::csaf::helpers::{SSVC_DECISION_POINTS, DP_VAL_LOOKUP, REGISTERED_SSVC_NAMESPACES}; + +pub fn test_6_1_48_ssvc_decision_points( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + let vulnerabilities = doc.get_vulnerabilities(); + + for (i_v, v) in vulnerabilities.iter().enumerate() { + if let Some(metrics) = v.get_metrics() { + for (i_m, m) in metrics.iter().enumerate() { + match m.get_content().get_ssvc_v1() { + Ok(ssvc) => { + for (i_s, selection) in ssvc.selections.iter().enumerate() { + // Skip this test for unregistered namespaces + if !REGISTERED_SSVC_NAMESPACES.contains(selection.namespace.deref()) { + continue; + } + + // Create the key for lookup in CSAF_SSVC_DECISION_POINTS + let (namespace, name, version) = ( + selection.namespace.deref().to_owned(), + selection.name.deref().to_owned(), + selection.version.deref().to_owned(), + ); + let dp_key = (namespace.clone(), name.clone(), version.clone()); + match SSVC_DECISION_POINTS.get(&dp_key) { + Some(_) => { + // Get value indices of decision point + let reference_indices = DP_VAL_LOOKUP.get(&dp_key).unwrap(); + // Index of last seen value + let mut last_index: i32 = -1; + // Check if all values exist and are correctly ordered + for (i_val, value) in selection.values.iter().map(|v| v.deref()).enumerate() { + match reference_indices.get(value) { + None => return Err(ValidationError { + message: format!( + "The SSVC decision point '{}::{}' (version {}) doesn't have the value '{}'", + namespace, name, version, value + ), + instance_path: format!( + "/vulnerabilities/{}/metrics/{}/content/ssvc_v1/selections/{}/values/{}", + i_v, i_m, i_s, i_val + ), + }), + Some(i_dp_val) => { + if last_index > *i_dp_val { + return Err(ValidationError { + message: format!( + "The values for SSVC decision point '{}::{}' (version {}) are not in correct order", + namespace, name, version + ), + instance_path: format!( + "/vulnerabilities/{}/metrics/{}/content/ssvc_v1/selections/{}/values/{}", + i_v, i_m, i_s, i_val + ), + }); + } else { + last_index = *i_dp_val; + } + } + } + } + }, + None => { + return Err(ValidationError { + message: format!( + "Unknown SSVC decision point '{}::{}' with version '{}'", + namespace, name, version + ), + instance_path: format!( + "/vulnerabilities/{}/metrics/{}/content/ssvc_v1/selections/{}", + i_v, i_m, i_s + ), + }); + } + } + } + }, + Err(err) => { + return Err(ValidationError { + message: format!("Invalid SSVC object: {}", err), + instance_path: format!("/vulnerabilities/{}/metrics/{}/content/ssvc_v1", i_v, i_m), + }); + }, + } + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::csaf::test_helper::run_csaf21_tests; + use crate::csaf::validation::ValidationError; + use crate::csaf::validations::test_6_1_48::test_6_1_48_ssvc_decision_points; + use std::collections::HashMap; + + #[test] + fn test_test_6_1_48() { + let instance_path = "/vulnerabilities/0/metrics/0/content/ssvc_v1/selections/0".to_string(); + + run_csaf21_tests( + "48", + test_6_1_48_ssvc_decision_points, + HashMap::from([ + ("01", &ValidationError { + message: "The SSVC decision point 'ssvc::Mission Impact' (version 1.0.0) doesn't have the value 'Degraded'".to_string(), + instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1/selections/0/values/1".to_string(), + }), + ("02", &ValidationError { + message: "Unknown SSVC decision point 'ssvc::Safety Impacts' with version '2.0.0'".to_string(), + instance_path: instance_path.clone(), + }), + ("03", &ValidationError { + message: "The values for SSVC decision point 'ssvc::Safety Impact' (version 2.0.0) are not in correct order".to_string(), + instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1/selections/0/values/1".to_string(), + }), + ("04", &ValidationError { + message: "Unknown SSVC decision point 'ssvc::Safety Impact' with version '1.9.7'".to_string(), + instance_path: instance_path.clone(), + }), + ("05", &ValidationError { + message: "The SSVC decision point 'cvss::Attack Complexity' (version 3.0.1) doesn't have the value 'Easy'".to_string(), + instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1/selections/0/values/0".to_string(), + }), + ("06", &ValidationError { + message: "The values for SSVC decision point 'cvss::Exploit Maturity' (version 2.0.0) are not in correct order".to_string(), + instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1/selections/0/values/1".to_string(), + }), + ]) + ); + } +} \ No newline at end of file diff --git a/csaf-lib/src/csaf/validations/test_6_1_49.rs b/csaf-lib/src/csaf/validations/test_6_1_49.rs new file mode 100644 index 0000000..ebd6f22 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_49.rs @@ -0,0 +1,111 @@ +use crate::csaf::csaf2_1::schema::DocumentStatus; +use crate::csaf::getter_traits::{ContentTrait, CsafTrait, DocumentTrait, MetricTrait, RevisionTrait, TrackingTrait, VulnerabilityTrait}; +use crate::csaf::validation::ValidationError; +use chrono::{DateTime, FixedOffset}; + +/// 6.1.49 Inconsistent SSVC Timestamp +/// +/// For each vulnerability, it is tested that the SSVC `timestamp` is earlier or equal to the `date` +/// of the newest item of the `revision_history` if the document status is `final` or `interim`. +pub fn test_6_1_49_inconsistent_ssvc_timestamp( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + let document = doc.get_document(); + let tracking = document.get_tracking(); + let status = tracking.get_status(); + + // Check if document status is "final" or "interim" + if status != DocumentStatus::Final && status != DocumentStatus::Interim { + return Ok(()); + } + + // Parse the date of each revision and find the newest one + let mut newest_revision_date: Option> = None; + for (i_r, revision) in tracking.get_revision_history().iter().enumerate() { + let date_str = revision.get_date(); + match DateTime::parse_from_rfc3339(date_str) { + Ok(date) => { + newest_revision_date = match newest_revision_date { + None => Some(date), + Some(newest_date) => Some(newest_date.max(date)) + }; + } + Err(_) => { + return Err(ValidationError { + message: format!("Invalid date format in revision history: {}", date_str), + instance_path: format!("/document/tracking/revision_history/{}/date", i_r), + }); + } + } + } + + let newest_revision_date = match newest_revision_date { + Some(date) => date, + // No entries in revision history + None => return Err(ValidationError { + message: "Revision history must not be empty for status final or interim".to_string(), + instance_path: "/document/tracking/revision_history".to_string(), + }), + }; + + // Check each vulnerability's SSVC timestamp + for (i_v, vulnerability) in doc.get_vulnerabilities().iter().enumerate() { + if let Some(metrics) = vulnerability.get_metrics() { + for (i_m, metric) in metrics.iter().enumerate() { + match metric.get_content().get_ssvc_v1() { + Ok(ssvc) => { + if ssvc.timestamp.fixed_offset() > newest_revision_date { + return Err(ValidationError { + message: format!( + "SSVC timestamp ({}) for vulnerability at index {} is later than the newest revision date ({})", + ssvc.timestamp.to_rfc3339(), i_v, newest_revision_date.to_rfc3339() + ), + instance_path: format!("/vulnerabilities/{}/metrics/{}/content/ssvc_v1/timestamp", i_v, i_m), + }) + } + }, + Err(err) => { + return Err(ValidationError { + message: format!("Invalid SSVC object: {}", err), + instance_path: format!("/vulnerabilities/{}/metrics/{}/content/ssvc_v1", i_v, i_m), + }); + }, + } + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::csaf::test_helper::run_csaf21_tests; + use crate::csaf::validation::ValidationError; + use crate::csaf::validations::test_6_1_49::test_6_1_49_inconsistent_ssvc_timestamp; + use std::collections::HashMap; + + #[test] + fn test_test_6_1_49() { + let instance_path = "/vulnerabilities/0/metrics/0/content/ssvc_v1/timestamp".to_string(); + + run_csaf21_tests( + "49", + test_6_1_49_inconsistent_ssvc_timestamp, + HashMap::from([ + ("01", &ValidationError { + message: "SSVC timestamp (2024-07-13T10:00:00+00:00) for vulnerability at index 0 is later than the newest revision date (2024-01-24T10:00:00+00:00)".to_string(), + instance_path: instance_path.clone(), + }), + ("02", &ValidationError { + message: "SSVC timestamp (2024-02-29T10:30:00+00:00) for vulnerability at index 0 is later than the newest revision date (2024-02-29T10:00:00+00:00)".to_string(), + instance_path: instance_path.clone(), + }), + ("03", &ValidationError { + message: "SSVC timestamp (2024-02-29T10:30:00+00:00) for vulnerability at index 0 is later than the newest revision date (2024-02-29T10:00:00+00:00)".to_string(), + instance_path: instance_path.clone(), + }), + ]) + ); + } +} \ No newline at end of file diff --git a/ssvc b/ssvc new file mode 160000 index 0000000..6557e21 --- /dev/null +++ b/ssvc @@ -0,0 +1 @@ +Subproject commit 6557e21cac6951704b28034e9f137bde8f4049d5