diff --git a/csaf b/csaf index ed6e732..b4d5053 160000 --- a/csaf +++ b/csaf @@ -1 +1 @@ -Subproject commit ed6e7320fb8cd7819df44638c1cb3c4803bd3c55 +Subproject commit b4d505309ad069e5677142e2a3f1e17fba28be79 diff --git a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs new file mode 100644 index 0000000..75dfb57 --- /dev/null +++ b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs @@ -0,0 +1,74 @@ +use crate::csaf::csaf2_0::schema::{CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, ProductGroup, ProductTree, Remediation, Vulnerability}; +use crate::csaf::csaf2_1::schema::CategoryOfTheRemediation as Remediation21; +use crate::csaf::getter_traits::{CsafTrait, ProductGroupTrait, ProductTreeTrait, RemediationTrait, VulnerabilityTrait}; +use std::ops::Deref; + +impl RemediationTrait for Remediation { + + /// Normalizes the remediation categories from CSAF 2.0 to those of CSAF 2.1. + /// + /// # Explanation + /// In CSAF 2.1, the list of remediation categories was expanded, making it a superset of those + /// in CSAF 2.0. This function ensures that the remediation category from a CSAF 2.0 remediation + /// object is converted into the corresponding category defined in CSAF 2.1. + /// + /// # Returns + /// A CSAF 2.1 `CategoryOfTheRemediation` that corresponds to the remediation category of the + /// current object. + fn get_category(&self) -> Remediation21 { + match self.category { + CategoryOfTheRemediation::Workaround => Remediation21::Workaround, + CategoryOfTheRemediation::Mitigation => Remediation21::Mitigation, + CategoryOfTheRemediation::VendorFix => Remediation21::VendorFix, + CategoryOfTheRemediation::NoFixPlanned => Remediation21::NoFixPlanned, + CategoryOfTheRemediation::NoneAvailable => Remediation21::NoneAvailable, + } + } + + fn get_product_ids(&self) -> Option> { + self.product_ids.as_ref().map(|p| (*p).iter().map(|x| x.deref()).collect()) + } + + fn get_group_ids(&self) -> Option> { + self.group_ids.as_ref().map(|g| (*g).iter().map(|x| x.deref()).collect()) + } +} + +impl VulnerabilityTrait for Vulnerability { + type RemediationType = Remediation; + + fn get_remediations(&self) -> Vec { + self.remediations.clone() + } +} + +impl CsafTrait for CommonSecurityAdvisoryFramework { + type VulnerabilityType = Vulnerability; + type ProductTreeType = ProductTree; + + fn get_product_tree(&self) -> Option { + self.product_tree.clone() + } + + fn get_vulnerabilities(&self) -> Vec { + self.vulnerabilities.clone() + } +} + +impl ProductTreeTrait for ProductTree { + type ProductGroupType = ProductGroup; + + fn get_product_groups(&self) -> Vec { + self.product_groups.clone() + } +} + +impl ProductGroupTrait for ProductGroup { + fn get_group_id(&self) -> &String { + self.group_id.deref() + } + + fn get_product_ids(&self) -> Vec<&String> { + self.product_ids.iter().map(|x| x.deref()).collect() + } +} \ No newline at end of file diff --git a/csaf-lib/src/csaf/csaf2_0/mod.rs b/csaf-lib/src/csaf/csaf2_0/mod.rs index bf1a005..b747560 100644 --- a/csaf-lib/src/csaf/csaf2_0/mod.rs +++ b/csaf-lib/src/csaf/csaf2_0/mod.rs @@ -2,3 +2,4 @@ pub mod loader; mod product_helper; pub mod schema; pub mod validation; +pub mod getter_implementations; diff --git a/csaf-lib/src/csaf/csaf2_0/validation.rs b/csaf-lib/src/csaf/csaf2_0/validation.rs index 8a6da0a..95af665 100644 --- a/csaf-lib/src/csaf/csaf2_0/validation.rs +++ b/csaf-lib/src/csaf/csaf2_0/validation.rs @@ -6,19 +6,25 @@ use crate::csaf::helpers::find_duplicates; impl Validatable for CommonSecurityAdvisoryFramework { fn presets(&self) -> HashMap> { + let basic_tests = Vec::from(["6.1.1", "6.1.2"]); + // More tests may be added in extend() here later + let extended_tests: Vec<&str> = basic_tests.clone(); + // extended_tests.extend(["foo"].iter()); + let full_tests: Vec<&str> = extended_tests.clone(); + // full_tests.extend(["bar"].iter()); HashMap::from([ - (ValidationPreset::Basic, Vec::from(["6.1.1", "6.1.2"])), - (ValidationPreset::Extended, Vec::from(["6.1.1", "6.1.2"])), - (ValidationPreset::Full, Vec::from(["6.1.1", "6.1.2"])), + (ValidationPreset::Basic, basic_tests), + (ValidationPreset::Extended, extended_tests), + (ValidationPreset::Full, full_tests), ]) } fn tests(&self) -> HashMap<&str, Test> { - HashMap::<&str, Test>::from([ - ("6.1.1", test_6_01_01_missing_definition_of_product_id), - ("6.1.2", test_6_01_02_multiple_definition_of_product_id), - ] - as [(&str, Test); 2]) + type CsafTest = Test; + HashMap::from([ + ("6.1.1", test_6_01_01_missing_definition_of_product_id as CsafTest), + ("6.1.2", test_6_01_02_multiple_definition_of_product_id as CsafTest), + ]) } fn doc(&self) -> &CommonSecurityAdvisoryFramework { 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 75a4521..0f22ebb 100644 --- a/csaf-lib/src/csaf/csaf2_1/csaf_json_schema.json +++ b/csaf-lib/src/csaf/csaf2_1/csaf_json_schema.json @@ -247,13 +247,20 @@ "minLength": 1 } }, - "purl": { - "title": "package URL representation", - "description": "The package URL (purl) attribute refers to a method for reliably identifying and locating software packages external to this specification.", - "type": "string", - "format": "uri", - "pattern": "^pkg:[A-Za-z\\.\\-\\+][A-Za-z0-9\\.\\-\\+]*\\/.+", - "minLength": 7 + "purls": { + "title": "List of package URLs", + "description": "Contains a list of package URLs (purl).", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "title": "package URL representation", + "description": "The package URL (purl) attribute refers to a method for reliably identifying and locating software packages external to this specification.", + "type": "string", + "format": "uri", + "pattern": "^pkg:[A-Za-z\\.\\-\\+][A-Za-z0-9\\.\\-\\+]*\\/.+", + "minLength": 7 + } }, "sbom_urls": { "title": "List of SBOM URLs", @@ -581,6 +588,36 @@ "tlp" ], "properties": { + "sharing_group": { + "title": "Sharing Group", + "description": "Contains information about the group this document is intended to be shared with.", + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "title": "Sharing Group ID", + "description": "Provides the unique ID for the sharing group.", + "type": "string", + "pattern": "^(([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})|([0]{8}-([0]{4}-){3}[0]{12})|([f]{8}-([f]{4}-){3}[f]{12}))$" + }, + "name": { + "title": "Sharing Group Name", + "description": "Contains a human-readable name for the sharing group.", + "type": "string", + "minLength": 1, + "examples": [ + "Customer A", + "ISAC members", + "NIS2 regulated important entities in Germany, sector water", + "Pre-Sharing group for advisory discussion", + "Users of Product A", + "US Federal Civilian Authorities" + ] + } + } + }, "text": { "title": "Textual description", "description": "Provides a textual description of additional constraints.", @@ -1328,9 +1365,11 @@ "description": "Specifies the category which this remediation belongs to.", "type": "string", "enum": [ + "fix_planned", "mitigation", "no_fix_planned", "none_available", + "optional_patch", "vendor_fix", "workaround" ] diff --git a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs new file mode 100644 index 0000000..7ae3a8c --- /dev/null +++ b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs @@ -0,0 +1,57 @@ +use crate::csaf::csaf2_1::schema::ProductTree; +use crate::csaf::csaf2_1::schema::{CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, ProductGroup, Remediation, Vulnerability}; +use crate::csaf::getter_traits::{CsafTrait, ProductGroupTrait, ProductTreeTrait, RemediationTrait, VulnerabilityTrait}; +use std::ops::Deref; + +impl RemediationTrait for Remediation { + fn get_category(&self) -> CategoryOfTheRemediation { + self.category.clone() + } + + fn get_product_ids(&self) -> Option> { + self.product_ids.as_ref().map(|p| (*p).iter().map(|x| x.deref()).collect()) + } + + fn get_group_ids(&self) -> Option> { + self.group_ids.as_ref().map(|g| (*g).iter().map(|x| x.deref()).collect()) + } +} + +impl VulnerabilityTrait for Vulnerability { + type RemediationType = Remediation; + + fn get_remediations(&self) -> Vec { + self.remediations.clone() + } +} + +impl CsafTrait for CommonSecurityAdvisoryFramework { + type VulnerabilityType = Vulnerability; + type ProductTreeType = ProductTree; + + fn get_product_tree(&self) -> Option { + self.product_tree.clone() + } + + fn get_vulnerabilities(&self) -> Vec { + self.vulnerabilities.clone() + } +} + +impl ProductTreeTrait for ProductTree { + type ProductGroupType = ProductGroup; + + fn get_product_groups(&self) -> Vec { + self.product_groups.clone() + } +} + +impl ProductGroupTrait for ProductGroup { + fn get_group_id(&self) -> &String { + self.group_id.deref() + } + + fn get_product_ids(&self) -> Vec<&String> { + self.product_ids.iter().map(|x| x.deref()).collect() + } +} \ No newline at end of file diff --git a/csaf-lib/src/csaf/csaf2_1/mod.rs b/csaf-lib/src/csaf/csaf2_1/mod.rs index bf1a005..b747560 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; mod product_helper; pub mod schema; pub mod validation; +pub mod getter_implementations; diff --git a/csaf-lib/src/csaf/csaf2_1/schema.rs b/csaf-lib/src/csaf/csaf2_1/schema.rs index 006729e..10f1422 100644 --- a/csaf-lib/src/csaf/csaf2_1/schema.rs +++ b/csaf-lib/src/csaf/csaf2_1/schema.rs @@ -1169,9 +1169,11 @@ impl std::convert::TryFrom for CategoryOfTheBranch { /// "description": "Specifies the category which this remediation belongs to.", /// "type": "string", /// "enum": [ +/// "fix_planned", /// "mitigation", /// "no_fix_planned", /// "none_available", +/// "optional_patch", /// "vendor_fix", /// "workaround" /// ] @@ -1191,12 +1193,16 @@ impl std::convert::TryFrom for CategoryOfTheBranch { PartialOrd )] pub enum CategoryOfTheRemediation { + #[serde(rename = "fix_planned")] + FixPlanned, #[serde(rename = "mitigation")] Mitigation, #[serde(rename = "no_fix_planned")] NoFixPlanned, #[serde(rename = "none_available")] NoneAvailable, + #[serde(rename = "optional_patch")] + OptionalPatch, #[serde(rename = "vendor_fix")] VendorFix, #[serde(rename = "workaround")] @@ -1210,9 +1216,11 @@ impl From<&CategoryOfTheRemediation> for CategoryOfTheRemediation { impl ::std::fmt::Display for CategoryOfTheRemediation { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { match *self { + Self::FixPlanned => write!(f, "fix_planned"), Self::Mitigation => write!(f, "mitigation"), Self::NoFixPlanned => write!(f, "no_fix_planned"), Self::NoneAvailable => write!(f, "none_available"), + Self::OptionalPatch => write!(f, "optional_patch"), Self::VendorFix => write!(f, "vendor_fix"), Self::Workaround => write!(f, "workaround"), } @@ -1222,9 +1230,11 @@ impl std::str::FromStr for CategoryOfTheRemediation { type Err = self::error::ConversionError; fn from_str(value: &str) -> Result { match value { + "fix_planned" => Ok(Self::FixPlanned), "mitigation" => Ok(Self::Mitigation), "no_fix_planned" => Ok(Self::NoFixPlanned), "none_available" => Ok(Self::NoneAvailable), + "optional_patch" => Ok(Self::OptionalPatch), "vendor_fix" => Ok(Self::VendorFix), "workaround" => Ok(Self::Workaround), _ => Err("invalid value".into()), @@ -1511,6 +1521,36 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "tlp" /// ], /// "properties": { +/// "sharing_group": { +/// "title": "Sharing Group", +/// "description": "Contains information about the group this document is intended to be shared with.", +/// "type": "object", +/// "required": [ +/// "id" +/// ], +/// "properties": { +/// "id": { +/// "title": "Sharing Group ID", +/// "description": "Provides the unique ID for the sharing group.", +/// "type": "string", +/// "pattern": "^(([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})|([0]{8}-([0]{4}-){3}[0]{12})|([f]{8}-([f]{4}-){3}[f]{12}))$" +/// }, +/// "name": { +/// "title": "Sharing Group Name", +/// "description": "Contains a human-readable name for the sharing group.", +/// "examples": [ +/// "Customer A", +/// "ISAC members", +/// "NIS2 regulated important entities in Germany, sector water", +/// "Pre-Sharing group for advisory discussion", +/// "Users of Product A", +/// "US Federal Civilian Authorities" +/// ], +/// "type": "string", +/// "minLength": 1 +/// } +/// } +/// }, /// "text": { /// "title": "Textual description", /// "description": "Provides a textual description of additional constraints.", @@ -2256,9 +2296,11 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "description": "Specifies the category which this remediation belongs to.", /// "type": "string", /// "enum": [ +/// "fix_planned", /// "mitigation", /// "no_fix_planned", /// "none_available", +/// "optional_patch", /// "vendor_fix", /// "workaround" /// ] @@ -3376,6 +3418,36 @@ impl DocumentGenerator { /// "tlp" /// ], /// "properties": { +/// "sharing_group": { +/// "title": "Sharing Group", +/// "description": "Contains information about the group this document is intended to be shared with.", +/// "type": "object", +/// "required": [ +/// "id" +/// ], +/// "properties": { +/// "id": { +/// "title": "Sharing Group ID", +/// "description": "Provides the unique ID for the sharing group.", +/// "type": "string", +/// "pattern": "^(([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})|([0]{8}-([0]{4}-){3}[0]{12})|([f]{8}-([f]{4}-){3}[f]{12}))$" +/// }, +/// "name": { +/// "title": "Sharing Group Name", +/// "description": "Contains a human-readable name for the sharing group.", +/// "examples": [ +/// "Customer A", +/// "ISAC members", +/// "NIS2 regulated important entities in Germany, sector water", +/// "Pre-Sharing group for advisory discussion", +/// "Users of Product A", +/// "US Federal Civilian Authorities" +/// ], +/// "type": "string", +/// "minLength": 1 +/// } +/// } +/// }, /// "text": { /// "title": "Textual description", /// "description": "Provides a textual description of additional constraints.", @@ -4396,13 +4468,20 @@ impl Flag { /// "minItems": 1, /// "uniqueItems": true /// }, -/// "purl": { -/// "title": "package URL representation", -/// "description": "The package URL (purl) attribute refers to a method for reliably identifying and locating software packages external to this specification.", -/// "type": "string", -/// "format": "uri", -/// "minLength": 7, -/// "pattern": "^pkg:[A-Za-z\\.\\-\\+][A-Za-z0-9\\.\\-\\+]*\\/.+" +/// "purls": { +/// "title": "List of package URLs", +/// "description": "Contains a list of package URLs (purl).", +/// "type": "array", +/// "items": { +/// "title": "package URL representation", +/// "description": "The package URL (purl) attribute refers to a method for reliably identifying and locating software packages external to this specification.", +/// "type": "string", +/// "format": "uri", +/// "minLength": 7, +/// "pattern": "^pkg:[A-Za-z\\.\\-\\+][A-Za-z0-9\\.\\-\\+]*\\/.+" +/// }, +/// "minItems": 1, +/// "uniqueItems": true /// }, /// "sbom_urls": { /// "title": "List of SBOM URLs", @@ -4643,13 +4722,20 @@ impl GenericUri { /// "minItems": 1, /// "uniqueItems": true /// }, -/// "purl": { -/// "title": "package URL representation", -/// "description": "The package URL (purl) attribute refers to a method for reliably identifying and locating software packages external to this specification.", -/// "type": "string", -/// "format": "uri", -/// "minLength": 7, -/// "pattern": "^pkg:[A-Za-z\\.\\-\\+][A-Za-z0-9\\.\\-\\+]*\\/.+" +/// "purls": { +/// "title": "List of package URLs", +/// "description": "Contains a list of package URLs (purl).", +/// "type": "array", +/// "items": { +/// "title": "package URL representation", +/// "description": "The package URL (purl) attribute refers to a method for reliably identifying and locating software packages external to this specification.", +/// "type": "string", +/// "format": "uri", +/// "minLength": 7, +/// "pattern": "^pkg:[A-Za-z\\.\\-\\+][A-Za-z0-9\\.\\-\\+]*\\/.+" +/// }, +/// "minItems": 1, +/// "uniqueItems": true /// }, /// "sbom_urls": { /// "title": "List of SBOM URLs", @@ -4732,9 +4818,9 @@ pub struct HelperToIdentifyTheProduct { ///Contains a list of full or abbreviated (partial) model numbers. #[serde(default, skip_serializing_if = "Option::is_none")] pub model_numbers: Option>, - ///The package URL (purl) attribute refers to a method for reliably identifying and locating software packages external to this specification. + ///Contains a list of package URLs (purl). #[serde(default, skip_serializing_if = "Option::is_none")] - pub purl: Option, + pub purls: Option>, ///Contains a list of URLs where SBOMs for this product can be retrieved. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub sbom_urls: Vec, @@ -7195,9 +7281,11 @@ impl std::convert::TryFrom for RelationshipCategory { /// "description": "Specifies the category which this remediation belongs to.", /// "type": "string", /// "enum": [ +/// "fix_planned", /// "mitigation", /// "no_fix_planned", /// "none_available", +/// "optional_patch", /// "vendor_fix", /// "workaround" /// ] @@ -7440,6 +7528,36 @@ impl Revision { /// "tlp" /// ], /// "properties": { +/// "sharing_group": { +/// "title": "Sharing Group", +/// "description": "Contains information about the group this document is intended to be shared with.", +/// "type": "object", +/// "required": [ +/// "id" +/// ], +/// "properties": { +/// "id": { +/// "title": "Sharing Group ID", +/// "description": "Provides the unique ID for the sharing group.", +/// "type": "string", +/// "pattern": "^(([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})|([0]{8}-([0]{4}-){3}[0]{12})|([f]{8}-([f]{4}-){3}[f]{12}))$" +/// }, +/// "name": { +/// "title": "Sharing Group Name", +/// "description": "Contains a human-readable name for the sharing group.", +/// "examples": [ +/// "Customer A", +/// "ISAC members", +/// "NIS2 regulated important entities in Germany, sector water", +/// "Pre-Sharing group for advisory discussion", +/// "Users of Product A", +/// "US Federal Civilian Authorities" +/// ], +/// "type": "string", +/// "minLength": 1 +/// } +/// } +/// }, /// "text": { /// "title": "Textual description", /// "description": "Provides a textual description of additional constraints.", @@ -7491,6 +7609,8 @@ impl Revision { /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] pub struct RulesForSharingDocument { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sharing_group: Option, ///Provides a textual description of additional constraints. #[serde(default, skip_serializing_if = "Option::is_none")] pub text: Option, @@ -7576,6 +7696,218 @@ impl<'de> ::serde::Deserialize<'de> for SerialNumber { }) } } +///Contains information about the group this document is intended to be shared with. +/// +///
JSON schema +/// +/// ```json +///{ +/// "title": "Sharing Group", +/// "description": "Contains information about the group this document is intended to be shared with.", +/// "type": "object", +/// "required": [ +/// "id" +/// ], +/// "properties": { +/// "id": { +/// "title": "Sharing Group ID", +/// "description": "Provides the unique ID for the sharing group.", +/// "type": "string", +/// "pattern": "^(([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})|([0]{8}-([0]{4}-){3}[0]{12})|([f]{8}-([f]{4}-){3}[f]{12}))$" +/// }, +/// "name": { +/// "title": "Sharing Group Name", +/// "description": "Contains a human-readable name for the sharing group.", +/// "examples": [ +/// "Customer A", +/// "ISAC members", +/// "NIS2 regulated important entities in Germany, sector water", +/// "Pre-Sharing group for advisory discussion", +/// "Users of Product A", +/// "US Federal Civilian Authorities" +/// ], +/// "type": "string", +/// "minLength": 1 +/// } +/// } +///} +/// ``` +///
+#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +pub struct SharingGroup { + ///Provides the unique ID for the sharing group. + pub id: SharingGroupId, + ///Contains a human-readable name for the sharing group. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, +} +impl From<&SharingGroup> for SharingGroup { + fn from(value: &SharingGroup) -> Self { + value.clone() + } +} +impl SharingGroup { + pub fn builder() -> builder::SharingGroup { + Default::default() + } +} +///Provides the unique ID for the sharing group. +/// +///
JSON schema +/// +/// ```json +///{ +/// "title": "Sharing Group ID", +/// "description": "Provides the unique ID for the sharing group.", +/// "type": "string", +/// "pattern": "^(([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})|([0]{8}-([0]{4}-){3}[0]{12})|([f]{8}-([f]{4}-){3}[f]{12}))$" +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct SharingGroupId(String); +impl ::std::ops::Deref for SharingGroupId { + type Target = String; + fn deref(&self) -> &String { + &self.0 + } +} +impl From for String { + fn from(value: SharingGroupId) -> Self { + value.0 + } +} +impl From<&SharingGroupId> for SharingGroupId { + fn from(value: &SharingGroupId) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for SharingGroupId { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> Result { + if regress::Regex::new( + "^(([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})|([0]{8}-([0]{4}-){3}[0]{12})|([f]{8}-([f]{4}-){3}[f]{12}))$", + ) + .unwrap() + .find(value) + .is_none() + { + return Err( + "doesn't match pattern \"^(([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})|([0]{8}-([0]{4}-){3}[0]{12})|([f]{8}-([f]{4}-){3}[f]{12}))$\"" + .into(), + ); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for SharingGroupId { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&String> for SharingGroupId { + type Error = self::error::ConversionError; + fn try_from(value: &String) -> Result { + value.parse() + } +} +impl ::std::convert::TryFrom for SharingGroupId { + type Error = self::error::ConversionError; + fn try_from(value: String) -> Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for SharingGroupId { + fn deserialize(deserializer: D) -> Result + where + D: ::serde::Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} +///Contains a human-readable name for the sharing group. +/// +///
JSON schema +/// +/// ```json +///{ +/// "title": "Sharing Group Name", +/// "description": "Contains a human-readable name for the sharing group.", +/// "examples": [ +/// "Customer A", +/// "ISAC members", +/// "NIS2 regulated important entities in Germany, sector water", +/// "Pre-Sharing group for advisory discussion", +/// "Users of Product A", +/// "US Federal Civilian Authorities" +/// ], +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct SharingGroupName(String); +impl ::std::ops::Deref for SharingGroupName { + type Target = String; + fn deref(&self) -> &String { + &self.0 + } +} +impl From for String { + fn from(value: SharingGroupName) -> Self { + value.0 + } +} +impl From<&SharingGroupName> for SharingGroupName { + fn from(value: &SharingGroupName) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for SharingGroupName { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> Result { + if value.len() < 1usize { + return Err("shorter than 1 characters".into()); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for SharingGroupName { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&String> for SharingGroupName { + type Error = self::error::ConversionError; + fn try_from(value: &String) -> Result { + value.parse() + } +} +impl ::std::convert::TryFrom for SharingGroupName { + type Error = self::error::ConversionError; + fn try_from(value: String) -> Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for SharingGroupName { + fn deserialize(deserializer: D) -> Result + where + D: ::serde::Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(|e: self::error::ConversionError| { + ::custom(e.to_string()) + }) + } +} ///Contains a full or abbreviated (partial) stock keeping unit (SKU) which is used in the ordering process to identify the component. /// ///
JSON schema @@ -9508,9 +9840,11 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "description": "Specifies the category which this remediation belongs to.", /// "type": "string", /// "enum": [ +/// "fix_planned", /// "mitigation", /// "no_fix_planned", /// "none_available", +/// "optional_patch", /// "vendor_fix", /// "workaround" /// ] @@ -10981,7 +11315,7 @@ pub mod builder { cpe: Result, String>, hashes: Result, String>, model_numbers: Result>, String>, - purl: Result, String>, + purls: Result>, String>, sbom_urls: Result, String>, serial_numbers: Result>, String>, skus: Result, String>, @@ -10993,7 +11327,7 @@ pub mod builder { cpe: Ok(Default::default()), hashes: Ok(Default::default()), model_numbers: Ok(Default::default()), - purl: Ok(Default::default()), + purls: Ok(Default::default()), sbom_urls: Ok(Default::default()), serial_numbers: Ok(Default::default()), skus: Ok(Default::default()), @@ -11038,14 +11372,16 @@ pub mod builder { }); self } - pub fn purl(mut self, value: T) -> Self + pub fn purls(mut self, value: T) -> Self where - T: std::convert::TryInto>, + T: std::convert::TryInto>>, T::Error: std::fmt::Display, { - self.purl = value + self.purls = value .try_into() - .map_err(|e| format!("error converting supplied value for purl: {}", e)); + .map_err(|e| { + format!("error converting supplied value for purls: {}", e) + }); self } pub fn sbom_urls(mut self, value: T) -> Self @@ -11105,7 +11441,7 @@ pub mod builder { cpe: value.cpe?, hashes: value.hashes?, model_numbers: value.model_numbers?, - purl: value.purl?, + purls: value.purls?, sbom_urls: value.sbom_urls?, serial_numbers: value.serial_numbers?, skus: value.skus?, @@ -11119,7 +11455,7 @@ pub mod builder { cpe: Ok(value.cpe), hashes: Ok(value.hashes), model_numbers: Ok(value.model_numbers), - purl: Ok(value.purl), + purls: Ok(value.purls), sbom_urls: Ok(value.sbom_urls), serial_numbers: Ok(value.serial_numbers), skus: Ok(value.skus), @@ -12317,18 +12653,32 @@ pub mod builder { } #[derive(Clone, Debug)] pub struct RulesForSharingDocument { + sharing_group: Result, String>, text: Result, String>, tlp: Result, } impl Default for RulesForSharingDocument { fn default() -> Self { Self { + sharing_group: Ok(Default::default()), text: Ok(Default::default()), tlp: Err("no value supplied for tlp".to_string()), } } } impl RulesForSharingDocument { + pub fn sharing_group(mut self, value: T) -> Self + where + T: std::convert::TryInto>, + T::Error: std::fmt::Display, + { + self.sharing_group = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for sharing_group: {}", e) + }); + self + } pub fn text(mut self, value: T) -> Self where T: std::convert::TryInto>, @@ -12357,6 +12707,7 @@ pub mod builder { value: RulesForSharingDocument, ) -> Result { Ok(Self { + sharing_group: value.sharing_group?, text: value.text?, tlp: value.tlp?, }) @@ -12365,12 +12716,65 @@ pub mod builder { impl From for RulesForSharingDocument { fn from(value: super::RulesForSharingDocument) -> Self { Self { + sharing_group: Ok(value.sharing_group), text: Ok(value.text), tlp: Ok(value.tlp), } } } #[derive(Clone, Debug)] + pub struct SharingGroup { + id: Result, + name: Result, String>, + } + impl Default for SharingGroup { + fn default() -> Self { + Self { + id: Err("no value supplied for id".to_string()), + name: Ok(Default::default()), + } + } + } + impl SharingGroup { + 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 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::SharingGroup { + type Error = super::error::ConversionError; + fn try_from(value: SharingGroup) -> Result { + Ok(Self { + id: value.id?, + name: value.name?, + }) + } + } + impl From for SharingGroup { + fn from(value: super::SharingGroup) -> Self { + Self { + id: Ok(value.id), + name: Ok(value.name), + } + } + } + #[derive(Clone, Debug)] pub struct Threat { category: Result, date: Result>, String>, diff --git a/csaf-lib/src/csaf/csaf2_1/validation.rs b/csaf-lib/src/csaf/csaf2_1/validation.rs index 24e88ff..d8a26ea 100644 --- a/csaf-lib/src/csaf/csaf2_1/validation.rs +++ b/csaf-lib/src/csaf/csaf2_1/validation.rs @@ -1,28 +1,32 @@ use super::product_helper::*; use super::schema::CommonSecurityAdvisoryFramework; use crate::csaf::helpers::find_duplicates; -use crate::csaf::validation::{Test, Validatable, ValidationPreset}; +use crate::csaf::validation::{test_6_01_35_contradicting_remediations, Test, Validatable, ValidationPreset}; use std::collections::{HashMap, HashSet}; impl Validatable for CommonSecurityAdvisoryFramework { fn presets(&self) -> HashMap> { + let basic_tests = Vec::from(["6.1.1", "6.1.2", "6.1.34", "6.1.35"]); + // More tests may be added in extend() here later + let extended_tests: Vec<&str> = basic_tests.clone(); + // extended_tests.extend(["foo"].iter()); + let full_tests: Vec<&str> = extended_tests.clone(); + // full_tests.extend(["bar"].iter()); HashMap::from([ - ( - ValidationPreset::Basic, - Vec::from(["6.1.1", "6.1.2", "6.1.34"]), - ), - (ValidationPreset::Extended, Vec::from(["6.1.1", "6.1.2"])), - (ValidationPreset::Full, Vec::from(["6.1.1", "6.1.2"])), + (ValidationPreset::Basic, basic_tests), + (ValidationPreset::Extended, extended_tests), + (ValidationPreset::Full, full_tests), ]) } fn tests(&self) -> HashMap<&str, Test> { - HashMap::<&str, Test>::from([ - ("6.1.1", test_6_01_01_missing_definition_of_product_id), - ("6.1.2", test_6_01_02_multiple_definition_of_product_id), - ("6.1.34", test_6_01_34_branches_recursion_depth), - ] - as [(&str, Test); 3]) + type CsafTest = Test; + HashMap::from([ + ("6.1.1", test_6_01_01_missing_definition_of_product_id as CsafTest), + ("6.1.2", test_6_01_02_multiple_definition_of_product_id as CsafTest), + ("6.1.34", test_6_01_34_branches_recursion_depth as CsafTest), + ("6.1.35", test_6_01_35_contradicting_remediations as CsafTest), + ]) } fn doc(&self) -> &CommonSecurityAdvisoryFramework { @@ -82,6 +86,7 @@ mod tests { validation::test_6_01_02_multiple_definition_of_product_id, validation::test_6_01_34_branches_recursion_depth, }; + use crate::csaf::validation::test_6_01_35_contradicting_remediations; #[test] fn test_test_6_01_01() { @@ -111,4 +116,27 @@ mod tests { Err(String::from("Recursion depth too big: 31")) ) } + + #[test] + fn test_test_6_01_35() { + for x in ["11", "12", "13", "14"].iter() { + let doc = load_document(format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-35-{}.json", x).as_str()).unwrap(); + assert_eq!( + Ok(()), + test_6_01_35_contradicting_remediations(&doc) + ) + } + for (x, err) in [ + ("01", "Product CSAFPID-9080700 has contradicting remediations: no_fix_planned and vendor_fix"), + ("02", "Product CSAFPID-9080700 has contradicting remediations: none_available and mitigation"), + ("03", "Product CSAFPID-9080702 has contradicting remediations: workaround, fix_planned and optional_patch"), + ("04", "Product CSAFPID-9080701 has contradicting remediations: mitigation, fix_planned and optional_patch"), + ].iter() { + let doc = load_document(format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-35-{}.json", x).as_str()).unwrap(); + assert_eq!( + Err(format!("{}", err)), + test_6_01_35_contradicting_remediations(&doc) + ) + } + } } diff --git a/csaf-lib/src/csaf/getter_traits.rs b/csaf-lib/src/csaf/getter_traits.rs new file mode 100644 index 0000000..8568edf --- /dev/null +++ b/csaf-lib/src/csaf/getter_traits.rs @@ -0,0 +1,102 @@ +use crate::csaf::csaf2_1::schema::CategoryOfTheRemediation; +use crate::csaf::helpers::resolve_product_groups; +use std::collections::BTreeSet; + +/// Trait representing an abstract Common Security Advisory Framework (CSAF) document. +/// +/// The `CsafTrait` trait defines the key structure of a CSAF document, allowing +/// interaction with its vulnerabilities and product tree without tying to a +/// specific version of the CSAF schema. +pub trait CsafTrait { + /// The associated type representing the type of vulnerabilities in this CSAF structure. + type VulnerabilityType: VulnerabilityTrait; + + /// The associated type representing the type of product tree in this CSAF structure. + type ProductTreeType: ProductTreeTrait; + + /// Returns the product tree of the CSAF document. + fn get_product_tree(&self) -> Option; + + /// Retrieves all vulnerabilities in the CSAF document. + fn get_vulnerabilities(&self) -> Vec; +} + +/// Trait representing an abstract vulnerability in a CSAF document. +/// +/// The `VulnerabilityTrait` defines the structure of a vulnerability and includes +/// information about potential remediations. +pub trait VulnerabilityTrait { + /// The associated type representing the type of remediations in a vulnerability. + type RemediationType: RemediationTrait; + + /// Retrieves all remediations associated with the vulnerability. + fn get_remediations(&self) -> Vec; +} + +/// Trait representing an abstract remediation in a CSAF document. +/// +/// The `RemediationTrait` encapsulates the details of a remediation, such as its +/// category and the affected products or groups. +pub trait RemediationTrait { + /// Returns the category of the remediation. + /// + /// Categories are defined by the CSAF schema. + fn get_category(&self) -> CategoryOfTheRemediation; + + /// Retrieves the product IDs directly affected by this remediation, if any. + fn get_product_ids(&self) -> Option>; + + /// Retrieves the product group IDs related to this remediation, if any. + fn get_group_ids(&self) -> Option>; + + /// Computes a set of all product IDs affected by this remediation, either + /// directly or through product groups. + /// + /// # Arguments + /// + /// * `doc` - A reference to the CSAF document to resolve product groups. + /// + /// # Returns + /// + /// A `BTreeSet` containing all product IDs, or `None` if none exist. + fn get_all_product_ids(&self, doc: &impl CsafTrait) -> Option> { + if self.get_product_ids().is_none() && self.get_group_ids().is_none() { + None + } else { + let mut product_set: BTreeSet = match self.get_product_ids() { + Some(product_ids) => product_ids.iter().map(|p| p.to_string()).collect(), + None => BTreeSet::new(), + }; + if let Some(product_groups) = self.get_group_ids() { + if let Some(product_ids) = resolve_product_groups(doc, product_groups) { + product_set.extend(product_ids.iter().map(|p| p.to_string())); + } + } + Some(product_set) + } + } +} + +/// Trait representing an abstract product tree in a CSAF document. +/// +/// The `ProductTreeTrait` defines the structure of a product tree and allows +/// access to its product groups. +pub trait ProductTreeTrait { + /// The associated type representing the type of product groups in the product tree. + type ProductGroupType: ProductGroupTrait; + + /// Retrieves all product groups in the product tree. + fn get_product_groups(&self) -> Vec; +} + +/// Trait representing an abstract product group in a CSAF document. +/// +/// The `ProductGroupTrait` encapsulates the details of a product group, including +/// its IDs and associated product IDs. +pub trait ProductGroupTrait { + /// Retrieves the group ID of the product group. + fn get_group_id(&self) -> &String; + + /// Retrieves the product IDs contained within the product group. + fn get_product_ids(&self) -> Vec<&String>; +} \ No newline at end of file diff --git a/csaf-lib/src/csaf/helpers.rs b/csaf-lib/src/csaf/helpers.rs index 2052419..9dd47fe 100644 --- a/csaf-lib/src/csaf/helpers.rs +++ b/csaf-lib/src/csaf/helpers.rs @@ -1,4 +1,5 @@ -use std::collections::HashMap; +use crate::csaf::getter_traits::{CsafTrait, ProductGroupTrait, ProductTreeTrait}; +use std::collections::{BTreeSet, HashMap}; pub fn find_duplicates(vec: Vec) -> Vec { let mut occurrences = HashMap::new(); @@ -17,3 +18,15 @@ pub fn find_duplicates(vec: Vec) -> Vec { duplicates } + +pub fn resolve_product_groups(doc: &impl CsafTrait, product_groups: Vec<&String>) -> Option> { + doc.get_product_tree().map(|product_tree| { + product_tree + .get_product_groups() + .iter() + .filter(|x| product_groups.iter().any(|g| *g == x.get_group_id())) + .map(|x| x.get_product_ids().iter().map(|p| p.to_string()).collect::>()) + .flatten() + .collect() + }) +} \ No newline at end of file diff --git a/csaf-lib/src/csaf/mod.rs b/csaf-lib/src/csaf/mod.rs index ca2bfbb..248a2dd 100644 --- a/csaf-lib/src/csaf/mod.rs +++ b/csaf-lib/src/csaf/mod.rs @@ -2,3 +2,4 @@ pub mod csaf2_0; pub mod csaf2_1; mod helpers; pub mod validation; +pub mod getter_traits; diff --git a/csaf-lib/src/csaf/validation.rs b/csaf-lib/src/csaf/validation.rs index c255ab9..dddadec 100644 --- a/csaf-lib/src/csaf/validation.rs +++ b/csaf-lib/src/csaf/validation.rs @@ -1,3 +1,5 @@ +use crate::csaf::csaf2_1::schema::CategoryOfTheRemediation; +use crate::csaf::getter_traits::{CsafTrait, RemediationTrait, VulnerabilityTrait}; use std::collections::HashMap; use std::str::FromStr; @@ -83,3 +85,56 @@ pub fn validate_by_test( println!("Test with ID {} is missing implementation", test_id); } } + +static MUT_EX_MEASURES: &[CategoryOfTheRemediation] = &[ + CategoryOfTheRemediation::NoneAvailable, + CategoryOfTheRemediation::Workaround, + CategoryOfTheRemediation::Mitigation, +]; + +static MUT_EX_FIX_STATES: &[CategoryOfTheRemediation] = &[ + CategoryOfTheRemediation::NoneAvailable, + CategoryOfTheRemediation::NoFixPlanned, + CategoryOfTheRemediation::FixPlanned, + CategoryOfTheRemediation::OptionalPatch, + CategoryOfTheRemediation::VendorFix, +]; + +pub fn test_6_01_35_contradicting_remediations( + target: &impl CsafTrait, +) -> Result<(), String> { + for v in target.get_vulnerabilities().iter() { + // Data struct to store observed remediation categories per product IT + let mut product_categories: HashMap> = HashMap::new(); + for r in v.get_remediations().iter() { + // Only handle Remediations having product IDs associated + if let Some(product_ids) = r.get_all_product_ids(target) { + // Category of current remediation + let cat = r.get_category(); + // Iterate over product IDs + for p in product_ids { + // Check if product ID has categories associated + if let Some(exist_cat_set) = product_categories.get(&p) { + // Check if any seen category conflicts with the current one + if exist_cat_set.iter().any(|e_cat| { + MUT_EX_MEASURES.contains(e_cat) && MUT_EX_MEASURES.contains(&cat) + || MUT_EX_FIX_STATES.contains(e_cat) && MUT_EX_FIX_STATES.contains(&cat) + }) { + return Err(format!( + "Product {} has contradicting remediations: {} and {}", + p, exist_cat_set.iter().map(|c| c.to_string()).collect::>().join(", "), cat + )); + } + let mut new_cat_vec = exist_cat_set.clone(); + new_cat_vec.push(cat.clone()); + product_categories.insert(p, new_cat_vec); + } else { + product_categories.insert(p, Vec::from([cat.clone()])); + } + } + } + } + } + Ok(()) +} +