From 9b40dcfd7a17970f73ff401715b443111c3a3d33 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Mon, 7 Apr 2025 17:09:10 +0200 Subject: [PATCH 01/17] Add test_6_1_43 to validate model numbers for unescaped stars Introduces a new validation function `test_6_1_43` to ensure model numbers in the CSAF document do not contain multiple unescaped asterisks. Includes corresponding unit tests to verify compliance with the requirement. --- csaf-lib/src/csaf/validations/mod.rs | 1 + csaf-lib/src/csaf/validations/test_6_1_43.rs | 50 ++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 csaf-lib/src/csaf/validations/test_6_1_43.rs diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index 52fd5da..3ba99e3 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -9,3 +9,4 @@ 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; 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), + ]) + ); + } +} From 74383c1005bf9cd2aaf46dbdbf19cd5cb478f9a8 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Mon, 7 Apr 2025 17:13:10 +0200 Subject: [PATCH 02/17] Add validation for multiple unescaped stars in serial numbers Introduce a new rule, test_6_1_44, to enforce restrictions on serial numbers in the CSAF document. The validation ensures serial numbers do not contain multiple unescaped asterisks and includes corresponding unit tests for compliance. --- csaf-lib/src/csaf/validations/mod.rs | 1 + csaf-lib/src/csaf/validations/test_6_1_44.rs | 50 ++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 csaf-lib/src/csaf/validations/test_6_1_44.rs diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index 3ba99e3..31b81c5 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -10,3 +10,4 @@ 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; 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), + ]) + ); + } +} From 7846c9a65206c83e2d52b51d8515fbd86b9ea9a9 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Wed, 9 Apr 2025 15:03:15 +0200 Subject: [PATCH 03/17] Implement test 6.1.45, replace 'release_date' with 'disclosure_date' Updated the schema and related code, aligning with standard conventions. Added a new validation (test_6_1_45) to ensure disclosure dates are consistent with revision history. Adjusted logic, getters, and test cases accordingly. --- .../csaf/csaf2_0/getter_implementations.rs | 2 +- .../src/csaf/csaf2_1/csaf_json_schema.json | 17 +-- .../csaf/csaf2_1/getter_implementations.rs | 4 +- csaf-lib/src/csaf/csaf2_1/schema.rs | 110 ++++++++++++------ csaf-lib/src/csaf/getter_traits.rs | 2 +- csaf-lib/src/csaf/validations/mod.rs | 1 + csaf-lib/src/csaf/validations/test_6_1_37.rs | 12 +- csaf-lib/src/csaf/validations/test_6_1_45.rs | 105 +++++++++++++++++ 8 files changed, 199 insertions(+), 54 deletions(-) create mode 100644 csaf-lib/src/csaf/validations/test_6_1_45.rs diff --git a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs index 4c585c8..b414fc4 100644 --- a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs @@ -120,7 +120,7 @@ impl VulnerabilityTrait for Vulnerability { &self.threats } - fn get_release_date(&self) -> &Option { + fn get_disclosure_date(&self) -> &Option { &self.release_date } 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..be97023 100644 --- a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs @@ -95,8 +95,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 { 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/getter_traits.rs b/csaf-lib/src/csaf/getter_traits.rs index 368fab7..098be8d 100644 --- a/csaf-lib/src/csaf/getter_traits.rs +++ b/csaf-lib/src/csaf/getter_traits.rs @@ -157,7 +157,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; diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index 31b81c5..5e359a9 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -11,3 +11,4 @@ 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; 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..b6d5160 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_37.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_37.rs @@ -45,8 +45,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 @@ -136,12 +136,12 @@ 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_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), + ]) + ); + } +} From 64be9c7339eb5e81c05286a00200e029467fb6fd Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Mon, 14 Apr 2025 10:26:52 +0200 Subject: [PATCH 04/17] Add SSVC 1.0.1 schema and build integration This commit introduces the SSVC 1.0.1 schema definition, including JSON schema validation and data structure implementation. It integrates the new schema into the build system and ensures that the datetime handling is configurable when processing schemas. --- csaf-lib/build.rs | 17 +- csaf-lib/src/csaf/csaf2_1/mod.rs | 1 + .../csaf2_1/ssvc-1-0-1-merged.schema.json | 103 ++ csaf-lib/src/csaf/csaf2_1/ssvc_schema.rs | 890 ++++++++++++++++++ 4 files changed, 1007 insertions(+), 4 deletions(-) create mode 100644 csaf-lib/src/csaf/csaf2_1/ssvc-1-0-1-merged.schema.json create mode 100644 csaf-lib/src/csaf/csaf2_1/ssvc_schema.rs diff --git a/csaf-lib/build.rs b/csaf-lib/build.rs index 6538f4d..5b3cc41 100644 --- a/csaf-lib/build.rs +++ b/csaf-lib/build.rs @@ -21,21 +21,30 @@ 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, )?; 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_1/mod.rs b/csaf-lib/src/csaf/csaf2_1/mod.rs index 6f7125d..a86176e 100644 --- a/csaf-lib/src/csaf/csaf2_1/mod.rs +++ b/csaf-lib/src/csaf/csaf2_1/mod.rs @@ -2,3 +2,4 @@ pub mod loader; pub mod schema; pub mod validation; pub mod getter_implementations; +pub mod ssvc_schema; 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..db96af6 --- /dev/null +++ b/csaf-lib/src/csaf/csaf2_1/ssvc-1-0-1-merged.schema.json @@ -0,0 +1,103 @@ +{ + "$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.", + "$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_schema.rs b/csaf-lib/src/csaf/csaf2_1/ssvc_schema.rs new file mode 100644 index 0000000..2436dd8 --- /dev/null +++ b/csaf-lib/src/csaf/csaf2_1/ssvc_schema.rs @@ -0,0 +1,890 @@ +/// 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() + } +} +///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 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), + } + } + } +} From 3eeb77a0481eb89f9b0d3d0a851556bb86719ac1 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Mon, 14 Apr 2025 16:15:52 +0200 Subject: [PATCH 05/17] Add test 6.1.46 (validation for SSVC objects in vulnerabilities) Introduced a validation function `test_6_1_46_invalid_ssvc` to ensure proper SSVC object structure in vulnerabilities' metrics. Updated schemas, traits, and implementations to support SSVC content validation. --- .../csaf/csaf2_0/getter_implementations.rs | 16 +- .../csaf/csaf2_1/getter_implementations.rs | 19 +- .../csaf2_1/ssvc-1-0-1-merged.schema.json | 1 + csaf-lib/src/csaf/csaf2_1/ssvc_schema.rs | 178 ++++++++++++++++++ csaf-lib/src/csaf/getter_traits.rs | 9 + csaf-lib/src/csaf/validations/mod.rs | 1 + csaf-lib/src/csaf/validations/test_6_1_46.rs | 47 +++++ 7 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 csaf-lib/src/csaf/validations/test_6_1_46.rs diff --git a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs index b414fc4..d5a0794 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_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}; 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 { diff --git a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs index be97023..a06f1ad 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, 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}; 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 { 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 index db96af6..6fa7c5b 100644 --- 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 @@ -2,6 +2,7 @@ "$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.", diff --git a/csaf-lib/src/csaf/csaf2_1/ssvc_schema.rs b/csaf-lib/src/csaf/csaf2_1/ssvc_schema.rs index 2436dd8..87cad8d 100644 --- a/csaf-lib/src/csaf/csaf2_1/ssvc_schema.rs +++ b/csaf-lib/src/csaf/csaf2_1/ssvc_schema.rs @@ -273,6 +273,71 @@ impl ::std::convert::TryFrom<::std::string::String> for SchemaVersion { 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 @@ -785,6 +850,119 @@ impl<'de> ::serde::Deserialize<'de> for ValuesItem { } /// 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< diff --git a/csaf-lib/src/csaf/getter_traits.rs b/csaf-lib/src/csaf/getter_traits.rs index 098be8d..12b68e8 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; @@ -310,8 +311,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/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index 5e359a9..b0c9bd1 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -12,3 +12,4 @@ 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; 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..3351178 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_46.rs @@ -0,0 +1,47 @@ +use crate::csaf::getter_traits::{ContentTrait, CsafTrait, DistributionTrait, DocumentTrait, MetricTrait, RevisionTrait, TlpTrait, TrackingTrait, 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(), + }), + ]) + ); + } +} From 232514b7f45abbac8627d34c71b6e8ff3d85cae7 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Mon, 14 Apr 2025 18:11:53 +0200 Subject: [PATCH 06/17] Add SSVC validation for inconsistent IDs (test 6.1.47) Implemented a new validation function, `test_6_1_47_inconsistent_ssvc_id`, to ensure SSVC IDs in the document adhere to expected constraints. This includes checks against the document ID, CVE, and vulnerability IDs. Updated necessary traits and getter implementations to support this functionality. --- csaf | 2 +- .../csaf/csaf2_0/getter_implementations.rs | 26 ++++- .../csaf/csaf2_1/getter_implementations.rs | 27 ++++- csaf-lib/src/csaf/getter_traits.rs | 22 ++++- csaf-lib/src/csaf/validations/mod.rs | 1 + csaf-lib/src/csaf/validations/test_6_1_46.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_47.rs | 98 +++++++++++++++++++ 7 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 csaf-lib/src/csaf/validations/test_6_1_47.rs diff --git a/csaf b/csaf index 65f402d..70bb8a4 160000 --- a/csaf +++ b/csaf @@ -1 +1 @@ -Subproject commit 65f402d7d5298b1957ce61f1e175c755069214ae +Subproject commit 70bb8a4ff9a6c23efd0723f7255d07c9f4e5ff7f diff --git a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs index d5a0794..185f16b 100644 --- a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs @@ -1,6 +1,6 @@ -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, ContentTrait}; +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; @@ -116,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 @@ -149,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 { @@ -288,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/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs index a06f1ad..d23c189 100644 --- a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs @@ -1,5 +1,5 @@ -use crate::csaf::csaf2_1::schema::{Branch, CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, Content, 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, ContentTrait}; +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; @@ -93,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 @@ -125,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 { @@ -234,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/getter_traits.rs b/csaf-lib/src/csaf/getter_traits.rs index 12b68e8..2c8d0e5 100644 --- a/csaf-lib/src/csaf/getter_traits.rs +++ b/csaf-lib/src/csaf/getter_traits.rs @@ -101,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 @@ -139,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; @@ -168,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 diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index b0c9bd1..a0d93e7 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -13,3 +13,4 @@ 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; diff --git a/csaf-lib/src/csaf/validations/test_6_1_46.rs b/csaf-lib/src/csaf/validations/test_6_1_46.rs index 3351178..5e4cb75 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_46.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_46.rs @@ -1,4 +1,4 @@ -use crate::csaf::getter_traits::{ContentTrait, CsafTrait, DistributionTrait, DocumentTrait, MetricTrait, RevisionTrait, TlpTrait, TrackingTrait, VulnerabilityTrait}; +use crate::csaf::getter_traits::{ContentTrait, CsafTrait, MetricTrait, VulnerabilityTrait}; use crate::csaf::validation::ValidationError; pub fn test_6_1_46_invalid_ssvc( 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..aa676ca --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_47.rs @@ -0,0 +1,98 @@ +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), + }); + } + // Go to next metrics object + continue; + } + + // Check if it matches CVE + if let Some(cve) = v.get_cve() { + if ssvc_id == cve { + 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) { + 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 From 75045b1dc964594b69cc832080c8c69b4ba44567 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Tue, 15 Apr 2025 08:48:15 +0200 Subject: [PATCH 07/17] Improved comments --- csaf-lib/src/csaf/validations/test_6_1_47.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/csaf-lib/src/csaf/validations/test_6_1_47.rs b/csaf-lib/src/csaf/validations/test_6_1_47.rs index aa676ca..89df8ae 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_47.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_47.rs @@ -25,13 +25,14 @@ pub fn test_6_1_47_inconsistent_ssvc_id( instance_path: format!("/vulnerabilities/{}/metrics/{}/content/ssvc_v1/id", i_v, i_m), }); } - // Go to next metrics object + // 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; } } @@ -39,6 +40,7 @@ pub fn test_6_1_47_inconsistent_ssvc_id( // 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; } } From f56bd0d4a095ea510d23eddfad45d1f7143edd50 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Tue, 15 Apr 2025 09:51:52 +0200 Subject: [PATCH 08/17] Pulled fixed test (csaf submodule) --- csaf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csaf b/csaf index 70bb8a4..500de13 160000 --- a/csaf +++ b/csaf @@ -1 +1 @@ -Subproject commit 70bb8a4ff9a6c23efd0723f7255d07c9f4e5ff7f +Subproject commit 500de13452015136756e64ae03f84bdc9fd6405b From 0c0d65dfadd11869c42590401509a28be2083513 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Tue, 15 Apr 2025 18:44:28 +0200 Subject: [PATCH 09/17] Add SSVC decision point validation and schema support Introduce validation for SSVC decision points in CSAF documents, ensuring correctness and order of values. Added the corresponding schema definition for decision points for better type safety and compliance. Refactored to use `LazyLock` for cleaner regex handling. --- .gitmodules | 4 + csaf-lib/build.rs | 5 + csaf-lib/src/csaf/csaf2_1/mod.rs | 1 + csaf-lib/src/csaf/csaf2_1/ssvc_dp_schema.rs | 1216 ++++++++++++++++++ csaf-lib/src/csaf/validations/mod.rs | 1 + csaf-lib/src/csaf/validations/test_6_1_37.rs | 14 +- csaf-lib/src/csaf/validations/test_6_1_48.rs | 183 +++ ssvc | 1 + 8 files changed, 1416 insertions(+), 9 deletions(-) create mode 100644 csaf-lib/src/csaf/csaf2_1/ssvc_dp_schema.rs create mode 100644 csaf-lib/src/csaf/validations/test_6_1_48.rs create mode 160000 ssvc 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-lib/build.rs b/csaf-lib/build.rs index 5b3cc41..48c05e9 100644 --- a/csaf-lib/build.rs +++ b/csaf-lib/build.rs @@ -33,6 +33,11 @@ fn main() -> Result<(), BuildError> { "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(()) } diff --git a/csaf-lib/src/csaf/csaf2_1/mod.rs b/csaf-lib/src/csaf/csaf2_1/mod.rs index a86176e..b91c9c1 100644 --- a/csaf-lib/src/csaf/csaf2_1/mod.rs +++ b/csaf-lib/src/csaf/csaf2_1/mod.rs @@ -3,3 +3,4 @@ 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/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/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index a0d93e7..fb232c8 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -14,3 +14,4 @@ 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; 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 b6d5160..4063d72 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). @@ -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 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..f04c685 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_48.rs @@ -0,0 +1,183 @@ +use crate::csaf::csaf2_1::ssvc_dp_schema::DecisionPoint; +use crate::csaf::getter_traits::{ContentTrait, CsafTrait, DocumentTrait, MetricTrait, TrackingTrait, VulnerabilityIdTrait, VulnerabilityTrait}; +use crate::csaf::validation::ValidationError; +use glob::glob; +use std::collections::HashMap; +use std::fs; +use std::ops::Deref; +use std::sync::LazyLock; + +/// 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. +static CSAF_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 + if let Ok(paths) = glob("../ssvc/data/json/decision_points/**/*.json") { + for path in paths.filter_map(Result::ok) { + if let Ok(content) = fs::read_to_string(&path) { + if let Ok(dp) = serde_json::from_str::(&content) { + println!("Loaded SSVC decision point '{}' (version {})", dp.name.deref(), dp.version.deref()); + // Insert using (name, key) tuple as the key + let key = (dp.name.deref().to_owned(), dp.version.deref().to_owned()); + decision_points.insert(key, dp); + } + } + } + } + + decision_points +}); + +static DP_VAL_LOOKUP: LazyLock>> = LazyLock::new(|| { + let mut lookups = HashMap::new(); + + for (key, dp) in CSAF_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 +}); + +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() { + // Create the key for lookup in CSAF_SSVC_DECISION_POINTS + let (name, version) = (selection.name.deref().to_owned(), selection.version.deref().to_owned()); + let dp_key = (name.clone(), version.clone()); + match CSAF_SSVC_DECISION_POINTS.get(&dp_key) { + Some(dp) => { + // Decision point exists, check namespace + if dp.namespace.deref() != selection.namespace.deref() { + return Err(ValidationError { + message: format!( + "The selection has a namespace ({}) that differs from the SSVC decision point '{}' (version {}) namespace ({})", + selection.namespace.deref(), name, version, dp.namespace.deref() + ), + instance_path: format!( + "/vulnerabilities/{}/metrics/{}/content/ssvc_v1/selections/{}", + i_v, i_m, i_s + ), + }) + } + + // 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 '{}'", + 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", + 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 '{}'", + 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 '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 'Safety Impacts' with version '1.0.0'".to_string(), + instance_path: instance_path.clone(), + }), + ("03", &ValidationError { + message: "The SSVC decision point 'Safety Impact' (version 1.0.0) doesn't have the value 'Critical'".to_string(), + instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1/selections/0/values/1".to_string(), + }), + ("04", &ValidationError { + message: "Unknown SSVC decision point 'Safety Impact' with version '1.9.7'".to_string(), + instance_path: instance_path.clone(), + }), + ("05", &ValidationError { + message: "The SSVC decision point '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 '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/ssvc b/ssvc new file mode 160000 index 0000000..6557e21 --- /dev/null +++ b/ssvc @@ -0,0 +1 @@ +Subproject commit 6557e21cac6951704b28034e9f137bde8f4049d5 From a8623e1b407ba9483d09bba01b01a2d0e0e79e33 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Wed, 16 Apr 2025 09:00:35 +0200 Subject: [PATCH 10/17] Refactor SSVC decision point handling for reuse and clarity Moved SSVC decision point initialization to a helper module for better modularity and reuse. Simplified `test_6_1_48` logic by leveraging the centralized `CSAF_SSVC_DECISION_POINTS` and `DP_VAL_LOOKUP` structures. --- csaf-lib/src/csaf/helpers.rs | 54 +++++++++++++++++++- csaf-lib/src/csaf/validations/test_6_1_48.rs | 44 +--------------- 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/csaf-lib/src/csaf/helpers.rs b/csaf-lib/src/csaf/helpers.rs index cf0ec7f..bfc9307 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}; +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,50 @@ 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 CSAF_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 in paths.filter_map(Result::ok) { + 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.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(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 CSAF_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 +}); \ 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 index f04c685..bae73a5 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_48.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_48.rs @@ -1,47 +1,7 @@ -use crate::csaf::csaf2_1::ssvc_dp_schema::DecisionPoint; -use crate::csaf::getter_traits::{ContentTrait, CsafTrait, DocumentTrait, MetricTrait, TrackingTrait, VulnerabilityIdTrait, VulnerabilityTrait}; +use crate::csaf::getter_traits::{ContentTrait, CsafTrait, MetricTrait, VulnerabilityTrait}; use crate::csaf::validation::ValidationError; -use glob::glob; -use std::collections::HashMap; -use std::fs; use std::ops::Deref; -use std::sync::LazyLock; - -/// 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. -static CSAF_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 - if let Ok(paths) = glob("../ssvc/data/json/decision_points/**/*.json") { - for path in paths.filter_map(Result::ok) { - if let Ok(content) = fs::read_to_string(&path) { - if let Ok(dp) = serde_json::from_str::(&content) { - println!("Loaded SSVC decision point '{}' (version {})", dp.name.deref(), dp.version.deref()); - // Insert using (name, key) tuple as the key - let key = (dp.name.deref().to_owned(), dp.version.deref().to_owned()); - decision_points.insert(key, dp); - } - } - } - } - - decision_points -}); - -static DP_VAL_LOOKUP: LazyLock>> = LazyLock::new(|| { - let mut lookups = HashMap::new(); - - for (key, dp) in CSAF_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 -}); +use crate::csaf::helpers::{CSAF_SSVC_DECISION_POINTS, DP_VAL_LOOKUP}; pub fn test_6_1_48_ssvc_decision_points( doc: &impl CsafTrait, From fc985fcffa031ed8b84c3b0d246ff1ba862b0911 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Wed, 16 Apr 2025 10:34:53 +0200 Subject: [PATCH 11/17] Update SSVC decision point handling to include namespaces The decision point keys now incorporate a namespace component along with name and version, ensuring more precise identification and preventing potential conflicts. Adjusted related validation messages and logic to reflect the updated structure. --- csaf-lib/src/csaf/helpers.rs | 37 ++++++++------ csaf-lib/src/csaf/validations/test_6_1_48.rs | 52 ++++++++------------ 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/csaf-lib/src/csaf/helpers.rs b/csaf-lib/src/csaf/helpers.rs index bfc9307..dd249ad 100644 --- a/csaf-lib/src/csaf/helpers.rs +++ b/csaf-lib/src/csaf/helpers.rs @@ -49,26 +49,35 @@ pub fn count_unescaped_stars(s: &str) -> u32 { /// 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 CSAF_SSVC_DECISION_POINTS: LazyLock> = LazyLock::new(|| { +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 in paths.filter_map(Result::ok) { - 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.name.deref().to_owned(), dp.version.deref().to_owned()); - decision_points.insert(key, dp); + 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 parse decision point from file {:?}: {}", path, err), + Err(err) => eprintln!("Warning: Failed to read 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), } } }, @@ -80,10 +89,10 @@ pub static CSAF_SSVC_DECISION_POINTS: LazyLock>> = LazyLock::new(|| { +pub static DP_VAL_LOOKUP: LazyLock>> = LazyLock::new(|| { let mut lookups = HashMap::new(); - for (key, dp) in CSAF_SSVC_DECISION_POINTS.iter() { + 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); diff --git a/csaf-lib/src/csaf/validations/test_6_1_48.rs b/csaf-lib/src/csaf/validations/test_6_1_48.rs index bae73a5..60da59f 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_48.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_48.rs @@ -1,7 +1,7 @@ use crate::csaf::getter_traits::{ContentTrait, CsafTrait, MetricTrait, VulnerabilityTrait}; use crate::csaf::validation::ValidationError; use std::ops::Deref; -use crate::csaf::helpers::{CSAF_SSVC_DECISION_POINTS, DP_VAL_LOOKUP}; +use crate::csaf::helpers::{SSVC_DECISION_POINTS, DP_VAL_LOOKUP}; pub fn test_6_1_48_ssvc_decision_points( doc: &impl CsafTrait, @@ -15,24 +15,14 @@ pub fn test_6_1_48_ssvc_decision_points( Ok(ssvc) => { for (i_s, selection) in ssvc.selections.iter().enumerate() { // Create the key for lookup in CSAF_SSVC_DECISION_POINTS - let (name, version) = (selection.name.deref().to_owned(), selection.version.deref().to_owned()); - let dp_key = (name.clone(), version.clone()); - match CSAF_SSVC_DECISION_POINTS.get(&dp_key) { - Some(dp) => { - // Decision point exists, check namespace - if dp.namespace.deref() != selection.namespace.deref() { - return Err(ValidationError { - message: format!( - "The selection has a namespace ({}) that differs from the SSVC decision point '{}' (version {}) namespace ({})", - selection.namespace.deref(), name, version, dp.namespace.deref() - ), - instance_path: format!( - "/vulnerabilities/{}/metrics/{}/content/ssvc_v1/selections/{}", - i_v, i_m, i_s - ), - }) - } - + 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 @@ -42,8 +32,8 @@ pub fn test_6_1_48_ssvc_decision_points( match reference_indices.get(value) { None => return Err(ValidationError { message: format!( - "The SSVC decision point '{}' (version {}) doesn't have the value '{}'", - name, version, value + "The SSVC decision point '{}::{}' (version {}) doesn't have the value '{}'", + namespace, name, version, value ), instance_path: format!( "/vulnerabilities/{}/metrics/{}/content/ssvc_v1/selections/{}/values/{}", @@ -54,8 +44,8 @@ pub fn test_6_1_48_ssvc_decision_points( if last_index > *i_dp_val { return Err(ValidationError { message: format!( - "The values for SSVC decision point '{}' (version {}) are not in correct order", - name, version + "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/{}", @@ -72,8 +62,8 @@ pub fn test_6_1_48_ssvc_decision_points( None => { return Err(ValidationError { message: format!( - "Unknown SSVC decision point '{}' with version '{}'", - name, version + "Unknown SSVC decision point '{}::{}' with version '{}'", + namespace, name, version ), instance_path: format!( "/vulnerabilities/{}/metrics/{}/content/ssvc_v1/selections/{}", @@ -114,27 +104,27 @@ mod tests { test_6_1_48_ssvc_decision_points, HashMap::from([ ("01", &ValidationError { - message: "The SSVC decision point 'Mission Impact' (version 1.0.0) doesn't have the value 'Degraded'".to_string(), + 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 'Safety Impacts' with version '1.0.0'".to_string(), + message: "Unknown SSVC decision point 'ssvc::Safety Impacts' with version '1.0.0'".to_string(), instance_path: instance_path.clone(), }), ("03", &ValidationError { - message: "The SSVC decision point 'Safety Impact' (version 1.0.0) doesn't have the value 'Critical'".to_string(), + message: "The SSVC decision point 'ssvc::Safety Impact' (version 1.0.0) doesn't have the value 'Critical'".to_string(), instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1/selections/0/values/1".to_string(), }), ("04", &ValidationError { - message: "Unknown SSVC decision point 'Safety Impact' with version '1.9.7'".to_string(), + 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 'Attack Complexity' (version 3.0.1) doesn't have the value 'Easy'".to_string(), + 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 'Exploit Maturity' (version 2.0.0) are not in correct order".to_string(), + 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(), }), ]) From 032d0176ae039c11cb4a2d0d5dc4d54ab0f1e675 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Wed, 16 Apr 2025 12:20:45 +0200 Subject: [PATCH 12/17] Add validation test for SSVC timestamp consistency Implement test 6.1.49 to ensure that SSVC timestamps are earlier or equal to the newest revision date for documents with "final" or "interim" status. --- csaf-lib/src/csaf/validations/mod.rs | 1 + csaf-lib/src/csaf/validations/test_6_1_49.rs | 111 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 csaf-lib/src/csaf/validations/test_6_1_49.rs diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index fb232c8..d295fcf 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -15,3 +15,4 @@ 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_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 From e306c8f72c1c94cae90707339aad03f087559909 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Wed, 16 Apr 2025 14:35:46 +0200 Subject: [PATCH 13/17] Add namespace validation for SSVC decision points Introduce a `REGISTERED_SSVC_NAMESPACES` static set to track valid namespaces for SSVC. This ensures that unregistered namespaces are skipped during validation in `test_6_1_48_ssvc_decision_points`. --- csaf-lib/src/csaf/helpers.rs | 14 +++++++++++++- csaf-lib/src/csaf/validations/test_6_1_48.rs | 7 ++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/csaf-lib/src/csaf/helpers.rs b/csaf-lib/src/csaf/helpers.rs index dd249ad..48f9aee 100644 --- a/csaf-lib/src/csaf/helpers.rs +++ b/csaf-lib/src/csaf/helpers.rs @@ -1,5 +1,5 @@ use crate::csaf::getter_traits::{CsafTrait, ProductGroupTrait, ProductTreeTrait}; -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::fs; use std::ops::Deref; use std::sync::LazyLock; @@ -101,4 +101,16 @@ pub static DP_VAL_LOOKUP: 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/test_6_1_48.rs b/csaf-lib/src/csaf/validations/test_6_1_48.rs index 60da59f..458a985 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_48.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_48.rs @@ -1,7 +1,7 @@ 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}; +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, @@ -14,6 +14,11 @@ pub fn test_6_1_48_ssvc_decision_points( 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(), From d9064cf0c149d8dd5ab280edc33b15afd305c88f Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 25 Apr 2025 12:15:26 +0200 Subject: [PATCH 14/17] Updated CSAF repo --- csaf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csaf b/csaf index 500de13..8f94b00 160000 --- a/csaf +++ b/csaf @@ -1 +1 @@ -Subproject commit 500de13452015136756e64ae03f84bdc9fd6405b +Subproject commit 8f94b009b8909c4ab286f1dc8a467f7175669a97 From 5954f271c8d277a24599564eb51dc2eaae0bdd4e Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 25 Apr 2025 22:13:24 +0200 Subject: [PATCH 15/17] Fix test assumption --- csaf-lib/src/csaf/validations/test_6_1_48.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csaf-lib/src/csaf/validations/test_6_1_48.rs b/csaf-lib/src/csaf/validations/test_6_1_48.rs index 458a985..1fa8606 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_48.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_48.rs @@ -113,7 +113,7 @@ mod tests { 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 '1.0.0'".to_string(), + message: "Unknown SSVC decision point 'ssvc::Safety Impacts' with version '2.0.0'".to_string(), instance_path: instance_path.clone(), }), ("03", &ValidationError { From d47f09a1bdf5ffc8a22d703ee2bbf693058f41d7 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 25 Apr 2025 22:17:37 +0200 Subject: [PATCH 16/17] Fixed test assumption --- csaf-lib/src/csaf/validations/test_6_1_48.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csaf-lib/src/csaf/validations/test_6_1_48.rs b/csaf-lib/src/csaf/validations/test_6_1_48.rs index 1fa8606..225f6a9 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_48.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_48.rs @@ -117,7 +117,7 @@ mod tests { instance_path: instance_path.clone(), }), ("03", &ValidationError { - message: "The SSVC decision point 'ssvc::Safety Impact' (version 1.0.0) doesn't have the value 'Critical'".to_string(), + 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 { From 9e89a4eb3b09fda4feaba186d86f45602cfbdb17 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 30 Apr 2025 13:10:01 +0200 Subject: [PATCH 17/17] Ignoring 6.1.37 --- csaf | 2 +- csaf-lib/src/csaf/validations/test_6_1_37.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/csaf b/csaf index 8f94b00..62fb87c 160000 --- a/csaf +++ b/csaf @@ -1 +1 @@ -Subproject commit 8f94b009b8909c4ab286f1dc8a467f7175669a97 +Subproject commit 62fb87c6b6d0754bf0a4df20fc9e3d387497d0fe 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 4063d72..640896b 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_37.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_37.rs @@ -114,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( @@ -141,5 +143,5 @@ mod tests { }), ]) ); - } + }*/ }