From dc2d1a70667b63068f8f7224f68365bab0931c85 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Thu, 22 May 2025 10:33:19 +0200 Subject: [PATCH 01/12] Update CSAF schema 2.1, add support for handling notes Introduced `NoteTrait` and `WithGroupIds` traits, extending functionality to handle notes and group IDs across CSAF structures. Updated relevant getter implementations for compatibility with these enhancements. --- .../csaf/csaf2_0/getter_implementations.rs | 45 +- .../src/csaf/csaf2_1/csaf_json_schema.json | 49 +- .../csaf/csaf2_1/getter_implementations.rs | 44 +- csaf-lib/src/csaf/csaf2_1/schema.rs | 555 +++++++++++++++++- csaf-lib/src/csaf/getter_traits.rs | 44 +- csaf-lib/src/csaf/validations/test_6_1_37.rs | 7 +- 6 files changed, 697 insertions(+), 47 deletions(-) diff --git a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs index 185f16b..8e6907d 100644 --- a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs @@ -1,11 +1,17 @@ -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_0::schema::{Branch, CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, DocumentGenerator, DocumentLevelMetaData, DocumentStatus, Flag, FullProductNameT, HelperToIdentifyTheProduct, Id, Involvement, LabelOfTlp, Note, 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, VulnerabilityIdTrait}; +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, NoteTrait, WithGroupIds}; use std::ops::Deref; use serde::de::Error; use crate::csaf::csaf2_1::ssvc_schema::SsvcV1; use crate::csaf::validation::ValidationError; +impl WithGroupIds for Remediation { + fn get_group_ids(&self) -> Option + '_> { + self.group_ids.as_ref().map(|g| (*g).iter().map(|x| x.deref())) + } +} + impl RemediationTrait for Remediation { /// Normalizes the remediation categories from CSAF 2.0 to those of CSAF 2.1. /// @@ -31,10 +37,6 @@ impl RemediationTrait for Remediation { self.product_ids.as_ref().map(|p| (*p).iter().map(|x| x.deref())) } - fn get_group_ids(&self) -> Option + '_> { - self.group_ids.as_ref().map(|g| (*g).iter().map(|x| x.deref())) - } - fn get_date(&self) -> &Option { &self.date } @@ -98,6 +100,12 @@ impl ContentTrait for () { } } +impl WithGroupIds for Threat { + fn get_group_ids(&self) -> Option + '_> { + self.group_ids.as_ref().map(|g| (*g).iter().map(|x| x.deref())) + } +} + impl ThreatTrait for Threat { fn get_product_ids(&self) -> Option + '_> { self.product_ids.as_ref().map(|p| (*p).iter().map(|x| x.deref())) @@ -117,6 +125,7 @@ impl VulnerabilityTrait for Vulnerability { type FlagType = Flag; type InvolvementType = Involvement; type VulnerabilityIdType = Id; + type NoteType = Note; fn get_remediations(&self) -> &Vec { &self.remediations @@ -154,9 +163,14 @@ impl VulnerabilityTrait for Vulnerability { fn get_cve(&self) -> Option<&String> { self.cve.as_ref().map(|x| x.deref()) } + fn get_ids(&self) -> &Option> { &self.ids } + + fn get_notes(&self) -> Option<&Vec> { + self.notes.as_ref().map(|x| x.deref()) + } } impl VulnerabilityIdTrait for Id { @@ -169,6 +183,12 @@ impl VulnerabilityIdTrait for Id { } } +impl WithGroupIds for Flag { + fn get_group_ids(&self) -> Option + '_> { + self.group_ids.as_ref().map(|g| (*g).iter().map(|x| x.deref())) + } +} + impl FlagTrait for Flag { fn get_date(&self) -> &Option { &self.date @@ -202,6 +222,7 @@ impl CsafTrait for CommonSecurityAdvisoryFramework { impl DocumentTrait for DocumentLevelMetaData { type TrackingType = Tracking; type DistributionType = RulesForSharingDocument; + type NoteType = Note; fn get_tracking(&self) -> &Self::TrackingType { &self.tracking @@ -222,6 +243,10 @@ impl DocumentTrait for DocumentLevelMetaData { Some(distribution) => Ok(distribution) } } + + fn get_notes(&self) -> Option<&Vec> { + self.notes.as_ref().map(|x| x.deref()) + } } impl DistributionTrait for RulesForSharingDocument { @@ -249,6 +274,14 @@ impl DistributionTrait for RulesForSharingDocument { } } +impl WithGroupIds for Note { + fn get_group_ids(&self) -> Option + '_> { + None::> + } +} + +impl NoteTrait for Note {} + impl SharingGroupTrait for () { fn get_id(&self) -> &String { panic!("Sharing groups are not implemented in CSAF 2.0"); 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 042a764..8ad48dc 100644 --- a/csaf-lib/src/csaf/csaf2_1/csaf_json_schema.json +++ b/csaf-lib/src/csaf/csaf2_1/csaf_json_schema.json @@ -236,13 +236,13 @@ }, "model_numbers": { "title": "List of models", - "description": "Contains a list of full or abbreviated (partial) model numbers.", + "description": "Contains a list of model numbers.", "type": "array", "minItems": 1, "uniqueItems": true, "items": { "title": "Model number", - "description": "Contains a full or abbreviated (partial) model number of the component to identify.", + "description": "Contains a model number of the component to identify - possibly with placeholders.", "type": "string", "minLength": 1 } @@ -276,13 +276,13 @@ }, "serial_numbers": { "title": "List of serial numbers", - "description": "Contains a list of full or abbreviated (partial) serial numbers.", + "description": "Contains a list of serial numbers.", "type": "array", "minItems": 1, "uniqueItems": true, "items": { "title": "Serial number", - "description": "Contains a full or abbreviated (partial) serial number of the component to identify.", + "description": "Contains a serial number of the component to identify - possibly with placeholders.", "type": "string", "minLength": 1 } @@ -385,6 +385,12 @@ "summary" ] }, + "group_ids": { + "$ref": "#/$defs/product_groups_t" + }, + "product_ids": { + "$ref": "#/$defs/products_t" + }, "text": { "title": "Note content", "description": "Holds the content of the note. Content varies depending on type.", @@ -1274,6 +1280,36 @@ "cvss_v4": { "type": "object" }, + "epss": { + "title": "EPSS", + "description": "Contains the EPSS data.", + "type": "object", + "required": [ + "percentile", + "probability", + "timestamp" + ], + "properties": { + "percentile": { + "title": "Percentile", + "description": "Contains the rank ordering of probabilities from highest to lowest.", + "type": "string", + "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" + }, + "probability": { + "title": "Probability", + "description": "Contains the likelihood that any exploitation activity for this Vulnerability is being observed in the 30 days following the given timestamp.", + "type": "string", + "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" + }, + "timestamp": { + "title": "EPSS timestamp", + "description": "Holds the date and time the EPSS value was recorded.", + "type": "string", + "format": "date-time" + } + } + }, "ssvc_v1": { "type": "object" } @@ -1341,6 +1377,11 @@ "title": "Under investigation", "description": "It is not known yet whether these versions are or are not affected by the vulnerability. However, it is still under investigation - the result will be provided in a later release of the document.", "$ref": "#/$defs/products_t" + }, + "unknown": { + "title": "Unknown", + "description": "It is not known whether these versions are or are not affected by the vulnerability. There is also no investigation and therefore the status might never be determined.", + "$ref": "#/$defs/products_t" } } }, diff --git a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs index d23c189..f8c4315 100644 --- a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs @@ -1,10 +1,16 @@ -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 crate::csaf::csaf2_1::schema::{Branch, CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, Content, DocumentGenerator, DocumentLevelMetaData, DocumentStatus, Flag, FullProductNameT, HelperToIdentifyTheProduct, Id, Involvement, LabelOfTlp, Metric, Note, 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, NoteTrait, WithGroupIds}; use std::ops::Deref; use serde_json::Value; use crate::csaf::csaf2_1::ssvc_schema::SsvcV1; use crate::csaf::validation::ValidationError; +impl WithGroupIds for Remediation { + fn get_group_ids(&self) -> Option + '_> { + self.group_ids.as_ref().map(|g| (*g).iter().map(|x| x.deref())) + } +} + impl RemediationTrait for Remediation { fn get_category(&self) -> CategoryOfTheRemediation { self.category @@ -14,10 +20,6 @@ impl RemediationTrait for Remediation { self.product_ids.as_ref().map(|p| (*p).iter().map(|x| x.deref())) } - fn get_group_ids(&self) -> Option + '_> { - self.group_ids.as_ref().map(|g| (*g).iter().map(|x| x.deref())) - } - fn get_date(&self) -> &Option { &self.date } @@ -76,6 +78,12 @@ impl ContentTrait for Content { } } +impl WithGroupIds for Threat { + fn get_group_ids(&self) -> Option + '_> { + self.group_ids.as_ref().map(|g| (*g).iter().map(|x| x.deref())) + } +} + impl ThreatTrait for Threat { fn get_product_ids(&self) -> Option + '_> { self.product_ids.as_ref().map(|p| (*p).iter().map(|x| x.deref())) @@ -94,6 +102,7 @@ impl VulnerabilityTrait for Vulnerability { type FlagType = Flag; type InvolvementType = Involvement; type VulnerabilityIdType = Id; + type NoteType = Note; fn get_remediations(&self) -> &Vec { &self.remediations @@ -134,6 +143,10 @@ impl VulnerabilityTrait for Vulnerability { fn get_ids(&self) -> &Option> { &self.ids } + + fn get_notes(&self) -> Option<&Vec> { + self.notes.as_ref().map(|x| x.deref()) + } } impl VulnerabilityIdTrait for Id { @@ -146,6 +159,12 @@ impl VulnerabilityIdTrait for Id { } } +impl WithGroupIds for Flag { + fn get_group_ids(&self) -> Option + '_> { + self.group_ids.as_ref().map(|g| (*g).iter().map(|x| x.deref())) + } +} + impl FlagTrait for Flag { fn get_date(&self) -> &Option { &self.date @@ -179,6 +198,7 @@ impl CsafTrait for CommonSecurityAdvisoryFramework { impl DocumentTrait for DocumentLevelMetaData { type TrackingType = Tracking; type DistributionType = RulesForSharingDocument; + type NoteType = Note; fn get_tracking(&self) -> &Self::TrackingType { &self.tracking @@ -193,6 +213,10 @@ impl DocumentTrait for DocumentLevelMetaData { fn get_distribution_20(&self) -> Option<&Self::DistributionType> { Some(&self.distribution) } + + fn get_notes(&self) -> Option<&Vec> { + self.notes.as_ref().map(|x| x.deref()) + } } impl DistributionTrait for RulesForSharingDocument { @@ -214,6 +238,14 @@ impl DistributionTrait for RulesForSharingDocument { } } +impl WithGroupIds for Note { + fn get_group_ids(&self) -> Option + '_> { + self.group_ids.as_ref().map(|p| (*p).iter().map(|x| x.deref())) + } +} + +impl NoteTrait for Note {} + impl SharingGroupTrait for SharingGroup { fn get_id(&self) -> &String { &self.id diff --git a/csaf-lib/src/csaf/csaf2_1/schema.rs b/csaf-lib/src/csaf/csaf2_1/schema.rs index 09875c9..9e099be 100644 --- a/csaf-lib/src/csaf/csaf2_1/schema.rs +++ b/csaf-lib/src/csaf/csaf2_1/schema.rs @@ -2307,6 +2307,35 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "cvss_v4": { /// "type": "object" /// }, +/// "epss": { +/// "title": "EPSS", +/// "description": "Contains the EPSS data.", +/// "type": "object", +/// "required": [ +/// "percentile", +/// "probability", +/// "timestamp" +/// ], +/// "properties": { +/// "percentile": { +/// "title": "Percentile", +/// "description": "Contains the rank ordering of probabilities from highest to lowest.", +/// "type": "string", +/// "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" +/// }, +/// "probability": { +/// "title": "Probability", +/// "description": "Contains the likelihood that any exploitation activity for this Vulnerability is being observed in the 30 days following the given timestamp.", +/// "type": "string", +/// "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" +/// }, +/// "timestamp": { +/// "title": "EPSS timestamp", +/// "description": "Holds the date and time the EPSS value was recorded.", +/// "type": "string" +/// } +/// } +/// }, /// "ssvc_v1": { /// "type": "object" /// } @@ -2376,6 +2405,11 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "title": "Under investigation", /// "description": "It is not known yet whether these versions are or are not affected by the vulnerability. However, it is still under investigation - the result will be provided in a later release of the document.", /// "$ref": "#/$defs/products_t" +/// }, +/// "unknown": { +/// "title": "Unknown", +/// "description": "It is not known whether these versions are or are not affected by the vulnerability. There is also no investigation and therefore the status might never be determined.", +/// "$ref": "#/$defs/products_t" /// } /// } /// }, @@ -2665,6 +2699,35 @@ impl<'de> ::serde::Deserialize<'de> for ContactDetails { /// "cvss_v4": { /// "type": "object" /// }, +/// "epss": { +/// "title": "EPSS", +/// "description": "Contains the EPSS data.", +/// "type": "object", +/// "required": [ +/// "percentile", +/// "probability", +/// "timestamp" +/// ], +/// "properties": { +/// "percentile": { +/// "title": "Percentile", +/// "description": "Contains the rank ordering of probabilities from highest to lowest.", +/// "type": "string", +/// "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" +/// }, +/// "probability": { +/// "title": "Probability", +/// "description": "Contains the likelihood that any exploitation activity for this Vulnerability is being observed in the 30 days following the given timestamp.", +/// "type": "string", +/// "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" +/// }, +/// "timestamp": { +/// "title": "EPSS timestamp", +/// "description": "Holds the date and time the EPSS value was recorded.", +/// "type": "string" +/// } +/// } +/// }, /// "ssvc_v1": { /// "type": "object" /// } @@ -2680,6 +2743,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 = "::std::option::Option::is_none")] + pub epss: ::std::option::Option, #[serde(default, skip_serializing_if = "::serde_json::Map::is_empty")] pub ssvc_v1: ::serde_json::Map<::std::string::String, ::serde_json::Value>, } @@ -2694,6 +2759,7 @@ impl ::std::default::Default for Content { cvss_v2: Default::default(), cvss_v3: Default::default(), cvss_v4: Default::default(), + epss: Default::default(), ssvc_v1: Default::default(), } } @@ -4366,6 +4432,61 @@ impl<'de> ::serde::Deserialize<'de> for EntitlementOfTheRemediation { }) } } +///Contains the EPSS data. +/// +///
JSON schema +/// +/// ```json +///{ +/// "title": "EPSS", +/// "description": "Contains the EPSS data.", +/// "type": "object", +/// "required": [ +/// "percentile", +/// "probability", +/// "timestamp" +/// ], +/// "properties": { +/// "percentile": { +/// "title": "Percentile", +/// "description": "Contains the rank ordering of probabilities from highest to lowest.", +/// "type": "string", +/// "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" +/// }, +/// "probability": { +/// "title": "Probability", +/// "description": "Contains the likelihood that any exploitation activity for this Vulnerability is being observed in the 30 days following the given timestamp.", +/// "type": "string", +/// "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" +/// }, +/// "timestamp": { +/// "title": "EPSS timestamp", +/// "description": "Holds the date and time the EPSS value was recorded.", +/// "type": "string" +/// } +/// } +///} +/// ``` +///
+#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +pub struct Epss { + ///Contains the rank ordering of probabilities from highest to lowest. + pub percentile: Percentile, + ///Contains the likelihood that any exploitation activity for this Vulnerability is being observed in the 30 days following the given timestamp. + pub probability: Probability, + ///Holds the date and time the EPSS value was recorded. + pub timestamp: ::std::string::String, +} +impl ::std::convert::From<&Epss> for Epss { + fn from(value: &Epss) -> Self { + value.clone() + } +} +impl Epss { + pub fn builder() -> builder::Epss { + Default::default() + } +} ///Contains one hash value and algorithm of the file to be identified. /// ///
JSON schema @@ -4686,11 +4807,11 @@ impl Flag { /// }, /// "model_numbers": { /// "title": "List of models", -/// "description": "Contains a list of full or abbreviated (partial) model numbers.", +/// "description": "Contains a list of model numbers.", /// "type": "array", /// "items": { /// "title": "Model number", -/// "description": "Contains a full or abbreviated (partial) model number of the component to identify.", +/// "description": "Contains a model number of the component to identify - possibly with placeholders.", /// "type": "string", /// "minLength": 1 /// }, @@ -4726,11 +4847,11 @@ impl Flag { /// }, /// "serial_numbers": { /// "title": "List of serial numbers", -/// "description": "Contains a list of full or abbreviated (partial) serial numbers.", +/// "description": "Contains a list of serial numbers.", /// "type": "array", /// "items": { /// "title": "Serial number", -/// "description": "Contains a full or abbreviated (partial) serial number of the component to identify.", +/// "description": "Contains a serial number of the component to identify - possibly with placeholders.", /// "type": "string", /// "minLength": 1 /// }, @@ -4940,11 +5061,11 @@ impl GenericUri { /// }, /// "model_numbers": { /// "title": "List of models", -/// "description": "Contains a list of full or abbreviated (partial) model numbers.", +/// "description": "Contains a list of model numbers.", /// "type": "array", /// "items": { /// "title": "Model number", -/// "description": "Contains a full or abbreviated (partial) model number of the component to identify.", +/// "description": "Contains a model number of the component to identify - possibly with placeholders.", /// "type": "string", /// "minLength": 1 /// }, @@ -4980,11 +5101,11 @@ impl GenericUri { /// }, /// "serial_numbers": { /// "title": "List of serial numbers", -/// "description": "Contains a list of full or abbreviated (partial) serial numbers.", +/// "description": "Contains a list of serial numbers.", /// "type": "array", /// "items": { /// "title": "Serial number", -/// "description": "Contains a full or abbreviated (partial) serial number of the component to identify.", +/// "description": "Contains a serial number of the component to identify - possibly with placeholders.", /// "type": "string", /// "minLength": 1 /// }, @@ -5044,7 +5165,7 @@ pub struct HelperToIdentifyTheProduct { ///Contains a list of cryptographic hashes usable to identify files. #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] pub hashes: ::std::vec::Vec, - ///Contains a list of full or abbreviated (partial) model numbers. + ///Contains a list of model numbers. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub model_numbers: ::std::option::Option>, ///Contains a list of package URLs (purl). @@ -5053,7 +5174,7 @@ pub struct HelperToIdentifyTheProduct { ///Contains a list of URLs where SBOMs for this product can be retrieved. #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] pub sbom_urls: ::std::vec::Vec<::std::string::String>, - ///Contains a list of full or abbreviated (partial) serial numbers. + ///Contains a list of serial numbers. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub serial_numbers: ::std::option::Option>, ///Contains a list of full or abbreviated (partial) stock keeping units. @@ -5798,6 +5919,35 @@ impl<'de> ::serde::Deserialize<'de> for LegacyVersionOfTheRevision { /// "cvss_v4": { /// "type": "object" /// }, +/// "epss": { +/// "title": "EPSS", +/// "description": "Contains the EPSS data.", +/// "type": "object", +/// "required": [ +/// "percentile", +/// "probability", +/// "timestamp" +/// ], +/// "properties": { +/// "percentile": { +/// "title": "Percentile", +/// "description": "Contains the rank ordering of probabilities from highest to lowest.", +/// "type": "string", +/// "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" +/// }, +/// "probability": { +/// "title": "Probability", +/// "description": "Contains the likelihood that any exploitation activity for this Vulnerability is being observed in the 30 days following the given timestamp.", +/// "type": "string", +/// "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" +/// }, +/// "timestamp": { +/// "title": "EPSS timestamp", +/// "description": "Holds the date and time the EPSS value was recorded.", +/// "type": "string" +/// } +/// } +/// }, /// "ssvc_v1": { /// "type": "object" /// } @@ -5834,14 +5984,14 @@ impl Metric { Default::default() } } -///Contains a full or abbreviated (partial) model number of the component to identify. +///Contains a model number of the component to identify - possibly with placeholders. /// ///
JSON schema /// /// ```json ///{ /// "title": "Model number", -/// "description": "Contains a full or abbreviated (partial) model number of the component to identify.", +/// "description": "Contains a model number of the component to identify - possibly with placeholders.", /// "type": "string", /// "minLength": 1 ///} @@ -6209,6 +6359,12 @@ impl<'de> ::serde::Deserialize<'de> for NameOfTheContributor { /// "summary" /// ] /// }, +/// "group_ids": { +/// "$ref": "#/$defs/product_groups_t" +/// }, +/// "product_ids": { +/// "$ref": "#/$defs/products_t" +/// }, /// "text": { /// "title": "Note content", /// "description": "Holds the content of the note. Content varies depending on type.", @@ -6238,6 +6394,10 @@ pub struct Note { pub audience: ::std::option::Option, ///Contains the information of what kind of note this is. pub category: NoteCategory, + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub group_ids: ::std::option::Option, + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub product_ids: ::std::option::Option, ///Holds the content of the note. Content varies depending on type. pub text: NoteContent, ///Provides a concise description of what is contained in the text of the note. @@ -6485,6 +6645,12 @@ impl<'de> ::serde::Deserialize<'de> for NoteContent { /// "summary" /// ] /// }, +/// "group_ids": { +/// "$ref": "#/$defs/product_groups_t" +/// }, +/// "product_ids": { +/// "$ref": "#/$defs/products_t" +/// }, /// "text": { /// "title": "Note content", /// "description": "Holds the content of the note. Content varies depending on type.", @@ -6734,6 +6900,176 @@ impl ::std::convert::TryFrom<::std::string::String> for PartyStatus { value.parse() } } +///Contains the rank ordering of probabilities from highest to lowest. +/// +///
JSON schema +/// +/// ```json +///{ +/// "title": "Percentile", +/// "description": "Contains the rank ordering of probabilities from highest to lowest.", +/// "type": "string", +/// "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct Percentile(::std::string::String); +impl ::std::ops::Deref for Percentile { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: Percentile) -> Self { + value.0 + } +} +impl ::std::convert::From<&Percentile> for Percentile { + fn from(value: &Percentile) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for Percentile { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if regress::Regex::new("^(([0]\\.([0-9])+)|([1]\\.[0]+))$") + .unwrap() + .find(value) + .is_none() + { + return Err( + "doesn't match pattern \"^(([0]\\.([0-9])+)|([1]\\.[0]+))$\"".into(), + ); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for Percentile { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for Percentile { + 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 Percentile { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for Percentile { + 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()) + }) + } +} +///Contains the likelihood that any exploitation activity for this Vulnerability is being observed in the 30 days following the given timestamp. +/// +///
JSON schema +/// +/// ```json +///{ +/// "title": "Probability", +/// "description": "Contains the likelihood that any exploitation activity for this Vulnerability is being observed in the 30 days following the given timestamp.", +/// "type": "string", +/// "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct Probability(::std::string::String); +impl ::std::ops::Deref for Probability { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: Probability) -> Self { + value.0 + } +} +impl ::std::convert::From<&Probability> for Probability { + fn from(value: &Probability) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for Probability { + type Err = self::error::ConversionError; + fn from_str( + value: &str, + ) -> ::std::result::Result { + if regress::Regex::new("^(([0]\\.([0-9])+)|([1]\\.[0]+))$") + .unwrap() + .find(value) + .is_none() + { + return Err( + "doesn't match pattern \"^(([0]\\.([0-9])+)|([1]\\.[0]+))$\"".into(), + ); + } + Ok(Self(value.to_string())) + } +} +impl ::std::convert::TryFrom<&str> for Probability { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for Probability { + 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 Probability { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for Probability { + 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()) + }) + } +} ///Defines a new logical group of products that can then be referred to in other parts of the document to address a group of products with a single identifier. /// ///
JSON schema @@ -7052,6 +7388,11 @@ impl<'de> ::serde::Deserialize<'de> for ProductIdT { /// "title": "Under investigation", /// "description": "It is not known yet whether these versions are or are not affected by the vulnerability. However, it is still under investigation - the result will be provided in a later release of the document.", /// "$ref": "#/$defs/products_t" +/// }, +/// "unknown": { +/// "title": "Unknown", +/// "description": "It is not known whether these versions are or are not affected by the vulnerability. There is also no investigation and therefore the status might never be determined.", +/// "$ref": "#/$defs/products_t" /// } /// } ///} @@ -7083,6 +7424,9 @@ pub struct ProductStatus { ///It is not known yet whether these versions are or are not affected by the vulnerability. However, it is still under investigation - the result will be provided in a later release of the document. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub under_investigation: ::std::option::Option, + ///It is not known whether these versions are or are not affected by the vulnerability. There is also no investigation and therefore the status might never be determined. + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub unknown: ::std::option::Option, } impl ::std::convert::From<&ProductStatus> for ProductStatus { fn from(value: &ProductStatus) -> Self { @@ -7100,6 +7444,7 @@ impl ::std::default::Default for ProductStatus { last_affected: Default::default(), recommended: Default::default(), under_investigation: Default::default(), + unknown: Default::default(), } } } @@ -8044,14 +8389,14 @@ impl RulesForSharingDocument { Default::default() } } -///Contains a full or abbreviated (partial) serial number of the component to identify. +///Contains a serial number of the component to identify - possibly with placeholders. /// ///
JSON schema /// /// ```json ///{ /// "title": "Serial number", -/// "description": "Contains a full or abbreviated (partial) serial number of the component to identify.", +/// "description": "Contains a serial number of the component to identify - possibly with placeholders.", /// "type": "string", /// "minLength": 1 ///} @@ -10339,6 +10684,35 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "cvss_v4": { /// "type": "object" /// }, +/// "epss": { +/// "title": "EPSS", +/// "description": "Contains the EPSS data.", +/// "type": "object", +/// "required": [ +/// "percentile", +/// "probability", +/// "timestamp" +/// ], +/// "properties": { +/// "percentile": { +/// "title": "Percentile", +/// "description": "Contains the rank ordering of probabilities from highest to lowest.", +/// "type": "string", +/// "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" +/// }, +/// "probability": { +/// "title": "Probability", +/// "description": "Contains the likelihood that any exploitation activity for this Vulnerability is being observed in the 30 days following the given timestamp.", +/// "type": "string", +/// "pattern": "^(([0]\\.([0-9])+)|([1]\\.[0]+))$" +/// }, +/// "timestamp": { +/// "title": "EPSS timestamp", +/// "description": "Holds the date and time the EPSS value was recorded.", +/// "type": "string" +/// } +/// } +/// }, /// "ssvc_v1": { /// "type": "object" /// } @@ -10408,6 +10782,11 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "title": "Under investigation", /// "description": "It is not known yet whether these versions are or are not affected by the vulnerability. However, it is still under investigation - the result will be provided in a later release of the document.", /// "$ref": "#/$defs/products_t" +/// }, +/// "unknown": { +/// "title": "Unknown", +/// "description": "It is not known whether these versions are or are not affected by the vulnerability. There is also no investigation and therefore the status might never be determined.", +/// "$ref": "#/$defs/products_t" /// } /// } /// }, @@ -11194,6 +11573,10 @@ pub mod builder { ::serde_json::Map<::std::string::String, ::serde_json::Value>, ::std::string::String, >, + epss: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, ssvc_v1: ::std::result::Result< ::serde_json::Map<::std::string::String, ::serde_json::Value>, ::std::string::String, @@ -11205,6 +11588,7 @@ pub mod builder { cvss_v2: Ok(Default::default()), cvss_v3: Ok(Default::default()), cvss_v4: Ok(Default::default()), + epss: Ok(Default::default()), ssvc_v1: Ok(Default::default()), } } @@ -11252,6 +11636,16 @@ pub mod builder { }); self } + pub fn epss(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option>, + T::Error: ::std::fmt::Display, + { + self.epss = value + .try_into() + .map_err(|e| format!("error converting supplied value for epss: {}", e)); + self + } pub fn ssvc_v1(mut self, value: T) -> Self where T: ::std::convert::TryInto< @@ -11276,6 +11670,7 @@ pub mod builder { cvss_v2: value.cvss_v2?, cvss_v3: value.cvss_v3?, cvss_v4: value.cvss_v4?, + epss: value.epss?, ssvc_v1: value.ssvc_v1?, }) } @@ -11286,6 +11681,7 @@ pub mod builder { cvss_v2: Ok(value.cvss_v2), cvss_v3: Ok(value.cvss_v3), cvss_v4: Ok(value.cvss_v4), + epss: Ok(value.epss), ssvc_v1: Ok(value.ssvc_v1), } } @@ -11785,6 +12181,80 @@ pub mod builder { } } #[derive(Clone, Debug)] + pub struct Epss { + percentile: ::std::result::Result, + probability: ::std::result::Result, + timestamp: ::std::result::Result<::std::string::String, ::std::string::String>, + } + impl ::std::default::Default for Epss { + fn default() -> Self { + Self { + percentile: Err("no value supplied for percentile".to_string()), + probability: Err("no value supplied for probability".to_string()), + timestamp: Err("no value supplied for timestamp".to_string()), + } + } + } + impl Epss { + pub fn percentile(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.percentile = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for percentile: {}", e) + }); + self + } + pub fn probability(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.probability = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for probability: {}", e) + }); + self + } + pub fn timestamp(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + 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::Epss { + type Error = super::error::ConversionError; + fn try_from( + value: Epss, + ) -> ::std::result::Result { + Ok(Self { + percentile: value.percentile?, + probability: value.probability?, + timestamp: value.timestamp?, + }) + } + } + impl ::std::convert::From for Epss { + fn from(value: super::Epss) -> Self { + Self { + percentile: Ok(value.percentile), + probability: Ok(value.probability), + timestamp: Ok(value.timestamp), + } + } + } + #[derive(Clone, Debug)] pub struct FileHash { algorithm: ::std::result::Result< super::AlgorithmOfTheCryptographicHash, @@ -12500,6 +12970,14 @@ pub mod builder { ::std::string::String, >, category: ::std::result::Result, + group_ids: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, + product_ids: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, text: ::std::result::Result, title: ::std::result::Result< ::std::option::Option, @@ -12511,6 +12989,8 @@ pub mod builder { Self { audience: Ok(Default::default()), category: Err("no value supplied for category".to_string()), + group_ids: Ok(Default::default()), + product_ids: Ok(Default::default()), text: Err("no value supplied for text".to_string()), title: Ok(Default::default()), } @@ -12541,6 +13021,30 @@ pub mod builder { }); self } + pub fn group_ids(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option>, + T::Error: ::std::fmt::Display, + { + self.group_ids = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for group_ids: {}", e) + }); + self + } + pub fn product_ids(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option>, + T::Error: ::std::fmt::Display, + { + self.product_ids = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for product_ids: {}", e) + }); + self + } pub fn text(mut self, value: T) -> Self where T: ::std::convert::TryInto, @@ -12572,6 +13076,8 @@ pub mod builder { Ok(Self { audience: value.audience?, category: value.category?, + group_ids: value.group_ids?, + product_ids: value.product_ids?, text: value.text?, title: value.title?, }) @@ -12582,6 +13088,8 @@ pub mod builder { Self { audience: Ok(value.audience), category: Ok(value.category), + group_ids: Ok(value.group_ids), + product_ids: Ok(value.product_ids), text: Ok(value.text), title: Ok(value.title), } @@ -12703,6 +13211,10 @@ pub mod builder { ::std::option::Option, ::std::string::String, >, + unknown: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, } impl ::std::default::Default for ProductStatus { fn default() -> Self { @@ -12715,6 +13227,7 @@ pub mod builder { last_affected: Ok(Default::default()), recommended: Ok(Default::default()), under_investigation: Ok(Default::default()), + unknown: Ok(Default::default()), } } } @@ -12819,6 +13332,18 @@ pub mod builder { }); self } + pub fn unknown(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option>, + T::Error: ::std::fmt::Display, + { + self.unknown = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for unknown: {}", e) + }); + self + } } impl ::std::convert::TryFrom for super::ProductStatus { type Error = super::error::ConversionError; @@ -12834,6 +13359,7 @@ pub mod builder { last_affected: value.last_affected?, recommended: value.recommended?, under_investigation: value.under_investigation?, + unknown: value.unknown?, }) } } @@ -12848,6 +13374,7 @@ pub mod builder { last_affected: Ok(value.last_affected), recommended: Ok(value.recommended), under_investigation: Ok(value.under_investigation), + unknown: Ok(value.unknown), } } } diff --git a/csaf-lib/src/csaf/getter_traits.rs b/csaf-lib/src/csaf/getter_traits.rs index 2c8d0e5..780879f 100644 --- a/csaf-lib/src/csaf/getter_traits.rs +++ b/csaf-lib/src/csaf/getter_traits.rs @@ -37,6 +37,9 @@ pub trait DocumentTrait { /// Type representing document distribution information type DistributionType: DistributionTrait; + /// Type representing document notes + type NoteType: NoteTrait; + /// Returns the tracking information for this document fn get_tracking(&self) -> &Self::TrackingType; @@ -45,6 +48,9 @@ pub trait DocumentTrait { /// Returns the distribution information for this document with CSAF 2.0 semantics fn get_distribution_20(&self) -> Option<&Self::DistributionType>; + + /// Returns the notes associtated with this document + fn get_notes(&self) -> Option<&Vec>; } /// Trait representing distribution information for a document @@ -65,6 +71,8 @@ pub trait DistributionTrait { fn get_tlp_21(&self) -> Result<&Self::TlpType, ValidationError>; } +pub trait NoteTrait: WithGroupIds {} + /// Trait representing sharing group information pub trait SharingGroupTrait { /// Returns the ID of the sharing group @@ -142,15 +150,18 @@ pub trait VulnerabilityTrait { /// The associated type representing the threat information. type ThreatType: ThreatTrait; - /// The associated type representing a vulnerability flag + /// The associated type representing a vulnerability flag. type FlagType: FlagTrait; - /// The associated 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; + /// The associated type representing vulnerability notes. + type NoteType: NoteTrait; + /// Retrieves a list of remediations associated with the vulnerability. fn get_remediations(&self) -> &Vec; @@ -163,23 +174,26 @@ pub trait VulnerabilityTrait { /// Retrieves a list of potential threats related to the vulnerability. fn get_threats(&self) -> &Vec; - /// Returns the date when this vulnerability was initially disclosed + /// Returns the date when this vulnerability was initially disclosed. fn get_disclosure_date(&self) -> &Option; - /// Returns the date when this vulnerability was initially discovered + /// Returns the date when this vulnerability was initially discovered. fn get_discovery_date(&self) -> &Option; - /// Returns all flags associated with this vulnerability + /// Returns all flags associated with this vulnerability. fn get_flags(&self) -> &Option>; - /// Returns all involvements associated with this vulnerability + /// Returns all involvements associated with this vulnerability. fn get_involvements(&self) -> &Option>; - /// Returns the CVE associated with the vulnerability + /// Returns the CVE associated with the vulnerability. fn get_cve(&self) -> Option<&String>; - /// Returns the vulnerability IDs associated with this vulnerability + /// Returns the vulnerability IDs associated with this vulnerability. fn get_ids(&self) -> &Option>; + + /// Returns the notes associated with this vulnerability. + fn get_notes(&self) -> Option<&Vec>; } pub trait VulnerabilityIdTrait { @@ -189,7 +203,7 @@ pub trait VulnerabilityIdTrait { } /// Trait for accessing vulnerability flags information -pub trait FlagTrait { +pub trait FlagTrait: WithGroupIds { /// Returns the date associated with this vulnerability flag fn get_date(&self) -> &Option; } @@ -204,7 +218,7 @@ pub trait InvolvementTrait { /// /// The `RemediationTrait` encapsulates the details of a remediation, such as its /// category and the affected products or groups. -pub trait RemediationTrait { +pub trait RemediationTrait: WithGroupIds { /// Returns the category of the remediation. /// /// Categories are defined by the CSAF schema. @@ -213,9 +227,6 @@ pub trait RemediationTrait { /// 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. /// @@ -342,7 +353,7 @@ pub trait ContentTrait { } /// Trait representing an abstract threat in a CSAF document. -pub trait ThreatTrait { +pub trait ThreatTrait: WithGroupIds { /// Retrieves a list of product IDs associated with this threat, if any. fn get_product_ids(&self) -> Option + '_>; @@ -561,4 +572,9 @@ pub trait ProductIdentificationHelperTrait { fn get_model_numbers(&self) -> Option + '_>; fn get_serial_numbers(&self) -> Option + '_>; +} + +pub trait WithGroupIds { + /// Returns the product group IDs associated with this vulnerability flag + fn get_group_ids(&self) -> Option + '_>; } \ No newline at end of file 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 ad1d9a0..409f33f 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_37.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_37.rs @@ -109,13 +109,14 @@ fn check_datetime(date_time: &String, instance_path: &str) -> Result<(), Validat #[cfg(test)] mod tests { + /* + Ignored because of https://github.com/oasis-tcs/csaf/issues/963 + use crate::csaf::test_helper::run_csaf21_tests; use crate::csaf::validation::ValidationError; 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( From a47b65ca4e1c80a8e05cb0383f7be815512cf646 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Thu, 22 May 2025 10:34:00 +0200 Subject: [PATCH 02/12] Add validation for undefined product_group_id in CSAF documents This adds a new validation function, `test_6_1_04_missing_definition_of_product_group_id`, to ensure all product group IDs used in notes, vulnerabilities, remediations, threats, and flags are defined in the product tree. --- csaf-lib/src/csaf/validations/mod.rs | 1 + csaf-lib/src/csaf/validations/test_6_1_04.rs | 117 +++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 csaf-lib/src/csaf/validations/test_6_1_04.rs diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index 3b16988..4678e5e 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -1,6 +1,7 @@ pub mod test_6_1_01; pub mod test_6_1_02; pub mod test_6_1_03; +pub mod test_6_1_04; pub mod test_6_1_34; pub mod test_6_1_35; diff --git a/csaf-lib/src/csaf/validations/test_6_1_04.rs b/csaf-lib/src/csaf/validations/test_6_1_04.rs new file mode 100644 index 0000000..b77bb17 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_04.rs @@ -0,0 +1,117 @@ +use crate::csaf::getter_traits::{CsafTrait, DocumentTrait, ProductGroupTrait, ProductTreeTrait, VulnerabilityTrait, WithGroupIds}; +use crate::csaf::validation::ValidationError; +use std::collections::HashSet; + +pub fn test_6_1_04_missing_definition_of_product_group_id( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + if let Some(tree) = doc.get_product_tree().as_ref() { + let mut known_groups = HashSet::::new(); + // Collect all known product group IDs + for g in tree.get_product_groups().iter() { + known_groups.insert(g.get_group_id().to_owned()); + } + + // Check document notes + if let Some(notes) = doc.get_document().get_notes() { + for (i_n, note) in notes.iter().enumerate() { + if let Some(group_ids) = note.get_group_ids() { + for (i_g, group_id) in group_ids.enumerate() { + if !known_groups.contains(group_id) { + return Err(ValidationError { + message: format!("Missing definition of product_group_id: {}", group_id), + instance_path: format!("/document/notes/{}/group_ids/{}", i_n, i_g), + }); + } + } + } + } + } + + // Check vulnerabilities + for (i_v, vuln) in doc.get_vulnerabilities().iter().enumerate() { + // Check vulnerability flags + if let Some(flags) = vuln.get_flags() { + for (i_f, flag) in flags.iter().enumerate() { + if let Some(group_ids) = flag.get_group_ids() { + for (i_g, group_id) in group_ids.enumerate() { + if !known_groups.contains(group_id) { + return Err(ValidationError { + message: format!("Missing definition of product_group_id: {}", group_id), + instance_path: format!("/vulnerabilities/{}/flags/{}/group_ids/{}", i_v, i_f, i_g), + }); + } + } + } + } + } + + // Check vulnerability notes + if let Some(notes) = vuln.get_notes() { + for (i_n, note) in notes.iter().enumerate() { + if let Some(group_ids) = note.get_group_ids() { + for (i_g, group_id) in group_ids.enumerate() { + if !known_groups.contains(group_id) { + return Err(ValidationError { + message: format!("Missing definition of product_group_id: {}", group_id), + instance_path: format!("/vulnerabilities/{}/notes/{}/group_ids/{}", i_v, i_n, i_g), + }); + } + } + } + } + } + + // Check vulnerability remediations + for (i_r, remediation) in vuln.get_remediations().iter().enumerate() { + if let Some(group_ids) = remediation.get_group_ids() { + for (i_g, group_id) in group_ids.collect::>().iter().enumerate() { + if !known_groups.contains(*group_id) { + return Err(ValidationError { + message: format!("Missing definition of product_group_id: {}", group_id), + instance_path: format!("/vulnerabilities/{}/remediations/{}/group_ids/{}", i_v, i_r, i_g), + }); + } + } + } + } + + // Check vulnerability threats + for (i_t, threat) in vuln.get_threats().iter().enumerate() { + if let Some(group_ids) = threat.get_group_ids() { + for (i_g, group_id) in group_ids.collect::>().iter().enumerate() { + if !known_groups.contains(*group_id) { + return Err(ValidationError { + message: format!("Missing definition of product_group_id: {}", group_id), + instance_path: format!("/vulnerabilities/{}/threats/{}/group_ids/{}", i_v, i_t, i_g), + }); + } + } + } + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::csaf::test_helper::{run_csaf20_tests, run_csaf21_tests}; + use crate::csaf::validation::ValidationError; + use crate::csaf::validations::test_6_1_04::test_6_1_04_missing_definition_of_product_group_id; + use std::collections::HashMap; + + #[test] + fn test_test_6_1_04() { + let error01 = ValidationError { + message: "Missing definition of product_group_id: CSAFGID-1020301".to_string(), + instance_path: "/vulnerabilities/0/threats/0/group_ids/0".to_string(), + }; + let errors = HashMap::from([ + ("01", &error01) + ]); + run_csaf20_tests("04", test_6_1_04_missing_definition_of_product_group_id, &errors); + run_csaf21_tests("04", test_6_1_04_missing_definition_of_product_group_id, &errors); + } +} \ No newline at end of file From 3d2730ef338fa8d35813f0f7ac7737b7d4ca596c Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Thu, 22 May 2025 10:57:59 +0200 Subject: [PATCH 03/12] Add validation for duplicate product group IDs (test_6_1_05) Introduce a new validation to detect multiple definitions of the same product group ID in CSAF documents. This ensures data integrity by identifying conflicts within the `product_tree` structure. Includes corresponding unit tests for CSAF 2.0 and 2.1 versions. --- csaf-lib/src/csaf/validations/mod.rs | 1 + csaf-lib/src/csaf/validations/test_6_1_05.rs | 46 ++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 csaf-lib/src/csaf/validations/test_6_1_05.rs diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index 4678e5e..dc315ad 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -2,6 +2,7 @@ pub mod test_6_1_01; pub mod test_6_1_02; pub mod test_6_1_03; pub mod test_6_1_04; +pub mod test_6_1_05; pub mod test_6_1_34; pub mod test_6_1_35; diff --git a/csaf-lib/src/csaf/validations/test_6_1_05.rs b/csaf-lib/src/csaf/validations/test_6_1_05.rs new file mode 100644 index 0000000..b56d7b9 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_05.rs @@ -0,0 +1,46 @@ +use crate::csaf::getter_traits::{CsafTrait, ProductGroupTrait, ProductTrait, ProductTreeTrait}; +use crate::csaf::validation::ValidationError; +use std::collections::HashSet; + +pub fn test_6_1_05_multiple_definition_of_product_group_id( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + // Map to store each key with all of its paths + let mut conflicts = HashSet::::new(); + + if let Some(tree) = doc.get_product_tree().as_ref() { + for (i_g, g) in tree.get_product_groups().iter().enumerate() { + if conflicts.contains(g.get_group_id()) { + return Err(ValidationError { + message: format!("Duplicate definition for product group ID {}", g.get_group_id()), + instance_path: format!("/product_tree/product_groups/{}/group_id", i_g), + }) + } else { + conflicts.insert(g.get_group_id().to_owned()); + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::csaf::test_helper::{run_csaf20_tests, run_csaf21_tests}; + use crate::csaf::validation::ValidationError; + use crate::csaf::validations::test_6_1_05::test_6_1_05_multiple_definition_of_product_group_id; + use std::collections::HashMap; + + #[test] + fn test_test_6_1_02() { + let error01 = ValidationError { + message: "Duplicate definition for product group ID CSAFGID-1020300".to_string(), + instance_path: "/product_tree/product_groups/1/group_id".to_string(), + }; + let errors = HashMap::from([ + ("01", &error01) + ]); + run_csaf20_tests("05", test_6_1_05_multiple_definition_of_product_group_id, &errors); + run_csaf21_tests("05", test_6_1_05_multiple_definition_of_product_group_id, &errors); + } +} From 4aed9a518f4ddb76519e59599603ce673d76d838 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Thu, 22 May 2025 11:02:18 +0200 Subject: [PATCH 04/12] Add validation for conflicting remediation, minor cleanup Extended the test to include a case where a product listed as fixed has a conflicting remediation category of "no_fix_planned". Ensures better coverage and accuracy in remediation validation logic. --- csaf | 2 +- csaf-lib/src/csaf/validations/test_6_1_05.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_36.rs | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/csaf b/csaf index 1726fcf..3db72bd 160000 --- a/csaf +++ b/csaf @@ -1 +1 @@ -Subproject commit 1726fcf10d6e444e6e65a696ac9198c981858d23 +Subproject commit 3db72bd13f28dc0d12798c4f61a7d5c1bccd96b0 diff --git a/csaf-lib/src/csaf/validations/test_6_1_05.rs b/csaf-lib/src/csaf/validations/test_6_1_05.rs index b56d7b9..1387a5c 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_05.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_05.rs @@ -1,4 +1,4 @@ -use crate::csaf::getter_traits::{CsafTrait, ProductGroupTrait, ProductTrait, ProductTreeTrait}; +use crate::csaf::getter_traits::{CsafTrait, ProductGroupTrait, ProductTreeTrait}; use crate::csaf::validation::ValidationError; use std::collections::HashSet; diff --git a/csaf-lib/src/csaf/validations/test_6_1_36.rs b/csaf-lib/src/csaf/validations/test_6_1_36.rs index a771e8b..2931b31 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_36.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_36.rs @@ -104,6 +104,10 @@ mod tests { message: "Product CSAFPID-9080700 is listed as affected but has conflicting remediation category optional_patch".to_string(), instance_path: "/vulnerabilities/0/remediations/0".to_string(), }), + ("04", &ValidationError { + message: "Product CSAFPID-9080700 is listed as fixed but has conflicting remediation category no_fix_planned".to_string(), + instance_path: "/vulnerabilities/0/remediations/0".to_string(), + }), ]), ); } From bd4103ade93d6246eb791222230fa92074c54aa6 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Thu, 22 May 2025 12:57:20 +0200 Subject: [PATCH 05/12] Update RFC3339 regex to disallow leap seconds Revised the date-time validation regex to exclude leap seconds, ensuring stricter compliance with RFC3339. Updated test cases and error messages to reflect the change and improve clarity for non-compliant date-time issues. --- csaf-lib/src/csaf/validations/test_6_1_37.rs | 35 +++++++++++--------- 1 file changed, 20 insertions(+), 15 deletions(-) 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 409f33f..f2a4b36 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_37.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_37.rs @@ -4,7 +4,7 @@ use regex::Regex; use std::sync::LazyLock; 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() + Regex::new(r"^((\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:(?:[0-4]\d|5[0-9])(?:\.\d+)?)(Z|[+-]\d{2}:\d{2}))$").unwrap() ); /// Validates that all date/time fields in the CSAF document conform to the required format @@ -17,13 +17,13 @@ pub fn test_6_1_37_date_and_time( ) -> Result<(), ValidationError> { let tracking = doc.get_document().get_tracking(); - // Check initial release date + // Check the initial release date check_datetime(tracking.get_initial_release_date(), "/document/tracking/initial_release_date")?; - // Check current release date + // Check the current release date check_datetime(tracking.get_current_release_date(), "/document/tracking/current_release_date")?; - // Check generator date if present + // Check the generator date if present if let Some(generator) = tracking.get_generator() { if let Some(date) = generator.get_date() { check_datetime(date, "/document/tracking/generator/date")?; @@ -38,14 +38,14 @@ pub fn test_6_1_37_date_and_time( )?; } - // Check vulnerability related dates + // 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_disclosure_date() { check_datetime(date, &format!("/vulnerabilities/{}/disclosure_date", i_v))?; } - // Check discovery date if present + // Check the discovery date if present if let Some(date) = vuln.get_discovery_date() { check_datetime(date, &format!("/vulnerabilities/{}/discovery_date", i_v))?; } @@ -59,7 +59,7 @@ pub fn test_6_1_37_date_and_time( } } - // Check involvements dates if present + // Check involvement dates if present if let Some(involvements) = vuln.get_involvements() { for (i_i, involvement) in involvements.iter().enumerate() { if let Some(date) = involvement.get_date() { @@ -71,14 +71,14 @@ pub fn test_6_1_37_date_and_time( } } - // Check remediations dates if present + // Check remediation dates if present for (i_r, remediation) in vuln.get_remediations().iter().enumerate() { if let Some(date) = remediation.get_date() { check_datetime(date, &format!("/vulnerabilities/{}/remediations/{}/date", i_v, i_r))?; } } - // Check threats dates if present + // Check threat dates if present for (i_t, threat) in vuln.get_threats().iter().enumerate() { if let Some(date) = threat.get_date() { check_datetime(date, &format!("/vulnerabilities/{}/threats/{}/date", i_v, i_t))?; @@ -101,7 +101,7 @@ fn check_datetime(date_time: &String, instance_path: &str) -> Result<(), Validat } } else { Err(ValidationError { - message: format!("Invalid date-time string {}, expected RFC3339-compliant format with non-empty timezone", date_time), + message: format!("Invalid date-time string {}, expected RFC3339-compliant format with non-empty timezone and no leap seconds", date_time), instance_path: instance_path.to_string(), }) } @@ -123,16 +123,16 @@ mod tests { "37", test_6_1_37_date_and_time, &HashMap::from([ ("01", &ValidationError { - message: "Invalid date-time string 2024-01-24 10:00:00.000Z, expected RFC3339-compliant format with non-empty timezone".to_string(), + message: "Invalid date-time string 2024-01-24 10:00:00.000Z, expected RFC3339-compliant format with non-empty timezone and no leap seconds".to_string(), instance_path: "/document/tracking/initial_release_date".to_string(), }), ("02", &ValidationError { - message: "Invalid date-time string 2024-01-24T10:00:00.000z, expected RFC3339-compliant format with non-empty timezone".to_string(), + message: "Invalid date-time string 2024-01-24T10:00:00.000z, expected RFC3339-compliant format with non-empty timezone and no leap seconds".to_string(), instance_path: "/document/tracking/initial_release_date".to_string(), }), ("03", &ValidationError { - message: "Date-time string 2014-13-31T00: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: "Invalid date-time string 2017-01-01T02:59:60+04:00, expected RFC3339-compliant format with non-empty timezone and no leap seconds".to_string(), + instance_path: "/vulnerabilities/0/disclosure_date".to_string(), }), ("04", &ValidationError { 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(), @@ -142,7 +142,12 @@ mod tests { 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(), }), + ("06", &ValidationError { + message: "Invalid date-time string 2016-12-31T00:00:60+23:59, expected RFC3339-compliant format with non-empty timezone and no leap seconds".to_string(), + instance_path: "/vulnerabilities/0/disclosure_date".to_string(), + }), ]) ); - }*/ + } + */ } From 7d3cad280d63b706570440b7ee11002b524ba9d8 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Mon, 26 May 2025 13:57:58 +0200 Subject: [PATCH 06/12] Add validation for conflicting product status groups. This commit introduces `test_6_1_06_contradicting_product_status` to verify that no product has contradictory status groups (e.g., affected vs. not affected). Includes error handling, tests, and updates to the module index. --- .../csaf/csaf2_0/getter_implementations.rs | 5 + .../csaf/csaf2_1/getter_implementations.rs | 4 + csaf-lib/src/csaf/getter_traits.rs | 3 + csaf-lib/src/csaf/validations/mod.rs | 1 + csaf-lib/src/csaf/validations/test_6_1_06.rs | 165 ++++++++++++++++++ 5 files changed, 178 insertions(+) create mode 100644 csaf-lib/src/csaf/validations/test_6_1_06.rs diff --git a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs index 8e6907d..ef8c7c3 100644 --- a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs @@ -74,6 +74,11 @@ impl ProductStatusTrait for ProductStatus { fn get_under_investigation(&self) -> Option + '_> { self.under_investigation.as_ref().map(|p| (*p).iter().map(|x| x.deref())) } + + /// Not specified for CSAF 2.0, so `None` + fn get_unknown(&self) -> Option + '_> { + None::> + } } impl MetricTrait for () { diff --git a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs index f8c4315..79dcdbf 100644 --- a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs @@ -57,6 +57,10 @@ impl ProductStatusTrait for ProductStatus { fn get_under_investigation(&self) -> Option + '_> { self.under_investigation.as_ref().map(|p| (*p).iter().map(|x| x.deref())) } + + fn get_unknown(&self) -> Option + '_> { + self.unknown.as_ref().map(|p| (*p).iter().map(|x| x.deref())) + } } impl MetricTrait for Metric { diff --git a/csaf-lib/src/csaf/getter_traits.rs b/csaf-lib/src/csaf/getter_traits.rs index 780879f..dc62ac7 100644 --- a/csaf-lib/src/csaf/getter_traits.rs +++ b/csaf-lib/src/csaf/getter_traits.rs @@ -283,6 +283,9 @@ pub trait ProductStatusTrait { /// Returns a reference to the list of product IDs currently under investigation. fn get_under_investigation(&self) -> Option + '_>; + + /// Return a reference to the list of product IDs with unknown status. + fn get_unknown(&self) -> Option + '_>; /// Combines all affected product IDs into a `HashSet`. /// diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index dc315ad..91e0742 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -3,6 +3,7 @@ pub mod test_6_1_02; pub mod test_6_1_03; pub mod test_6_1_04; pub mod test_6_1_05; +pub mod test_6_1_06; pub mod test_6_1_34; pub mod test_6_1_35; diff --git a/csaf-lib/src/csaf/validations/test_6_1_06.rs b/csaf-lib/src/csaf/validations/test_6_1_06.rs new file mode 100644 index 0000000..a8b4fbb --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_06.rs @@ -0,0 +1,165 @@ +use crate::csaf::getter_traits::{CsafTrait, ProductStatusTrait, VulnerabilityTrait}; +use crate::csaf::validation::ValidationError; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; + +/// Contradiction Product Status Groups +#[derive(PartialEq, Clone)] +enum ProductStatusGroup { + // first_affected, known_affected, last_affected + Affected, + // known_not_affected + NotAffected, + // first_fixed, fixed + Fixed, + // under_investigation + UnderInvestigation, + // unknown + Unknown, +} + +impl Display for ProductStatusGroup { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ProductStatusGroup::Affected => write!(f, "affected"), + ProductStatusGroup::NotAffected => write!(f, "not affected"), + ProductStatusGroup::Fixed => write!(f, "fixed"), + ProductStatusGroup::UnderInvestigation => write!(f, "under investigation"), + ProductStatusGroup::Unknown => write!(f, "unknown"), + } + } +} + +pub fn test_6_1_06_contradicting_product_status( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + for (v_i, v) in doc.get_vulnerabilities().iter().enumerate() { + if let Some(product_status) = v.get_product_status() { + // Map of product IDs to product status groups (mutually exclusive, therefore only one allowed) + let mut product_statuses: HashMap = HashMap::new(); + + // Handle all products with an "affected" status - these don't need conflict checking + for pid in product_status.get_all_affected() { + product_statuses.insert(pid.to_owned(), ProductStatusGroup::Affected); + } + + // Handle all other status groups with conflict checking + check_status_group( + v_i, + &mut product_statuses, + product_status.get_known_not_affected(), + ProductStatusGroup::NotAffected, + "known_not_affected", + )?; + + check_status_group( + v_i, + &mut product_statuses, + product_status.get_first_fixed(), + ProductStatusGroup::Fixed, + "first_fixed", + )?; + check_status_group( + v_i, + &mut product_statuses, + product_status.get_fixed(), + ProductStatusGroup::Fixed, + "fixed", + )?; + + check_status_group( + v_i, + &mut product_statuses, + product_status.get_under_investigation(), + ProductStatusGroup::UnderInvestigation, + "under_investigation", + )?; + + check_status_group( + v_i, + &mut product_statuses, + product_status.get_unknown(), + ProductStatusGroup::Unknown, + "unknown", + )?; + } + } + Ok(()) +} + +// Helper function to check for status group conflicts +fn check_status_group<'a>( + v_i: usize, + product_statuses: &mut HashMap, + product_ids: Option>, + status_group: ProductStatusGroup, + field_name: &str, +) -> Result<(), ValidationError> { + if let Some(products) = product_ids { + for (i_pid, pid) in products.into_iter().enumerate() { + match product_statuses.get(pid) { + None => { + product_statuses.insert(pid.to_owned(), status_group.clone()); + } + Some(existing_status) => { + if *existing_status != status_group { + return Err(ValidationError { + message: format!( + "Product {} is marked with product status group \"{}\" but has conflicting product status belonging to group \"{}\"", + pid, + status_group.to_string(), + existing_status.to_string() + ), + instance_path: format!("/vulnerabilities/{}/product_status/{}/{}", v_i, field_name, i_pid), + }); + } + } + } + } + } + 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_06::test_6_1_06_contradicting_product_status; + use std::collections::HashMap; + + #[test] + fn test_test_6_1_06() { + let first_error_message = "Product CSAFPID-9080700 is marked with product status group \"not affected\" but has conflicting product status belonging to group \"affected\""; + let first_error_path = "/vulnerabilities/0/product_status/known_not_affected/0"; + run_csaf21_tests( + "06", + test_6_1_06_contradicting_product_status, + &HashMap::from([ + ("01", &ValidationError { + message: first_error_message.to_string(), + instance_path: first_error_path.to_string() + }), + ("02", &ValidationError { + message: first_error_message.to_string(), + instance_path: first_error_path.to_string() + }), + ("03", &ValidationError { + message: first_error_message.to_string(), + instance_path: first_error_path.to_string() + }), + ("04", &ValidationError { + message: "Product CSAFPID-9080701 is marked with product status group \"fixed\" but has conflicting product status belonging to group \"not affected\"".to_string(), + instance_path: "/vulnerabilities/0/product_status/fixed/0".to_string(), + }), + ("05", &ValidationError { + message: "Product CSAFPID-9080702 is marked with product status group \"fixed\" but has conflicting product status belonging to group \"affected\"".to_string(), + instance_path: "/vulnerabilities/0/product_status/first_fixed/0".to_string(), + }), + ("06", &ValidationError { + message: "Product CSAFPID-9080700 is marked with product status group \"unknown\" but has conflicting product status belonging to group \"affected\"".to_string(), + instance_path: "/vulnerabilities/0/product_status/unknown/0".to_string(), + }), + ]), + ); + } +} From a9e4a4fb00552ab05e4a2fa2d6252d103315cc11 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Wed, 28 May 2025 18:40:15 +0200 Subject: [PATCH 07/12] Add support for accessing CVSS, EPSS, and content paths in traits Expanded `ContentTrait` and related implementations to include methods for accessing CVSS v2/v3/v4, EPSS, and JSON content paths. Updated CSAF 2.0 and 2.1 schema integrations with new fields and improved consistency in metrics handling. --- .../csaf/csaf2_0/getter_implementations.rs | 65 ++++++++++++++----- .../csaf/csaf2_1/getter_implementations.rs | 48 ++++++++++++-- csaf-lib/src/csaf/getter_traits.rs | 16 ++++- 3 files changed, 105 insertions(+), 24 deletions(-) diff --git a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs index ef8c7c3..980c2fb 100644 --- a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs @@ -1,8 +1,9 @@ -use crate::csaf::csaf2_0::schema::{Branch, CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, DocumentGenerator, DocumentLevelMetaData, DocumentStatus, Flag, FullProductNameT, HelperToIdentifyTheProduct, Id, Involvement, LabelOfTlp, Note, 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::csaf2_0::schema::{Branch, CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, DocumentGenerator, DocumentLevelMetaData, DocumentStatus, Flag, FullProductNameT, HelperToIdentifyTheProduct, Id, Involvement, LabelOfTlp, Note, ProductGroup, ProductStatus, ProductTree, Relationship, Remediation, Revision, RulesForSharingDocument, Score, Threat, Tracking, TrafficLightProtocolTlp, Vulnerability}; +use crate::csaf::csaf2_1::schema::{CategoryOfTheRemediation as Remediation21, DocumentStatus as Status21, Epss, 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, VulnerabilityIdTrait, NoteTrait, WithGroupIds}; use std::ops::Deref; use serde::de::Error; +use serde_json::{Map, Value}; use crate::csaf::csaf2_1::ssvc_schema::SsvcV1; use crate::csaf::validation::ValidationError; @@ -81,27 +82,56 @@ impl ProductStatusTrait for ProductStatus { } } -impl MetricTrait for () { - type ContentType = (); +impl MetricTrait for Score { + type ContentType = Score; - //noinspection RsConstantConditionIf fn get_products(&self) -> impl Iterator + '_ { - // This construction is required to satisfy compiler checks - // and still panic if this is ever called (as this would be a clear error!). - if true { - panic!("Metrics are not implemented in CSAF 2.0"); - } - std::iter::empty() + self.products.iter().map(|x| x.deref()) } fn get_content(&self) -> &Self::ContentType { - panic!("Metrics are not implemented in CSAF 2.0"); + self } } -impl ContentTrait for () { +impl ContentTrait for Score { + fn has_ssvc_v1(&self) -> bool { + false + } + fn get_ssvc_v1(&self) -> Result { - Err(serde_json::Error::custom("Metrics are not implemented in CSAF 2.0")) + Err(serde_json::Error::custom("SSVC metrics are not implemented in CSAF 2.0")) + } + + fn get_cvss_v2(&self) -> Option<&Map> { + if self.cvss_v2.is_empty() { + None + } else { + Some(&self.cvss_v2) + } + } + + fn get_cvss_v3(&self) -> Option<&Map> { + if self.cvss_v3.is_empty() { + None + } else { + Some(&self.cvss_v3) + } + } + + fn get_cvss_v4(&self) -> Option<&Map> { + None + } + + fn get_epss(&self) -> &Option { + &None:: + } + + fn get_content_json_path(&self, vulnerability_idx: usize, metric_idx: usize) -> String { + format!( + "/vulnerabilities/{}/scores/{}", + vulnerability_idx, metric_idx + ) } } @@ -125,7 +155,7 @@ impl VulnerabilityTrait for Vulnerability { type RemediationType = Remediation; type ProductStatusType = ProductStatus; // Metrics are not implemented in CSAF 2.0 - type MetricType = (); + type MetricType = Score; type ThreatType = Threat; type FlagType = Flag; type InvolvementType = Involvement; @@ -140,9 +170,8 @@ impl VulnerabilityTrait for Vulnerability { &self.product_status } - fn get_metrics(&self) -> &Option> { - // Metrics are not implemented in CSAF 2.0 - &None + fn get_metrics(&self) -> Option<&Vec> { + Some(&self.scores) } fn get_threats(&self) -> &Vec { diff --git a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs index 79dcdbf..39b22da 100644 --- a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs @@ -1,7 +1,7 @@ -use crate::csaf::csaf2_1::schema::{Branch, CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, Content, DocumentGenerator, DocumentLevelMetaData, DocumentStatus, Flag, FullProductNameT, HelperToIdentifyTheProduct, Id, Involvement, LabelOfTlp, Metric, Note, ProductGroup, ProductStatus, ProductTree, Relationship, Remediation, Revision, RulesForSharingDocument, SharingGroup, Threat, Tracking, TrafficLightProtocolTlp, Vulnerability}; +use crate::csaf::csaf2_1::schema::{Branch, CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, Content, DocumentGenerator, DocumentLevelMetaData, DocumentStatus, Epss, Flag, FullProductNameT, HelperToIdentifyTheProduct, Id, Involvement, LabelOfTlp, Metric, Note, 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, NoteTrait, WithGroupIds}; use std::ops::Deref; -use serde_json::Value; +use serde_json::{Map, Value}; use crate::csaf::csaf2_1::ssvc_schema::SsvcV1; use crate::csaf::validation::ValidationError; @@ -76,10 +76,50 @@ impl MetricTrait for Metric { } impl ContentTrait for Content { + fn has_ssvc_v1(&self) -> bool { + !self.ssvc_v1.is_empty() + } + fn get_ssvc_v1(&self) -> Result { let ssvc_value = Value::Object(self.ssvc_v1.clone()); serde_json::from_value::(ssvc_value) } + + fn get_cvss_v2(&self) -> Option<&Map> { + if self.cvss_v2.is_empty() { + None + } else { + Some(&self.cvss_v2) + } + } + + fn get_cvss_v3(&self) -> Option<&Map> { + if self.cvss_v3.is_empty() { + None + } else { + Some(&self.cvss_v3) + } + } + + fn get_cvss_v4(&self) -> Option<&Map> { + if self.cvss_v4.is_empty() { + None + } else { + Some(&self.cvss_v4) + } + } + + fn get_epss(&self) -> &Option { + &self.epss + } + + fn get_content_json_path(&self, vulnerability_idx: usize, metric_idx: usize) -> String { + format!( + "/vulnerabilities/{}/metrics/{}/content", + vulnerability_idx, + metric_idx, + ) + } } impl WithGroupIds for Threat { @@ -116,8 +156,8 @@ impl VulnerabilityTrait for Vulnerability { &self.product_status } - fn get_metrics(&self) -> &Option> { - &self.metrics + fn get_metrics(&self) -> Option<&Vec> { + self.metrics.as_ref() } fn get_threats(&self) -> &Vec { diff --git a/csaf-lib/src/csaf/getter_traits.rs b/csaf-lib/src/csaf/getter_traits.rs index dc62ac7..b86a838 100644 --- a/csaf-lib/src/csaf/getter_traits.rs +++ b/csaf-lib/src/csaf/getter_traits.rs @@ -1,5 +1,5 @@ use std::collections::{BTreeSet, HashSet}; -use crate::csaf::csaf2_1::schema::{CategoryOfTheRemediation, DocumentStatus, LabelOfTlp}; +use crate::csaf::csaf2_1::schema::{CategoryOfTheRemediation, DocumentStatus, Epss, LabelOfTlp}; use crate::csaf::csaf2_1::ssvc_schema::SsvcV1; use crate::csaf::helpers::resolve_product_groups; use crate::csaf::validation::ValidationError; @@ -169,7 +169,7 @@ pub trait VulnerabilityTrait { fn get_product_status(&self) -> &Option; /// Returns an optional vector of metrics related to the vulnerability. - fn get_metrics(&self) -> &Option>; + fn get_metrics(&self) -> Option<&Vec>; /// Retrieves a list of potential threats related to the vulnerability. fn get_threats(&self) -> &Vec; @@ -352,7 +352,19 @@ pub trait MetricTrait { } pub trait ContentTrait { + fn has_ssvc_v1(&self) -> bool; + fn get_ssvc_v1(&self) -> Result; + + fn get_cvss_v2(&self) -> Option<&serde_json::Map>; + + fn get_cvss_v3(&self) -> Option<&serde_json::Map>; + + fn get_cvss_v4(&self) -> Option<&serde_json::Map>; + + fn get_epss(&self) -> &Option; + + fn get_content_json_path(&self, vulnerability_idx: usize, metric_idx: usize) -> String; } /// Trait representing an abstract threat in a CSAF document. From 107c7b4d1a0182a6f1dbb810119f7c729c41b199 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Wed, 28 May 2025 18:41:54 +0200 Subject: [PATCH 08/12] Add validation for duplicate vulnerability metrics check Introduced a new validation test (test_6_1_07) to ensure no product is assigned the same type of vulnerability metric multiple times. This includes support for various metrics like CVSS and EPSS, with detailed error handling and unit tests for CSAF 2.1 compliance. --- csaf-lib/src/csaf/validations/mod.rs | 1 + csaf-lib/src/csaf/validations/test_6_1_07.rs | 145 +++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 csaf-lib/src/csaf/validations/test_6_1_07.rs diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index 91e0742..dac6ada 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -4,6 +4,7 @@ pub mod test_6_1_03; pub mod test_6_1_04; pub mod test_6_1_05; pub mod test_6_1_06; +pub mod test_6_1_07; pub mod test_6_1_34; pub mod test_6_1_35; diff --git a/csaf-lib/src/csaf/validations/test_6_1_07.rs b/csaf-lib/src/csaf/validations/test_6_1_07.rs new file mode 100644 index 0000000..db9113f --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_07.rs @@ -0,0 +1,145 @@ +use std::collections::{HashMap, HashSet}; +use crate::csaf::getter_traits::{ContentTrait, CsafTrait, MetricTrait, VulnerabilityTrait}; +use crate::csaf::validation::ValidationError; +use std::fmt::{Display, Formatter}; +use crate::csaf::validations::test_6_1_07::VulnerabilityMetrics::{CvssV2, CvssV30, CvssV31, CvssV4, SsvcV1, Epss}; + +/// Types of metrics known until CSAF 2.1 +#[derive(Hash, Eq, PartialEq, Clone)] +enum VulnerabilityMetrics { + SsvcV1, + CvssV2, + CvssV30, + CvssV31, + CvssV4, + Epss, +} + +impl Display for VulnerabilityMetrics { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SsvcV1 => write!(f, "SSVC-v1"), + CvssV2 => write!(f, "CVSS-v2"), + CvssV30 => write!(f, "CVSS-v3.0"), + CvssV31 => write!(f, "CVSS-v3.1"), + CvssV4 => write!(f, "CVSS-v4"), + Epss => write!(f, "EPSS"), + } + } +} + +fn get_metric_prop_name(metric: &VulnerabilityMetrics) -> &'static str { + match metric { + SsvcV1 => "ssvc_v1", + CvssV2 => "cvss_v2", + CvssV30 => "cvss_v3", + CvssV31 => "cvss_v3", + CvssV4 => "cvss_v4", + Epss => "epss", + } +} + +pub fn test_6_1_07_multiple_same_scores_per_product( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + for (v_i, v) in doc.get_vulnerabilities().iter().enumerate() { + let mut seen_metrics: HashMap> = HashMap::new(); + if let Some(metrics) = v.get_metrics() { + for (m_i, m) in metrics.iter().enumerate() { + let content = m.get_content(); + let mut content_metrics = Vec::::new(); + if content.has_ssvc_v1() { + content_metrics.push(SsvcV1); + } + if content.get_cvss_v2().is_some() { + content_metrics.push(CvssV2); + } + if let Some(cvss_v3) = content.get_cvss_v3() { + if let Some(version) = cvss_v3.get("version") { + if version == "3.1" { + content_metrics.push(CvssV31); + } else if version == "3.0" { + content_metrics.push(CvssV30); + } else { + return Err(ValidationError { + message: format!("CVSS-v3 version {} is not supported.", version), + instance_path: format!( + "{}/{}", + content.get_content_json_path(v_i, m_i), + get_metric_prop_name(&CvssV30), + ), + }); + } + } + } + if content.get_cvss_v4().is_some() { + content_metrics.push(CvssV4); + } + if content.get_epss().is_some() { + content_metrics.push(Epss); + } + for p in m.get_products() { + let metrics_set = seen_metrics.entry(p.to_string()).or_insert_with(|| HashSet::new()); + for cm in content_metrics.iter() { + if metrics_set.contains(cm) { + return Err(ValidationError { + message: format!( + "Product {} already has another metric \"{}\" assigned.", + p, + cm, + ), + instance_path: format!( + "{}/{}", + content.get_content_json_path(v_i, m_i), + get_metric_prop_name(cm)), + }); + } else { + metrics_set.insert(cm.to_owned()); + } + } + } + } + } + } + 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_07::test_6_1_07_multiple_same_scores_per_product; + use std::collections::HashMap; + + #[test] + fn test_test_6_1_07() { + let cvss_v31_error_message = "Product CSAFPID-9080700 already has another metric \"CVSS-v3.1\" assigned."; + let cvss_v3_error_path = "/vulnerabilities/0/metrics/1/content/cvss_v3"; + run_csaf21_tests( + "07", + test_6_1_07_multiple_same_scores_per_product, + &HashMap::from([ + ("01", &ValidationError { + message: cvss_v31_error_message.to_string(), + instance_path: cvss_v3_error_path.to_string() + }), + ("02", &ValidationError { + message: "Product CSAFPID-9080700 already has another metric \"CVSS-v3.0\" assigned.".to_string(), + instance_path: cvss_v3_error_path.to_string() + }), + ("03", &ValidationError { + message: "Product CSAFPID-9080700 already has another metric \"CVSS-v2\" assigned.".to_string(), + instance_path: "/vulnerabilities/0/metrics/1/content/cvss_v2".to_string() + }), + ("04", &ValidationError { + message: "Product CSAFPID-9080700 already has another metric \"CVSS-v4\" assigned.".to_string(), + instance_path: "/vulnerabilities/0/metrics/1/content/cvss_v4".to_string(), + }), + ("05", &ValidationError { + message: cvss_v31_error_message.to_string(), + instance_path: cvss_v3_error_path.to_string(), + }), + ]), + ); + } +} From 0d6c4f84fdfc069f32c890efe7c19d380b14d220 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Wed, 28 May 2025 18:49:12 +0200 Subject: [PATCH 09/12] Add support for CSAF 2.0 tests in test_6_1_07 validation. This update introduces `run_csaf20_tests` to validate CSAF 2.0 cases alongside CSAF 2.1. Common path prefixes are refactored for clarity and reuse, ensuring consistent test implementation across versions. --- csaf-lib/src/csaf/validations/test_6_1_07.rs | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/csaf-lib/src/csaf/validations/test_6_1_07.rs b/csaf-lib/src/csaf/validations/test_6_1_07.rs index db9113f..85dacdd 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_07.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_07.rs @@ -106,7 +106,7 @@ pub fn test_6_1_07_multiple_same_scores_per_product( #[cfg(test)] mod tests { - use crate::csaf::test_helper::run_csaf21_tests; + use crate::csaf::test_helper::{run_csaf20_tests, run_csaf21_tests}; use crate::csaf::validation::ValidationError; use crate::csaf::validations::test_6_1_07::test_6_1_07_multiple_same_scores_per_product; use std::collections::HashMap; @@ -114,30 +114,41 @@ mod tests { #[test] fn test_test_6_1_07() { let cvss_v31_error_message = "Product CSAFPID-9080700 already has another metric \"CVSS-v3.1\" assigned."; - let cvss_v3_error_path = "/vulnerabilities/0/metrics/1/content/cvss_v3"; + let csaf_20_path_prefix = "/vulnerabilities/0/scores/1"; + let csaf_21_path_prefix = "/vulnerabilities/0/metrics/1/content"; + run_csaf20_tests( + "07", + test_6_1_07_multiple_same_scores_per_product, + &HashMap::from([ + ("01", &ValidationError { + message: cvss_v31_error_message.to_string(), + instance_path: format!("{}/cvss_v3", csaf_20_path_prefix), + }), + ]), + ); run_csaf21_tests( "07", test_6_1_07_multiple_same_scores_per_product, &HashMap::from([ ("01", &ValidationError { message: cvss_v31_error_message.to_string(), - instance_path: cvss_v3_error_path.to_string() + instance_path: format!("{}/cvss_v3", csaf_21_path_prefix), }), ("02", &ValidationError { message: "Product CSAFPID-9080700 already has another metric \"CVSS-v3.0\" assigned.".to_string(), - instance_path: cvss_v3_error_path.to_string() + instance_path: format!("{}/cvss_v3", csaf_21_path_prefix), }), ("03", &ValidationError { message: "Product CSAFPID-9080700 already has another metric \"CVSS-v2\" assigned.".to_string(), - instance_path: "/vulnerabilities/0/metrics/1/content/cvss_v2".to_string() + instance_path: format!("{}/cvss_v2", csaf_21_path_prefix), }), ("04", &ValidationError { message: "Product CSAFPID-9080700 already has another metric \"CVSS-v4\" assigned.".to_string(), - instance_path: "/vulnerabilities/0/metrics/1/content/cvss_v4".to_string(), + instance_path: format!("{}/cvss_v4", csaf_21_path_prefix), }), ("05", &ValidationError { message: cvss_v31_error_message.to_string(), - instance_path: cvss_v3_error_path.to_string(), + instance_path: format!("{}/cvss_v3", csaf_21_path_prefix), }), ]), ); From 36066701cbf446aa291f29c3e0880fd00879b1c0 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Tue, 3 Jun 2025 12:55:41 +0200 Subject: [PATCH 10/12] Update CSAF schema enforcing stricter validation rules Replaced JSON schema reference and file name for CSAF v2.1. Introduced `additionalProperties: false` across the schema to ensure no extraneous fields are allowed. Added new fields such as `first_known_exploitation_dates`, `license_expression`, and others. Updated deserialization logic with `deny_unknown_fields` to improve validation rigor. --- csaf | 2 +- csaf-lib/build.rs | 2 +- .../{csaf_json_schema.json => csaf.json} | 175 +++- csaf-lib/src/csaf/csaf2_1/loader.rs | 2 +- csaf-lib/src/csaf/csaf2_1/schema.rs | 972 +++++++++++++++--- ssvc | 2 +- 6 files changed, 986 insertions(+), 169 deletions(-) rename csaf-lib/src/csaf/csaf2_1/{csaf_json_schema.json => csaf.json} (92%) diff --git a/csaf b/csaf index 3db72bd..bf62791 160000 --- a/csaf +++ b/csaf @@ -1 +1 @@ -Subproject commit 3db72bd13f28dc0d12798c4f61a7d5c1bccd96b0 +Subproject commit bf6279113d14f731aa92f2e0fd6944c8938a2d6c diff --git a/csaf-lib/build.rs b/csaf-lib/build.rs index 48c05e9..0bfe036 100644 --- a/csaf-lib/build.rs +++ b/csaf-lib/build.rs @@ -29,7 +29,7 @@ fn main() -> Result<(), BuildError> { false, )?; build( - "./src/csaf/csaf2_1/csaf_json_schema.json", + "./src/csaf/csaf2_1/csaf.json", "csaf/csaf2_1/schema.rs", true, )?; diff --git a/csaf-lib/src/csaf/csaf2_1/csaf_json_schema.json b/csaf-lib/src/csaf/csaf2_1/csaf.json similarity index 92% rename from csaf-lib/src/csaf/csaf2_1/csaf_json_schema.json rename to csaf-lib/src/csaf/csaf2_1/csaf.json index 8ad48dc..5b3e79b 100644 --- a/csaf-lib/src/csaf/csaf2_1/csaf_json_schema.json +++ b/csaf-lib/src/csaf/csaf2_1/csaf.json @@ -1,6 +1,6 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json", + "$schema": "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/meta.json", + "$id": "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json", "title": "Common Security Advisory Framework", "description": "Representation of security advisory information as a JSON document.", "type": "object", @@ -64,7 +64,8 @@ "format": "uri" } } - } + }, + "additionalProperties": false } }, "branches_t": { @@ -125,7 +126,8 @@ "product": { "$ref": "#/$defs/full_product_name_t" } - } + }, + "additionalProperties": false } }, "full_product_name_t": { @@ -217,7 +219,8 @@ "9ea4c8200113d49d26505da0e02e2f49055dc078d1ad7a419b32e291c7afebbb84badfbd46dec42883bea0b2a1fa697c" ] } - } + }, + "additionalProperties": false } }, "filename": { @@ -231,7 +234,8 @@ "sudoers.so" ] } - } + }, + "additionalProperties": false } }, "model_numbers": { @@ -325,12 +329,15 @@ "type": "string", "format": "uri" } - } + }, + "additionalProperties": false } } - } + }, + "additionalProperties": false } - } + }, + "additionalProperties": false }, "lang_t": { "title": "Language type", @@ -409,7 +416,8 @@ "Impact on safety systems" ] } - } + }, + "additionalProperties": false } }, "product_group_id_t": { @@ -489,7 +497,8 @@ "type": "string", "format": "uri" } - } + }, + "additionalProperties": false } }, "version_t": { @@ -516,7 +525,7 @@ "description": "Contains the URL of the CSAF JSON schema which the document promises to be valid for.", "type": "string", "enum": [ - "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json" + "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json" ], "format": "uri" }, @@ -563,7 +572,8 @@ "Moderate" ] } - } + }, + "additionalProperties": false }, "category": { "title": "Document category", @@ -622,7 +632,8 @@ "US Federal Civilian Authorities" ] } - } + }, + "additionalProperties": false }, "text": { "title": "Textual description", @@ -667,15 +678,29 @@ "https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Kritis/Merkblatt_TLP.pdf" ] } - } + }, + "additionalProperties": false } - } + }, + "additionalProperties": false }, "lang": { "title": "Document language", "description": "Identifies the language used by this document, corresponding to IETF BCP 47 / RFC 5646.", "$ref": "#/$defs/lang_t" }, + "license_expression": { + "title": "License expression", + "description": "Contains the SPDX license expression for the CSAF document.", + "type": "string", + "minLength": 1, + "examples": [ + "CC-BY-4.0", + "LicenseRef-www.example.org-Example-CSAF-License-3.0+", + "LicenseRef-scancode-public-domain", + "MIT OR any-OSI" + ] + }, "notes": { "title": "Document notes", "description": "Holds notes associated with the whole document.", @@ -741,7 +766,8 @@ "https://www.example.com" ] } - } + }, + "additionalProperties": false }, "references": { "title": "Document references", @@ -842,9 +868,11 @@ "2" ] } - } + }, + "additionalProperties": false } - } + }, + "additionalProperties": false }, "id": { "title": "Unique identifier for the document", @@ -903,7 +931,8 @@ "Initial version." ] } - } + }, + "additionalProperties": false } }, "status": { @@ -919,9 +948,11 @@ "version": { "$ref": "#/$defs/version_t" } - } + }, + "additionalProperties": false } - } + }, + "additionalProperties": false }, "product_tree": { "title": "Product tree", @@ -978,7 +1009,8 @@ "The x64 versions of the operating system." ] } - } + }, + "additionalProperties": false } }, "relationships": { @@ -1022,10 +1054,12 @@ "description": "Holds a Product ID that refers to the Full Product Name element, which is referenced as the second element of the relationship.", "$ref": "#/$defs/product_id_t" } - } + }, + "additionalProperties": false } } - } + }, + "additionalProperties": false }, "vulnerabilities": { "title": "Vulnerabilities", @@ -1080,6 +1114,7 @@ "title": "Weakness name", "description": "Holds the full name of the weakness as given in the CWE specification.", "type": "string", + "pattern": "^[^\\s\\-_\\.](.*[^\\s\\-_\\.])?$", "minLength": 1, "examples": [ "Cross-Site Request Forgery (CSRF)", @@ -1091,7 +1126,6 @@ "title": "CWE version", "description": "Holds the version string of the CWE specification this weakness was extracted from.", "type": "string", - "minLength": 1, "pattern": "^[1-9]\\d*\\.([0-9]|([1-9]\\d+))(\\.\\d+)?$", "examples": [ "1.0", @@ -1101,7 +1135,8 @@ "4.12" ] } - } + }, + "additionalProperties": false } }, "disclosure_date": { @@ -1116,6 +1151,44 @@ "type": "string", "format": "date-time" }, + "first_known_exploitation_dates": { + "title": "List of first known exploitation dates", + "description": "Contains a list of dates of first known exploitations.", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "title": "First known exploitation date", + "description": "Contains information on when this vulnerability was first known to be exploited in the wild in the products specified.", + "type": "object", + "minProperties": 3, + "required": [ + "date", + "exploitation_date" + ], + "properties": { + "date": { + "title": "Date of the information", + "description": "Contains the date when the information was last updated.", + "type": "string", + "format": "date-time" + }, + "exploitation_date": { + "title": "Date of the exploitation", + "description": "Contains the date when the exploitation happened.", + "type": "string", + "format": "date-time" + }, + "group_ids": { + "$ref": "#/$defs/product_groups_t" + }, + "product_ids": { + "$ref": "#/$defs/products_t" + } + }, + "additionalProperties": false + } + }, "flags": { "title": "List of flags", "description": "Contains a list of machine readable flags.", @@ -1154,7 +1227,8 @@ "product_ids": { "$ref": "#/$defs/products_t" } - } + }, + "additionalProperties": false } }, "ids": { @@ -1192,7 +1266,8 @@ "oasis-tcs/csaf#210" ] } - } + }, + "additionalProperties": false } }, "involvements": { @@ -1210,12 +1285,21 @@ "status" ], "properties": { + "contact": { + "title": "Party contact information", + "description": "Contains the contact information of the party that was used in this state.", + "type": "string", + "minLength": 1 + }, "date": { "title": "Date of involvement", "description": "Holds the date and time of the involvement entry.", "type": "string", "format": "date-time" }, + "group_ids": { + "$ref": "#/$defs/product_groups_t" + }, "party": { "title": "Party category", "description": "Defines the category of the involved party.", @@ -1228,6 +1312,9 @@ "vendor" ] }, + "product_ids": { + "$ref": "#/$defs/products_t" + }, "status": { "title": "Party status", "description": "Defines contact status of the involved party.", @@ -1247,7 +1334,8 @@ "type": "string", "minLength": 1 } - } + }, + "additionalProperties": false } }, "metrics": { @@ -1308,12 +1396,14 @@ "type": "string", "format": "date-time" } - } + }, + "additionalProperties": false }, "ssvc_v1": { "type": "object" } - } + }, + "additionalProperties": false }, "products": { "$ref": "#/$defs/products_t" @@ -1324,7 +1414,8 @@ "type": "string", "format": "uri" } - } + }, + "additionalProperties": false } }, "notes": { @@ -1383,7 +1474,8 @@ "description": "It is not known whether these versions are or are not affected by the vulnerability. There is also no investigation and therefore the status might never be determined.", "$ref": "#/$defs/products_t" } - } + }, + "additionalProperties": false }, "references": { "title": "Vulnerability references", @@ -1478,7 +1570,8 @@ "type": "string", "minLength": 1 } - } + }, + "additionalProperties": false }, "url": { "title": "URL to the remediation", @@ -1486,7 +1579,8 @@ "type": "string", "format": "uri" } - } + }, + "additionalProperties": false } }, "threats": { @@ -1531,7 +1625,8 @@ "product_ids": { "$ref": "#/$defs/products_t" } - } + }, + "additionalProperties": false } }, "title": { @@ -1540,8 +1635,10 @@ "type": "string", "minLength": 1 } - } + }, + "additionalProperties": false } } - } + }, + "additionalProperties": false } diff --git a/csaf-lib/src/csaf/csaf2_1/loader.rs b/csaf-lib/src/csaf/csaf2_1/loader.rs index b8fa11a..a718f5b 100644 --- a/csaf-lib/src/csaf/csaf2_1/loader.rs +++ b/csaf-lib/src/csaf/csaf2_1/loader.rs @@ -57,7 +57,7 @@ mod tests { .unwrap(); CommonSecurityAdvisoryFramework::builder() .document(metadata) - .schema(JsonSchema::HttpsDocsOasisOpenOrgCsafCsafV21CsafJsonSchemaJson) + .schema(JsonSchema::HttpsDocsOasisOpenOrgCsafCsafV21SchemaCsafJson) .try_into() .unwrap() } diff --git a/csaf-lib/src/csaf/csaf2_1/schema.rs b/csaf-lib/src/csaf/csaf2_1/schema.rs index 9e099be..0106d2b 100644 --- a/csaf-lib/src/csaf/csaf2_1/schema.rs +++ b/csaf-lib/src/csaf/csaf2_1/schema.rs @@ -89,11 +89,13 @@ pub mod error { /// }, /// "minItems": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` ///
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Acknowledgment { ///Contains the names of contributors being recognized. #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] @@ -191,7 +193,8 @@ impl Acknowledgment { /// }, /// "minItems": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 ///} @@ -331,11 +334,13 @@ impl<'de> ::serde::Deserialize<'de> for AdditionalRestartInformation { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` ///
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct AggregateSeverity { ///Points to the namespace so referenced. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] @@ -672,11 +677,13 @@ impl<'de> ::serde::Deserialize<'de> for AudienceOfNote { /// "product": { /// "$ref": "#/$defs/full_product_name_t" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` ///
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Branch { #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub branches: ::std::option::Option, @@ -759,7 +766,8 @@ impl Branch { /// "product": { /// "$ref": "#/$defs/full_product_name_t" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 ///} @@ -1540,7 +1548,7 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// /// ```json ///{ -/// "$id": "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json", +/// "$id": "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json", /// "title": "Common Security Advisory Framework", /// "description": "Representation of security advisory information as a JSON document.", /// "type": "object", @@ -1555,7 +1563,7 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "format": "uri", /// "enum": [ -/// "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json" +/// "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json" /// ] /// }, /// "document": { @@ -1601,7 +1609,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "category": { /// "title": "Document category", @@ -1660,7 +1669,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "text": { /// "title": "Textual description", @@ -1705,15 +1715,29 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "lang": { /// "title": "Document language", /// "description": "Identifies the language used by this document, corresponding to IETF BCP 47 / RFC 5646.", /// "$ref": "#/$defs/lang_t" /// }, +/// "license_expression": { +/// "title": "License expression", +/// "description": "Contains the SPDX license expression for the CSAF document.", +/// "examples": [ +/// "CC-BY-4.0", +/// "LicenseRef-www.example.org-Example-CSAF-License-3.0+", +/// "LicenseRef-scancode-public-domain", +/// "MIT OR any-OSI" +/// ], +/// "type": "string", +/// "minLength": 1 +/// }, /// "notes": { /// "title": "Document notes", /// "description": "Holds notes associated with the whole document.", @@ -1779,7 +1803,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "references": { /// "title": "Document references", @@ -1878,9 +1903,11 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "id": { /// "title": "Unique identifier for the document", @@ -1936,7 +1963,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -1953,9 +1981,11 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "version": { /// "$ref": "#/$defs/version_t" /// } -/// } +/// }, +/// "additionalProperties": false /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "product_tree": { /// "title": "Product tree", @@ -2011,7 +2041,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -2055,11 +2086,13 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "description": "Holds a Product ID that refers to the Full Product Name element, which is referenced as the second element of the relationship.", /// "$ref": "#/$defs/product_id_t" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "vulnerabilities": { /// "title": "Vulnerabilities", @@ -2116,7 +2149,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" /// ], /// "type": "string", -/// "minLength": 1 +/// "minLength": 1, +/// "pattern": "^[^\\s\\-_\\.](.*[^\\s\\-_\\.])?$" /// }, /// "version": { /// "title": "CWE version", @@ -2129,10 +2163,10 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "4.12" /// ], /// "type": "string", -/// "minLength": 1, /// "pattern": "^[1-9]\\d*\\.([0-9]|([1-9]\\d+))(\\.\\d+)?$" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -2147,6 +2181,42 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "description": "Holds the date and time the vulnerability was originally discovered.", /// "type": "string" /// }, +/// "first_known_exploitation_dates": { +/// "title": "List of first known exploitation dates", +/// "description": "Contains a list of dates of first known exploitations.", +/// "type": "array", +/// "items": { +/// "title": "First known exploitation date", +/// "description": "Contains information on when this vulnerability was first known to be exploited in the wild in the products specified.", +/// "type": "object", +/// "minProperties": 3, +/// "required": [ +/// "date", +/// "exploitation_date" +/// ], +/// "properties": { +/// "date": { +/// "title": "Date of the information", +/// "description": "Contains the date when the information was last updated.", +/// "type": "string" +/// }, +/// "exploitation_date": { +/// "title": "Date of the exploitation", +/// "description": "Contains the date when the exploitation happened.", +/// "type": "string" +/// }, +/// "group_ids": { +/// "$ref": "#/$defs/product_groups_t" +/// }, +/// "product_ids": { +/// "$ref": "#/$defs/products_t" +/// } +/// }, +/// "additionalProperties": false +/// }, +/// "minItems": 1, +/// "uniqueItems": true +/// }, /// "flags": { /// "title": "List of flags", /// "description": "Contains a list of machine readable flags.", @@ -2182,7 +2252,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "product_ids": { /// "$ref": "#/$defs/products_t" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -2220,7 +2291,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -2238,11 +2310,20 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "status" /// ], /// "properties": { +/// "contact": { +/// "title": "Party contact information", +/// "description": "Contains the contact information of the party that was used in this state.", +/// "type": "string", +/// "minLength": 1 +/// }, /// "date": { /// "title": "Date of involvement", /// "description": "Holds the date and time of the involvement entry.", /// "type": "string" /// }, +/// "group_ids": { +/// "$ref": "#/$defs/product_groups_t" +/// }, /// "party": { /// "title": "Party category", /// "description": "Defines the category of the involved party.", @@ -2255,6 +2336,9 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "vendor" /// ] /// }, +/// "product_ids": { +/// "$ref": "#/$defs/products_t" +/// }, /// "status": { /// "title": "Party status", /// "description": "Defines contact status of the involved party.", @@ -2274,7 +2358,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -2334,12 +2419,14 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "description": "Holds the date and time the EPSS value was recorded.", /// "type": "string" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "ssvc_v1": { /// "type": "object" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "products": { /// "$ref": "#/$defs/products_t" @@ -2350,7 +2437,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -2411,7 +2499,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "description": "It is not known whether these versions are or are not affected by the vulnerability. There is also no investigation and therefore the status might never be determined.", /// "$ref": "#/$defs/products_t" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "references": { /// "title": "Vulnerability references", @@ -2504,7 +2593,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "url": { /// "title": "URL to the remediation", @@ -2512,7 +2602,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -2556,7 +2647,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "product_ids": { /// "$ref": "#/$defs/products_t" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -2566,15 +2658,18 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` ///
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct CommonSecurityAdvisoryFramework { pub document: DocumentLevelMetaData, #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] @@ -2726,16 +2821,19 @@ impl<'de> ::serde::Deserialize<'de> for ContactDetails { /// "description": "Holds the date and time the EPSS value was recorded.", /// "type": "string" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "ssvc_v1": { /// "type": "object" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Content { #[serde(default, skip_serializing_if = "::serde_json::Map::is_empty")] pub cvss_v2: ::serde_json::Map<::std::string::String, ::serde_json::Value>, @@ -2906,7 +3004,8 @@ impl<'de> ::serde::Deserialize<'de> for ContributingOrganization { /// "minLength": 32, /// "pattern": "^[0-9a-fA-F]{32,}$" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -2921,11 +3020,13 @@ impl<'de> ::serde::Deserialize<'de> for ContributingOrganization { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct CryptographicHashes { ///Contains a list of cryptographic hashes for this file. pub file_hashes: ::std::vec::Vec, @@ -3135,7 +3236,8 @@ impl<'de> ::serde::Deserialize<'de> for Cve { /// "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" /// ], /// "type": "string", -/// "minLength": 1 +/// "minLength": 1, +/// "pattern": "^[^\\s\\-_\\.](.*[^\\s\\-_\\.])?$" /// }, /// "version": { /// "title": "CWE version", @@ -3148,14 +3250,15 @@ impl<'de> ::serde::Deserialize<'de> for Cve { /// "4.12" /// ], /// "type": "string", -/// "minLength": 1, /// "pattern": "^[1-9]\\d*\\.([0-9]|([1-9]\\d+))(\\.\\d+)?$" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Cwe { ///Holds the ID for the weakness associated. pub id: WeaknessId, @@ -3190,7 +3293,6 @@ impl Cwe { /// "4.12" /// ], /// "type": "string", -/// "minLength": 1, /// "pattern": "^[1-9]\\d*\\.([0-9]|([1-9]\\d+))(\\.\\d+)?$" ///} /// ``` @@ -3219,9 +3321,6 @@ impl ::std::str::FromStr for CweVersion { fn from_str( value: &str, ) -> ::std::result::Result { - if value.len() < 1usize { - return Err("shorter than 1 characters".into()); - } if regress::Regex::new("^[1-9]\\d*\\.([0-9]|([1-9]\\d+))(\\.\\d+)?$") .unwrap() .find(value) @@ -3572,13 +3671,16 @@ impl<'de> ::serde::Deserialize<'de> for DocumentCategory { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct DocumentGenerator { ///This SHOULD be the current date that the document was generated. Because documents are often generated internally by a document producer and exist for a nonzero amount of time before being released, this field MAY be different from the Initial Release Date and Current Release Date. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] @@ -3643,7 +3745,8 @@ impl DocumentGenerator { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "category": { /// "title": "Document category", @@ -3702,7 +3805,8 @@ impl DocumentGenerator { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "text": { /// "title": "Textual description", @@ -3747,15 +3851,29 @@ impl DocumentGenerator { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "lang": { /// "title": "Document language", /// "description": "Identifies the language used by this document, corresponding to IETF BCP 47 / RFC 5646.", /// "$ref": "#/$defs/lang_t" /// }, +/// "license_expression": { +/// "title": "License expression", +/// "description": "Contains the SPDX license expression for the CSAF document.", +/// "examples": [ +/// "CC-BY-4.0", +/// "LicenseRef-www.example.org-Example-CSAF-License-3.0+", +/// "LicenseRef-scancode-public-domain", +/// "MIT OR any-OSI" +/// ], +/// "type": "string", +/// "minLength": 1 +/// }, /// "notes": { /// "title": "Document notes", /// "description": "Holds notes associated with the whole document.", @@ -3821,7 +3939,8 @@ impl DocumentGenerator { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "references": { /// "title": "Document references", @@ -3920,9 +4039,11 @@ impl DocumentGenerator { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "id": { /// "title": "Unique identifier for the document", @@ -3978,7 +4099,8 @@ impl DocumentGenerator { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -3995,13 +4117,16 @@ impl DocumentGenerator { /// "version": { /// "$ref": "#/$defs/version_t" /// } -/// } +/// }, +/// "additionalProperties": false /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct DocumentLevelMetaData { ///Contains a list of acknowledgment elements associated with the whole document. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] @@ -4016,6 +4141,9 @@ pub struct DocumentLevelMetaData { ///Identifies the language used by this document, corresponding to IETF BCP 47 / RFC 5646. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub lang: ::std::option::Option, + ///Contains the SPDX license expression for the CSAF document. + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub license_expression: ::std::option::Option, ///Holds notes associated with the whole document. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub notes: ::std::option::Option, @@ -4247,11 +4375,13 @@ impl<'de> ::serde::Deserialize<'de> for EngineName { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct EngineOfDocumentGeneration { ///Represents the name of the engine that generated the CSAF document. pub name: EngineName, @@ -4464,11 +4594,13 @@ impl<'de> ::serde::Deserialize<'de> for EntitlementOfTheRemediation { /// "description": "Holds the date and time the EPSS value was recorded.", /// "type": "string" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Epss { ///Contains the rank ordering of probabilities from highest to lowest. pub percentile: Percentile, @@ -4527,11 +4659,13 @@ impl Epss { /// "minLength": 32, /// "pattern": "^[0-9a-fA-F]{32,}$" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct FileHash { ///Contains the name of the cryptographic hash algorithm used to calculate the value. pub algorithm: AlgorithmOfTheCryptographicHash, @@ -4632,6 +4766,64 @@ impl<'de> ::serde::Deserialize<'de> for Filename { }) } } +///Contains information on when this vulnerability was first known to be exploited in the wild in the products specified. +/// +///
JSON schema +/// +/// ```json +///{ +/// "title": "First known exploitation date", +/// "description": "Contains information on when this vulnerability was first known to be exploited in the wild in the products specified.", +/// "type": "object", +/// "minProperties": 3, +/// "required": [ +/// "date", +/// "exploitation_date" +/// ], +/// "properties": { +/// "date": { +/// "title": "Date of the information", +/// "description": "Contains the date when the information was last updated.", +/// "type": "string" +/// }, +/// "exploitation_date": { +/// "title": "Date of the exploitation", +/// "description": "Contains the date when the exploitation happened.", +/// "type": "string" +/// }, +/// "group_ids": { +/// "$ref": "#/$defs/product_groups_t" +/// }, +/// "product_ids": { +/// "$ref": "#/$defs/products_t" +/// } +/// }, +/// "additionalProperties": false +///} +/// ``` +///
+#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] +pub struct FirstKnownExploitationDate { + ///Contains the date when the information was last updated. + pub date: ::std::string::String, + ///Contains the date when the exploitation happened. + pub exploitation_date: ::std::string::String, + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub group_ids: ::std::option::Option, + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub product_ids: ::std::option::Option, +} +impl ::std::convert::From<&FirstKnownExploitationDate> for FirstKnownExploitationDate { + fn from(value: &FirstKnownExploitationDate) -> Self { + value.clone() + } +} +impl FirstKnownExploitationDate { + pub fn builder() -> builder::FirstKnownExploitationDate { + Default::default() + } +} ///Contains product specific information in regard to this vulnerability as a single machine readable flag. /// ///
JSON schema @@ -4668,11 +4860,13 @@ impl<'de> ::serde::Deserialize<'de> for Filename { /// "product_ids": { /// "$ref": "#/$defs/products_t" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` ///
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Flag { ///Contains the date when assessment was done or the flag was assigned. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] @@ -4786,7 +4980,8 @@ impl Flag { /// "minLength": 32, /// "pattern": "^[0-9a-fA-F]{32,}$" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -4801,7 +4996,8 @@ impl Flag { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -4895,17 +5091,21 @@ impl Flag { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct FullProductNameT { ///The value should be the product’s full canonical name, including version number and other attributes, as it would be used in a human-friendly document. pub name: TextualDescriptionOfTheProduct, @@ -4949,11 +5149,13 @@ impl FullProductNameT { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct GenericUri { ///Refers to a URL which provides the name and knowledge about the specification used or is the namespace in which these values are valid. pub namespace: ::std::string::String, @@ -5040,7 +5242,8 @@ impl GenericUri { /// "minLength": 32, /// "pattern": "^[0-9a-fA-F]{32,}$" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -5055,7 +5258,8 @@ impl GenericUri { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -5149,15 +5353,18 @@ impl GenericUri { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct HelperToIdentifyTheProduct { ///The Common Platform Enumeration (CPE) attribute refers to a method for naming platforms external to this specification. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] @@ -5242,11 +5449,13 @@ impl HelperToIdentifyTheProduct { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Id { ///Indicates the name of the vulnerability tracking or numbering system. pub system_name: SystemName, @@ -5277,11 +5486,20 @@ impl Id { /// "status" /// ], /// "properties": { +/// "contact": { +/// "title": "Party contact information", +/// "description": "Contains the contact information of the party that was used in this state.", +/// "type": "string", +/// "minLength": 1 +/// }, /// "date": { /// "title": "Date of involvement", /// "description": "Holds the date and time of the involvement entry.", /// "type": "string" /// }, +/// "group_ids": { +/// "$ref": "#/$defs/product_groups_t" +/// }, /// "party": { /// "title": "Party category", /// "description": "Defines the category of the involved party.", @@ -5294,6 +5512,9 @@ impl Id { /// "vendor" /// ] /// }, +/// "product_ids": { +/// "$ref": "#/$defs/products_t" +/// }, /// "status": { /// "title": "Party status", /// "description": "Defines contact status of the involved party.", @@ -5313,17 +5534,26 @@ impl Id { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Involvement { + ///Contains the contact information of the party that was used in this state. + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub contact: ::std::option::Option, ///Holds the date and time of the involvement entry. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub date: ::std::option::Option<::std::string::String>, + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub group_ids: ::std::option::Option, ///Defines the category of the involved party. pub party: PartyCategory, + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub product_ids: ::std::option::Option, ///Defines contact status of the involved party. pub status: PartyStatus, ///Contains additional context regarding what is going on. @@ -5430,7 +5660,7 @@ impl<'de> ::serde::Deserialize<'de> for IssuingAuthority { /// "type": "string", /// "format": "uri", /// "enum": [ -/// "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json" +/// "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json" /// ] ///} /// ``` @@ -5448,8 +5678,8 @@ impl<'de> ::serde::Deserialize<'de> for IssuingAuthority { PartialOrd )] pub enum JsonSchema { - #[serde(rename = "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json")] - HttpsDocsOasisOpenOrgCsafCsafV21CsafJsonSchemaJson, + #[serde(rename = "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json")] + HttpsDocsOasisOpenOrgCsafCsafV21SchemaCsafJson, } impl ::std::convert::From<&Self> for JsonSchema { fn from(value: &JsonSchema) -> Self { @@ -5459,10 +5689,8 @@ impl ::std::convert::From<&Self> for JsonSchema { impl ::std::fmt::Display for JsonSchema { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { match *self { - Self::HttpsDocsOasisOpenOrgCsafCsafV21CsafJsonSchemaJson => { - write!( - f, "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json" - ) + Self::HttpsDocsOasisOpenOrgCsafCsafV21SchemaCsafJson => { + write!(f, "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json") } } } @@ -5473,8 +5701,8 @@ impl ::std::str::FromStr for JsonSchema { value: &str, ) -> ::std::result::Result { match value { - "https://docs.oasis-open.org/csaf/csaf/v2.1/csaf_json_schema.json" => { - Ok(Self::HttpsDocsOasisOpenOrgCsafCsafV21CsafJsonSchemaJson) + "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json" => { + Ok(Self::HttpsDocsOasisOpenOrgCsafCsafV21SchemaCsafJson) } _ => Err("invalid value".into()), } @@ -5890,6 +6118,91 @@ impl<'de> ::serde::Deserialize<'de> for LegacyVersionOfTheRevision { }) } } +///Contains the SPDX license expression for the CSAF document. +/// +///
JSON schema +/// +/// ```json +///{ +/// "title": "License expression", +/// "description": "Contains the SPDX license expression for the CSAF document.", +/// "examples": [ +/// "CC-BY-4.0", +/// "LicenseRef-www.example.org-Example-CSAF-License-3.0+", +/// "LicenseRef-scancode-public-domain", +/// "MIT OR any-OSI" +/// ], +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct LicenseExpression(::std::string::String); +impl ::std::ops::Deref for LicenseExpression { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: LicenseExpression) -> Self { + value.0 + } +} +impl ::std::convert::From<&LicenseExpression> for LicenseExpression { + fn from(value: &LicenseExpression) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for LicenseExpression { + 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 LicenseExpression { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for LicenseExpression { + 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 LicenseExpression { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for LicenseExpression { + 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()) + }) + } +} ///Contains all metadata about the metric including products it applies to and the source and the content itself. /// ///
JSON schema @@ -5946,12 +6259,14 @@ impl<'de> ::serde::Deserialize<'de> for LegacyVersionOfTheRevision { /// "description": "Holds the date and time the EPSS value was recorded.", /// "type": "string" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "ssvc_v1": { /// "type": "object" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "products": { /// "$ref": "#/$defs/products_t" @@ -5962,11 +6277,13 @@ impl<'de> ::serde::Deserialize<'de> for LegacyVersionOfTheRevision { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` ///
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Metric { pub content: Content, pub products: ProductsT, @@ -6383,11 +6700,13 @@ impl<'de> ::serde::Deserialize<'de> for NameOfTheContributor { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Note { ///Indicates who is intended to read it. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] @@ -6669,7 +6988,8 @@ impl<'de> ::serde::Deserialize<'de> for NoteContent { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 ///} @@ -6797,6 +7117,85 @@ impl ::std::convert::TryFrom<::std::string::String> for PartyCategory { value.parse() } } +///Contains the contact information of the party that was used in this state. +/// +///
JSON schema +/// +/// ```json +///{ +/// "title": "Party contact information", +/// "description": "Contains the contact information of the party that was used in this state.", +/// "type": "string", +/// "minLength": 1 +///} +/// ``` +///
+#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct PartyContactInformation(::std::string::String); +impl ::std::ops::Deref for PartyContactInformation { + type Target = ::std::string::String; + fn deref(&self) -> &::std::string::String { + &self.0 + } +} +impl ::std::convert::From for ::std::string::String { + fn from(value: PartyContactInformation) -> Self { + value.0 + } +} +impl ::std::convert::From<&PartyContactInformation> for PartyContactInformation { + fn from(value: &PartyContactInformation) -> Self { + value.clone() + } +} +impl ::std::str::FromStr for PartyContactInformation { + 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 PartyContactInformation { + type Error = self::error::ConversionError; + fn try_from( + value: &str, + ) -> ::std::result::Result { + value.parse() + } +} +impl ::std::convert::TryFrom<&::std::string::String> for PartyContactInformation { + 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 PartyContactInformation { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } +} +impl<'de> ::serde::Deserialize<'de> for PartyContactInformation { + 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()) + }) + } +} ///Defines contact status of the involved party. /// ///
JSON schema @@ -7107,11 +7506,13 @@ impl<'de> ::serde::Deserialize<'de> for Probability { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` ///
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct ProductGroup { pub group_id: ProductGroupIdT, ///Lists the product_ids of those products which known as one group in the document. @@ -7394,11 +7795,13 @@ impl<'de> ::serde::Deserialize<'de> for ProductIdT { /// "description": "It is not known whether these versions are or are not affected by the vulnerability. There is also no investigation and therefore the status might never be determined.", /// "$ref": "#/$defs/products_t" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct ProductStatus { ///These are the first versions of the releases known to be affected by the vulnerability. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] @@ -7512,7 +7915,8 @@ impl ProductStatus { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -7556,15 +7960,18 @@ impl ProductStatus { /// "description": "Holds a Product ID that refers to the Full Product Name element, which is referenced as the second element of the relationship.", /// "$ref": "#/$defs/product_id_t" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct ProductTree { #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub branches: ::std::option::Option, @@ -7704,11 +8111,13 @@ impl ::std::convert::From> for ProductsT { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Publisher { ///Provides information about the category of publisher releasing the document. pub category: CategoryOfPublisher, @@ -7769,11 +8178,13 @@ impl Publisher { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Reference { ///Indicates whether the reference points to the same document or vulnerability in focus (depending on scope) or to an external resource. #[serde(default = "defaults::reference_category")] @@ -7833,7 +8244,8 @@ impl Reference { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 ///} @@ -7904,11 +8316,13 @@ impl ::std::convert::From<::std::vec::Vec> for ReferencesT { /// "description": "Holds a Product ID that refers to the Full Product Name element, which is referenced as the second element of the relationship.", /// "$ref": "#/$defs/product_id_t" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Relationship { ///Defines the category of relationship for the referenced component. pub category: RelationshipCategory, @@ -8113,7 +8527,8 @@ impl ::std::convert::TryFrom<::std::string::String> for RelationshipCategory { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "url": { /// "title": "URL to the remediation", @@ -8121,11 +8536,13 @@ impl ::std::convert::TryFrom<::std::string::String> for RelationshipCategory { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Remediation { ///Specifies the category which this remediation belongs to. pub category: CategoryOfTheRemediation, @@ -8192,11 +8609,13 @@ impl Remediation { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct RestartRequiredByRemediation { ///Specifies what category of restart is required by this remediation to become effective. pub category: CategoryOfRestart, @@ -8253,11 +8672,13 @@ impl RestartRequiredByRemediation { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Revision { ///The date of the revision entry pub date: ::std::string::String, @@ -8319,7 +8740,8 @@ impl Revision { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "text": { /// "title": "Textual description", @@ -8364,13 +8786,16 @@ impl Revision { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct RulesForSharingDocument { #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub sharing_group: ::std::option::Option, @@ -8501,11 +8926,13 @@ impl<'de> ::serde::Deserialize<'de> for SerialNumber { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct SharingGroup { ///Provides the unique ID for the sharing group. pub id: SharingGroupId, @@ -9641,11 +10068,13 @@ impl<'de> ::serde::Deserialize<'de> for TextualDescriptionOfTheProduct { /// "product_ids": { /// "$ref": "#/$defs/products_t" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Threat { ///Categorizes the threat according to the rules of the specification. pub category: CategoryOfTheThreat, @@ -9998,9 +10427,11 @@ impl<'de> ::serde::Deserialize<'de> for TitleOfThisDocument { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "id": { /// "title": "Unique identifier for the document", @@ -10056,7 +10487,8 @@ impl<'de> ::serde::Deserialize<'de> for TitleOfThisDocument { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -10073,11 +10505,13 @@ impl<'de> ::serde::Deserialize<'de> for TitleOfThisDocument { /// "version": { /// "$ref": "#/$defs/version_t" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Tracking { ///Contains a list of alternate names for the same document. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] @@ -10143,11 +10577,13 @@ impl Tracking { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct TrafficLightProtocolTlp { ///Provides the TLP label of the document. pub label: LabelOfTlp, @@ -10493,7 +10929,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" /// ], /// "type": "string", -/// "minLength": 1 +/// "minLength": 1, +/// "pattern": "^[^\\s\\-_\\.](.*[^\\s\\-_\\.])?$" /// }, /// "version": { /// "title": "CWE version", @@ -10506,10 +10943,10 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "4.12" /// ], /// "type": "string", -/// "minLength": 1, /// "pattern": "^[1-9]\\d*\\.([0-9]|([1-9]\\d+))(\\.\\d+)?$" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -10524,6 +10961,42 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "description": "Holds the date and time the vulnerability was originally discovered.", /// "type": "string" /// }, +/// "first_known_exploitation_dates": { +/// "title": "List of first known exploitation dates", +/// "description": "Contains a list of dates of first known exploitations.", +/// "type": "array", +/// "items": { +/// "title": "First known exploitation date", +/// "description": "Contains information on when this vulnerability was first known to be exploited in the wild in the products specified.", +/// "type": "object", +/// "minProperties": 3, +/// "required": [ +/// "date", +/// "exploitation_date" +/// ], +/// "properties": { +/// "date": { +/// "title": "Date of the information", +/// "description": "Contains the date when the information was last updated.", +/// "type": "string" +/// }, +/// "exploitation_date": { +/// "title": "Date of the exploitation", +/// "description": "Contains the date when the exploitation happened.", +/// "type": "string" +/// }, +/// "group_ids": { +/// "$ref": "#/$defs/product_groups_t" +/// }, +/// "product_ids": { +/// "$ref": "#/$defs/products_t" +/// } +/// }, +/// "additionalProperties": false +/// }, +/// "minItems": 1, +/// "uniqueItems": true +/// }, /// "flags": { /// "title": "List of flags", /// "description": "Contains a list of machine readable flags.", @@ -10559,7 +11032,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "product_ids": { /// "$ref": "#/$defs/products_t" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -10597,7 +11071,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -10615,11 +11090,20 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "status" /// ], /// "properties": { +/// "contact": { +/// "title": "Party contact information", +/// "description": "Contains the contact information of the party that was used in this state.", +/// "type": "string", +/// "minLength": 1 +/// }, /// "date": { /// "title": "Date of involvement", /// "description": "Holds the date and time of the involvement entry.", /// "type": "string" /// }, +/// "group_ids": { +/// "$ref": "#/$defs/product_groups_t" +/// }, /// "party": { /// "title": "Party category", /// "description": "Defines the category of the involved party.", @@ -10632,6 +11116,9 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "vendor" /// ] /// }, +/// "product_ids": { +/// "$ref": "#/$defs/products_t" +/// }, /// "status": { /// "title": "Party status", /// "description": "Defines contact status of the involved party.", @@ -10651,7 +11138,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -10711,12 +11199,14 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "description": "Holds the date and time the EPSS value was recorded.", /// "type": "string" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "ssvc_v1": { /// "type": "object" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "products": { /// "$ref": "#/$defs/products_t" @@ -10727,7 +11217,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -10788,7 +11279,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "description": "It is not known whether these versions are or are not affected by the vulnerability. There is also no investigation and therefore the status might never be determined.", /// "$ref": "#/$defs/products_t" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "references": { /// "title": "Vulnerability references", @@ -10881,7 +11373,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "url": { /// "title": "URL to the remediation", @@ -10889,7 +11382,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -10933,7 +11427,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "product_ids": { /// "$ref": "#/$defs/products_t" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -10943,11 +11438,13 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false ///} /// ``` /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub struct Vulnerability { ///Contains a list of acknowledgment elements associated with this vulnerability item. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] @@ -10964,6 +11461,11 @@ pub struct Vulnerability { ///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>, + ///Contains a list of dates of first known exploitations. + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub first_known_exploitation_dates: ::std::option::Option< + Vec, + >, ///Contains a list of machine readable flags. #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] pub flags: ::std::option::Option>, @@ -11007,6 +11509,7 @@ impl ::std::default::Default for Vulnerability { cwes: Default::default(), disclosure_date: Default::default(), discovery_date: Default::default(), + first_known_exploitation_dates: Default::default(), flags: Default::default(), ids: Default::default(), involvements: Default::default(), @@ -11123,7 +11626,8 @@ impl<'de> ::serde::Deserialize<'de> for WeaknessId { /// "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" /// ], /// "type": "string", -/// "minLength": 1 +/// "minLength": 1, +/// "pattern": "^[^\\s\\-_\\.](.*[^\\s\\-_\\.])?$" ///} /// ``` /// @@ -11154,6 +11658,15 @@ impl ::std::str::FromStr for WeaknessName { if value.len() < 1usize { return Err("shorter than 1 characters".into()); } + if regress::Regex::new("^[^\\s\\-_\\.](.*[^\\s\\-_\\.])?$") + .unwrap() + .find(value) + .is_none() + { + return Err( + "doesn't match pattern \"^[^\\s\\-_\\.](.*[^\\s\\-_\\.])?$\"".into(), + ); + } Ok(Self(value.to_string())) } } @@ -11899,6 +12412,10 @@ pub mod builder { ::std::option::Option, ::std::string::String, >, + license_expression: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, notes: ::std::result::Result< ::std::option::Option, ::std::string::String, @@ -11924,6 +12441,7 @@ pub mod builder { csaf_version: Err("no value supplied for csaf_version".to_string()), distribution: Err("no value supplied for distribution".to_string()), lang: Ok(Default::default()), + license_expression: Ok(Default::default()), notes: Ok(Default::default()), publisher: Err("no value supplied for publisher".to_string()), references: Ok(Default::default()), @@ -12006,6 +12524,20 @@ pub mod builder { .map_err(|e| format!("error converting supplied value for lang: {}", e)); self } + pub fn license_expression(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option>, + T::Error: ::std::fmt::Display, + { + self.license_expression = value + .try_into() + .map_err(|e| { + format!( + "error converting supplied value for license_expression: {}", e + ) + }); + self + } pub fn notes(mut self, value: T) -> Self where T: ::std::convert::TryInto<::std::option::Option>, @@ -12092,6 +12624,7 @@ pub mod builder { csaf_version: value.csaf_version?, distribution: value.distribution?, lang: value.lang?, + license_expression: value.license_expression?, notes: value.notes?, publisher: value.publisher?, references: value.references?, @@ -12110,6 +12643,7 @@ pub mod builder { csaf_version: Ok(value.csaf_version), distribution: Ok(value.distribution), lang: Ok(value.lang), + license_expression: Ok(value.license_expression), notes: Ok(value.notes), publisher: Ok(value.publisher), references: Ok(value.references), @@ -12319,6 +12853,109 @@ pub mod builder { } } #[derive(Clone, Debug)] + pub struct FirstKnownExploitationDate { + date: ::std::result::Result<::std::string::String, ::std::string::String>, + exploitation_date: ::std::result::Result< + ::std::string::String, + ::std::string::String, + >, + group_ids: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, + product_ids: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, + } + impl ::std::default::Default for FirstKnownExploitationDate { + fn default() -> Self { + Self { + date: Err("no value supplied for date".to_string()), + exploitation_date: Err( + "no value supplied for exploitation_date".to_string(), + ), + group_ids: Ok(Default::default()), + product_ids: Ok(Default::default()), + } + } + } + impl FirstKnownExploitationDate { + pub fn date(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.date = value + .try_into() + .map_err(|e| format!("error converting supplied value for date: {}", e)); + self + } + pub fn exploitation_date(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.exploitation_date = value + .try_into() + .map_err(|e| { + format!( + "error converting supplied value for exploitation_date: {}", e + ) + }); + self + } + pub fn group_ids(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option>, + T::Error: ::std::fmt::Display, + { + self.group_ids = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for group_ids: {}", e) + }); + self + } + pub fn product_ids(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option>, + T::Error: ::std::fmt::Display, + { + self.product_ids = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for product_ids: {}", e) + }); + self + } + } + impl ::std::convert::TryFrom + for super::FirstKnownExploitationDate { + type Error = super::error::ConversionError; + fn try_from( + value: FirstKnownExploitationDate, + ) -> ::std::result::Result { + Ok(Self { + date: value.date?, + exploitation_date: value.exploitation_date?, + group_ids: value.group_ids?, + product_ids: value.product_ids?, + }) + } + } + impl ::std::convert::From + for FirstKnownExploitationDate { + fn from(value: super::FirstKnownExploitationDate) -> Self { + Self { + date: Ok(value.date), + exploitation_date: Ok(value.exploitation_date), + group_ids: Ok(value.group_ids), + product_ids: Ok(value.product_ids), + } + } + } + #[derive(Clone, Debug)] pub struct Flag { date: ::std::result::Result< ::std::option::Option<::std::string::String>, @@ -12792,11 +13429,23 @@ pub mod builder { } #[derive(Clone, Debug)] pub struct Involvement { + contact: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, date: ::std::result::Result< ::std::option::Option<::std::string::String>, ::std::string::String, >, + group_ids: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, party: ::std::result::Result, + product_ids: ::std::result::Result< + ::std::option::Option, + ::std::string::String, + >, status: ::std::result::Result, summary: ::std::result::Result< ::std::option::Option, @@ -12806,14 +13455,31 @@ pub mod builder { impl ::std::default::Default for Involvement { fn default() -> Self { Self { + contact: Ok(Default::default()), date: Ok(Default::default()), + group_ids: Ok(Default::default()), party: Err("no value supplied for party".to_string()), + product_ids: Ok(Default::default()), status: Err("no value supplied for status".to_string()), summary: Ok(Default::default()), } } } impl Involvement { + pub fn contact(mut self, value: T) -> Self + where + T: ::std::convert::TryInto< + ::std::option::Option, + >, + T::Error: ::std::fmt::Display, + { + self.contact = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for contact: {}", e) + }); + self + } pub fn date(mut self, value: T) -> Self where T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>, @@ -12824,6 +13490,18 @@ pub mod builder { .map_err(|e| format!("error converting supplied value for date: {}", e)); self } + pub fn group_ids(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option>, + T::Error: ::std::fmt::Display, + { + self.group_ids = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for group_ids: {}", e) + }); + self + } pub fn party(mut self, value: T) -> Self where T: ::std::convert::TryInto, @@ -12836,6 +13514,18 @@ pub mod builder { }); self } + pub fn product_ids(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option>, + T::Error: ::std::fmt::Display, + { + self.product_ids = value + .try_into() + .map_err(|e| { + format!("error converting supplied value for product_ids: {}", e) + }); + self + } pub fn status(mut self, value: T) -> Self where T: ::std::convert::TryInto, @@ -12869,8 +13559,11 @@ pub mod builder { value: Involvement, ) -> ::std::result::Result { Ok(Self { + contact: value.contact?, date: value.date?, + group_ids: value.group_ids?, party: value.party?, + product_ids: value.product_ids?, status: value.status?, summary: value.summary?, }) @@ -12879,8 +13572,11 @@ pub mod builder { impl ::std::convert::From for Involvement { fn from(value: super::Involvement) -> Self { Self { + contact: Ok(value.contact), date: Ok(value.date), + group_ids: Ok(value.group_ids), party: Ok(value.party), + product_ids: Ok(value.product_ids), status: Ok(value.status), summary: Ok(value.summary), } @@ -14647,6 +15343,10 @@ pub mod builder { ::std::option::Option<::std::string::String>, ::std::string::String, >, + first_known_exploitation_dates: ::std::result::Result< + ::std::option::Option>, + ::std::string::String, + >, flags: ::std::result::Result< ::std::option::Option>, ::std::string::String, @@ -14696,6 +15396,7 @@ pub mod builder { cwes: Ok(Default::default()), disclosure_date: Ok(Default::default()), discovery_date: Ok(Default::default()), + first_known_exploitation_dates: Ok(Default::default()), flags: Ok(Default::default()), ids: Ok(Default::default()), involvements: Ok(Default::default()), @@ -14766,6 +15467,23 @@ pub mod builder { }); self } + pub fn first_known_exploitation_dates(mut self, value: T) -> Self + where + T: ::std::convert::TryInto< + ::std::option::Option>, + >, + T::Error: ::std::fmt::Display, + { + self.first_known_exploitation_dates = value + .try_into() + .map_err(|e| { + format!( + "error converting supplied value for first_known_exploitation_dates: {}", + e + ) + }); + self + } pub fn flags(mut self, value: T) -> Self where T: ::std::convert::TryInto<::std::option::Option>>, @@ -14896,6 +15614,7 @@ pub mod builder { cwes: value.cwes?, disclosure_date: value.disclosure_date?, discovery_date: value.discovery_date?, + first_known_exploitation_dates: value.first_known_exploitation_dates?, flags: value.flags?, ids: value.ids?, involvements: value.involvements?, @@ -14917,6 +15636,7 @@ pub mod builder { cwes: Ok(value.cwes), disclosure_date: Ok(value.disclosure_date), discovery_date: Ok(value.discovery_date), + first_known_exploitation_dates: Ok(value.first_known_exploitation_dates), flags: Ok(value.flags), ids: Ok(value.ids), involvements: Ok(value.involvements), diff --git a/ssvc b/ssvc index 6557e21..34b5e9b 160000 --- a/ssvc +++ b/ssvc @@ -1 +1 @@ -Subproject commit 6557e21cac6951704b28034e9f137bde8f4049d5 +Subproject commit 34b5e9b3db970826a3122baa3b79f4dc51039aab From 4a51623a6b1427d6fb715d5b669bc62b5027536d Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Wed, 4 Jun 2025 09:18:37 +0200 Subject: [PATCH 11/12] Add `get_source` method to traits and fixed validation 6.1.7 Introduced the `get_source` method in relevant traits and implementations to access the source of vulnerability metrics. Enhanced the duplicate metric validation logic to account for sources, updating error messages to reflect source details. --- .../csaf/csaf2_0/getter_implementations.rs | 4 ++ .../csaf/csaf2_1/getter_implementations.rs | 4 ++ csaf-lib/src/csaf/getter_traits.rs | 2 + csaf-lib/src/csaf/validations/test_6_1_07.rs | 52 +++++++++++-------- 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs index 980c2fb..7135e96 100644 --- a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs @@ -92,6 +92,10 @@ impl MetricTrait for Score { fn get_content(&self) -> &Self::ContentType { self } + + fn get_source(&self) -> &Option { + &None + } } impl ContentTrait for Score { diff --git a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs index 39b22da..cdafab4 100644 --- a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs @@ -73,6 +73,10 @@ impl MetricTrait for Metric { fn get_content(&self) -> &Self::ContentType { &self.content } + + fn get_source(&self) -> &Option { + &self.source + } } impl ContentTrait for Content { diff --git a/csaf-lib/src/csaf/getter_traits.rs b/csaf-lib/src/csaf/getter_traits.rs index b86a838..e42e026 100644 --- a/csaf-lib/src/csaf/getter_traits.rs +++ b/csaf-lib/src/csaf/getter_traits.rs @@ -349,6 +349,8 @@ pub trait MetricTrait { fn get_products(&self) -> impl Iterator + '_; fn get_content(&self) -> &Self::ContentType; + + fn get_source(&self) -> &Option; } pub trait ContentTrait { diff --git a/csaf-lib/src/csaf/validations/test_6_1_07.rs b/csaf-lib/src/csaf/validations/test_6_1_07.rs index 85dacdd..d251875 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_07.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_07.rs @@ -1,8 +1,8 @@ -use std::collections::{HashMap, HashSet}; use crate::csaf::getter_traits::{ContentTrait, CsafTrait, MetricTrait, VulnerabilityTrait}; use crate::csaf::validation::ValidationError; +use crate::csaf::validations::test_6_1_07::VulnerabilityMetrics::{CvssV2, CvssV30, CvssV31, CvssV4, Epss, SsvcV1}; +use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; -use crate::csaf::validations::test_6_1_07::VulnerabilityMetrics::{CvssV2, CvssV30, CvssV31, CvssV4, SsvcV1, Epss}; /// Types of metrics known until CSAF 2.1 #[derive(Hash, Eq, PartialEq, Clone)] @@ -28,7 +28,7 @@ impl Display for VulnerabilityMetrics { } } -fn get_metric_prop_name(metric: &VulnerabilityMetrics) -> &'static str { +fn get_metric_prop_name(metric: VulnerabilityMetrics) -> &'static str { match metric { SsvcV1 => "ssvc_v1", CvssV2 => "cvss_v2", @@ -43,58 +43,63 @@ pub fn test_6_1_07_multiple_same_scores_per_product( doc: &impl CsafTrait, ) -> Result<(), ValidationError> { for (v_i, v) in doc.get_vulnerabilities().iter().enumerate() { - let mut seen_metrics: HashMap> = HashMap::new(); + let mut seen_metrics: HashMap)>> = HashMap::new(); if let Some(metrics) = v.get_metrics() { for (m_i, m) in metrics.iter().enumerate() { let content = m.get_content(); - let mut content_metrics = Vec::::new(); + let mut content_metrics = Vec::<(VulnerabilityMetrics, &Option)>::new(); if content.has_ssvc_v1() { - content_metrics.push(SsvcV1); + content_metrics.push((SsvcV1, m.get_source())); } if content.get_cvss_v2().is_some() { - content_metrics.push(CvssV2); + content_metrics.push((CvssV2, m.get_source())); } if let Some(cvss_v3) = content.get_cvss_v3() { if let Some(version) = cvss_v3.get("version") { if version == "3.1" { - content_metrics.push(CvssV31); + content_metrics.push((CvssV31, m.get_source())); } else if version == "3.0" { - content_metrics.push(CvssV30); + content_metrics.push((CvssV30, m.get_source())); } else { return Err(ValidationError { message: format!("CVSS-v3 version {} is not supported.", version), instance_path: format!( "{}/{}", content.get_content_json_path(v_i, m_i), - get_metric_prop_name(&CvssV30), + get_metric_prop_name(CvssV30), ), }); } } } if content.get_cvss_v4().is_some() { - content_metrics.push(CvssV4); + content_metrics.push((CvssV4, m.get_source())); } if content.get_epss().is_some() { - content_metrics.push(Epss); + content_metrics.push((Epss, m.get_source())); } for p in m.get_products() { let metrics_set = seen_metrics.entry(p.to_string()).or_insert_with(|| HashSet::new()); - for cm in content_metrics.iter() { - if metrics_set.contains(cm) { + for cm_src in content_metrics.iter() { + if metrics_set.contains(cm_src) { return Err(ValidationError { message: format!( - "Product {} already has another metric \"{}\" assigned.", + "Product {} already has another metric \"{}\" {} assigned.", p, - cm, + cm_src.0, + match cm_src.1 { + Some(src) => format!("with the same source \"{}\"", src), + None => "without a source".to_string(), + } ), instance_path: format!( "{}/{}", content.get_content_json_path(v_i, m_i), - get_metric_prop_name(cm)), + get_metric_prop_name(cm_src.0.to_owned()) + ), }); } else { - metrics_set.insert(cm.to_owned()); + metrics_set.insert(cm_src.to_owned()); } } } @@ -113,7 +118,7 @@ mod tests { #[test] fn test_test_6_1_07() { - let cvss_v31_error_message = "Product CSAFPID-9080700 already has another metric \"CVSS-v3.1\" assigned."; + let cvss_v31_error_message = "Product CSAFPID-9080700 already has another metric \"CVSS-v3.1\" without a source assigned."; let csaf_20_path_prefix = "/vulnerabilities/0/scores/1"; let csaf_21_path_prefix = "/vulnerabilities/0/metrics/1/content"; run_csaf20_tests( @@ -135,19 +140,20 @@ mod tests { instance_path: format!("{}/cvss_v3", csaf_21_path_prefix), }), ("02", &ValidationError { - message: "Product CSAFPID-9080700 already has another metric \"CVSS-v3.0\" assigned.".to_string(), + message: "Product CSAFPID-9080700 already has another metric \"CVSS-v3.0\" without a source assigned.".to_string(), instance_path: format!("{}/cvss_v3", csaf_21_path_prefix), }), ("03", &ValidationError { - message: "Product CSAFPID-9080700 already has another metric \"CVSS-v2\" assigned.".to_string(), + message: "Product CSAFPID-9080700 already has another metric \"CVSS-v2\" without a source assigned.".to_string(), instance_path: format!("{}/cvss_v2", csaf_21_path_prefix), }), ("04", &ValidationError { - message: "Product CSAFPID-9080700 already has another metric \"CVSS-v4\" assigned.".to_string(), + message: "Product CSAFPID-9080700 already has another metric \"CVSS-v4\" without a source assigned.".to_string(), instance_path: format!("{}/cvss_v4", csaf_21_path_prefix), }), ("05", &ValidationError { - message: cvss_v31_error_message.to_string(), + message: "Product CSAFPID-9080700 already has another metric \"CVSS-v3.1\" with the same source \ + \"https://www.example.com/.well-known/csaf/clear/2024/esa-2024-0001.json\" assigned.".to_string(), instance_path: format!("{}/cvss_v3", csaf_21_path_prefix), }), ]), From 0e3ce89c4284a022ff2f690f5103286745baa217 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Wed, 4 Jun 2025 09:52:29 +0200 Subject: [PATCH 12/12] Add documentation Added comments to enhance code clarity. --- csaf-lib/src/csaf/validations/test_6_1_07.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/csaf-lib/src/csaf/validations/test_6_1_07.rs b/csaf-lib/src/csaf/validations/test_6_1_07.rs index d251875..a5946e6 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_07.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_07.rs @@ -15,6 +15,7 @@ enum VulnerabilityMetrics { Epss, } +/// Display implementation for VulnerabilityMetrics. impl Display for VulnerabilityMetrics { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -28,6 +29,7 @@ impl Display for VulnerabilityMetrics { } } +/// Returns the name of the metric property for the given metric type. fn get_metric_prop_name(metric: VulnerabilityMetrics) -> &'static str { match metric { SsvcV1 => "ssvc_v1", @@ -39,6 +41,8 @@ fn get_metric_prop_name(metric: VulnerabilityMetrics) -> &'static str { } } +/// Test 6.1.7: Check for multiple identical metric types (with an identical source) per +/// vulnerability. pub fn test_6_1_07_multiple_same_scores_per_product( doc: &impl CsafTrait, ) -> Result<(), ValidationError> {