diff --git a/csaf b/csaf index 1726fcf..bf62791 160000 --- a/csaf +++ b/csaf @@ -1 +1 @@ -Subproject commit 1726fcf10d6e444e6e65a696ac9198c981858d23 +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_0/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs index 185f16b..7135e96 100644 --- a/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs +++ b/csaf-lib/src/csaf/csaf2_0/getter_implementations.rs @@ -1,11 +1,18 @@ -use crate::csaf::csaf2_0::schema::{Branch, CategoryOfTheRemediation, CommonSecurityAdvisoryFramework, DocumentGenerator, DocumentLevelMetaData, DocumentStatus, Flag, FullProductNameT, HelperToIdentifyTheProduct, Id, Involvement, LabelOfTlp, ProductGroup, ProductStatus, ProductTree, Relationship, Remediation, Revision, RulesForSharingDocument, Threat, Tracking, TrafficLightProtocolTlp, Vulnerability}; -use crate::csaf::csaf2_1::schema::{CategoryOfTheRemediation as Remediation21, DocumentStatus as Status21, LabelOfTlp as Tlp21}; -use crate::csaf::getter_traits::{BranchTrait, CsafTrait, DistributionTrait, DocumentTrait, FlagTrait, ProductTrait, GeneratorTrait, InvolvementTrait, MetricTrait, ProductGroupTrait, ProductIdentificationHelperTrait, ProductStatusTrait, ProductTreeTrait, RelationshipTrait, RemediationTrait, RevisionTrait, SharingGroupTrait, ThreatTrait, TlpTrait, TrackingTrait, VulnerabilityTrait, ContentTrait, VulnerabilityIdTrait}; +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; +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 +38,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 } @@ -72,29 +75,73 @@ 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 () { - 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 + } + + fn get_source(&self) -> &Option { + &None } } -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 + ) + } +} + +impl WithGroupIds for Threat { + fn get_group_ids(&self) -> Option + '_> { + self.group_ids.as_ref().map(|g| (*g).iter().map(|x| x.deref())) } } @@ -112,11 +159,12 @@ 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; type VulnerabilityIdType = Id; + type NoteType = Note; fn get_remediations(&self) -> &Vec { &self.remediations @@ -126,9 +174,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 { @@ -154,9 +201,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 +221,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 +260,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 +281,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 +312,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 similarity index 89% rename from csaf-lib/src/csaf/csaf2_1/csaf_json_schema.json rename to csaf-lib/src/csaf/csaf2_1/csaf.json index 042a764..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,18 +234,19 @@ "sudoers.so" ] } - } + }, + "additionalProperties": false } }, "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 +280,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 } @@ -325,12 +329,15 @@ "type": "string", "format": "uri" } - } + }, + "additionalProperties": false } } - } + }, + "additionalProperties": false } - } + }, + "additionalProperties": false }, "lang_t": { "title": "Language type", @@ -385,6 +392,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.", @@ -403,7 +416,8 @@ "Impact on safety systems" ] } - } + }, + "additionalProperties": false } }, "product_group_id_t": { @@ -483,7 +497,8 @@ "type": "string", "format": "uri" } - } + }, + "additionalProperties": false } }, "version_t": { @@ -510,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" }, @@ -557,7 +572,8 @@ "Moderate" ] } - } + }, + "additionalProperties": false }, "category": { "title": "Document category", @@ -616,7 +632,8 @@ "US Federal Civilian Authorities" ] } - } + }, + "additionalProperties": false }, "text": { "title": "Textual description", @@ -661,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.", @@ -735,7 +766,8 @@ "https://www.example.com" ] } - } + }, + "additionalProperties": false }, "references": { "title": "Document references", @@ -836,9 +868,11 @@ "2" ] } - } + }, + "additionalProperties": false } - } + }, + "additionalProperties": false }, "id": { "title": "Unique identifier for the document", @@ -897,7 +931,8 @@ "Initial version." ] } - } + }, + "additionalProperties": false } }, "status": { @@ -913,9 +948,11 @@ "version": { "$ref": "#/$defs/version_t" } - } + }, + "additionalProperties": false } - } + }, + "additionalProperties": false }, "product_tree": { "title": "Product tree", @@ -972,7 +1009,8 @@ "The x64 versions of the operating system." ] } - } + }, + "additionalProperties": false } }, "relationships": { @@ -1016,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", @@ -1074,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)", @@ -1085,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", @@ -1095,7 +1135,8 @@ "4.12" ] } - } + }, + "additionalProperties": false } }, "disclosure_date": { @@ -1110,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.", @@ -1148,7 +1227,8 @@ "product_ids": { "$ref": "#/$defs/products_t" } - } + }, + "additionalProperties": false } }, "ids": { @@ -1186,7 +1266,8 @@ "oasis-tcs/csaf#210" ] } - } + }, + "additionalProperties": false } }, "involvements": { @@ -1204,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.", @@ -1222,6 +1312,9 @@ "vendor" ] }, + "product_ids": { + "$ref": "#/$defs/products_t" + }, "status": { "title": "Party status", "description": "Defines contact status of the involved party.", @@ -1241,7 +1334,8 @@ "type": "string", "minLength": 1 } - } + }, + "additionalProperties": false } }, "metrics": { @@ -1274,10 +1368,42 @@ "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" + } + }, + "additionalProperties": false + }, "ssvc_v1": { "type": "object" } - } + }, + "additionalProperties": false }, "products": { "$ref": "#/$defs/products_t" @@ -1288,7 +1414,8 @@ "type": "string", "format": "uri" } - } + }, + "additionalProperties": false } }, "notes": { @@ -1341,8 +1468,14 @@ "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" } - } + }, + "additionalProperties": false }, "references": { "title": "Vulnerability references", @@ -1437,7 +1570,8 @@ "type": "string", "minLength": 1 } - } + }, + "additionalProperties": false }, "url": { "title": "URL to the remediation", @@ -1445,7 +1579,8 @@ "type": "string", "format": "uri" } - } + }, + "additionalProperties": false } }, "threats": { @@ -1490,7 +1625,8 @@ "product_ids": { "$ref": "#/$defs/products_t" } - } + }, + "additionalProperties": false } }, "title": { @@ -1499,8 +1635,10 @@ "type": "string", "minLength": 1 } - } + }, + "additionalProperties": false } } - } + }, + "additionalProperties": false } diff --git a/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs b/csaf-lib/src/csaf/csaf2_1/getter_implementations.rs index d23c189..cdafab4 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, 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; +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 } @@ -55,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 { @@ -67,13 +73,63 @@ impl MetricTrait for Metric { fn get_content(&self) -> &Self::ContentType { &self.content } + + fn get_source(&self) -> &Option { + &self.source + } } 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 { + fn get_group_ids(&self) -> Option + '_> { + self.group_ids.as_ref().map(|g| (*g).iter().map(|x| x.deref())) + } } impl ThreatTrait for Threat { @@ -94,6 +150,7 @@ impl VulnerabilityTrait for Vulnerability { type FlagType = Flag; type InvolvementType = Involvement; type VulnerabilityIdType = Id; + type NoteType = Note; fn get_remediations(&self) -> &Vec { &self.remediations @@ -103,8 +160,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 { @@ -134,6 +191,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 +207,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 +246,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 +261,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 +286,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/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 09875c9..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 @@ -2307,10 +2392,41 @@ 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" +/// } +/// }, +/// "additionalProperties": false +/// }, /// "ssvc_v1": { /// "type": "object" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "products": { /// "$ref": "#/$defs/products_t" @@ -2321,7 +2437,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -2376,8 +2493,14 @@ 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" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "references": { /// "title": "Vulnerability references", @@ -2470,7 +2593,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "url": { /// "title": "URL to the remediation", @@ -2478,7 +2602,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -2522,7 +2647,8 @@ impl<'de> ::serde::Deserialize<'de> for CommonPlatformEnumerationRepresentation /// "product_ids": { /// "$ref": "#/$defs/products_t" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -2532,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")] @@ -2665,14 +2794,46 @@ 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" +/// } +/// }, +/// "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>, @@ -2680,6 +2841,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 +2857,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(), } } @@ -2840,7 +3004,8 @@ impl<'de> ::serde::Deserialize<'de> for ContributingOrganization { /// "minLength": 32, /// "pattern": "^[0-9a-fA-F]{32,}$" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -2855,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, @@ -3069,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", @@ -3082,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, @@ -3124,7 +3293,6 @@ impl Cwe { /// "4.12" /// ], /// "type": "string", -/// "minLength": 1, /// "pattern": "^[1-9]\\d*\\.([0-9]|([1-9]\\d+))(\\.\\d+)?$" ///} /// ``` @@ -3153,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) @@ -3506,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")] @@ -3577,7 +3745,8 @@ impl DocumentGenerator { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "category": { /// "title": "Document category", @@ -3636,7 +3805,8 @@ impl DocumentGenerator { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "text": { /// "title": "Textual description", @@ -3681,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.", @@ -3755,7 +3939,8 @@ impl DocumentGenerator { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "references": { /// "title": "Document references", @@ -3854,9 +4039,11 @@ impl DocumentGenerator { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "id": { /// "title": "Unique identifier for the document", @@ -3912,7 +4099,8 @@ impl DocumentGenerator { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -3929,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")] @@ -3950,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, @@ -4181,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, @@ -4366,6 +4562,63 @@ 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" +/// } +/// }, +/// "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, + ///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 @@ -4406,11 +4659,13 @@ impl<'de> ::serde::Deserialize<'de> for EntitlementOfTheRemediation { /// "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, @@ -4511,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 @@ -4547,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")] @@ -4665,7 +4980,8 @@ impl Flag { /// "minLength": 32, /// "pattern": "^[0-9a-fA-F]{32,}$" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -4680,17 +4996,18 @@ impl Flag { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, /// "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 +5043,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 /// }, @@ -4774,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, @@ -4828,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, @@ -4919,7 +5242,8 @@ impl GenericUri { /// "minLength": 32, /// "pattern": "^[0-9a-fA-F]{32,}$" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -4934,17 +5258,18 @@ impl GenericUri { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, /// "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 +5305,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 /// }, @@ -5028,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")] @@ -5044,7 +5372,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 +5381,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. @@ -5121,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, @@ -5156,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.", @@ -5173,6 +5512,9 @@ impl Id { /// "vendor" /// ] /// }, +/// "product_ids": { +/// "$ref": "#/$defs/products_t" +/// }, /// "status": { /// "title": "Party status", /// "description": "Defines contact status of the involved party.", @@ -5192,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. @@ -5309,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" /// ] ///} /// ``` @@ -5327,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 { @@ -5338,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") } } } @@ -5352,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()), } @@ -5769,13 +6118,98 @@ impl<'de> ::serde::Deserialize<'de> for LegacyVersionOfTheRevision { }) } } -///Contains all metadata about the metric including products it applies to and the source and the content itself. +///Contains the SPDX license expression for the CSAF document. /// ///
JSON schema /// /// ```json ///{ -/// "title": "metric", +/// "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 +/// +/// ```json +///{ +/// "title": "metric", /// "description": "Contains all metadata about the metric including products it applies to and the source and the content itself.", /// "type": "object", /// "required": [ @@ -5798,10 +6232,41 @@ 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" +/// } +/// }, +/// "additionalProperties": false +/// }, /// "ssvc_v1": { /// "type": "object" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "products": { /// "$ref": "#/$defs/products_t" @@ -5812,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, @@ -5834,14 +6301,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 +6676,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.", @@ -6227,17 +6700,23 @@ 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")] 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 +6964,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.", @@ -6503,7 +6988,8 @@ impl<'de> ::serde::Deserialize<'de> for NoteContent { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 ///} @@ -6631,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 @@ -6734,6 +7299,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 @@ -6771,11 +7506,13 @@ impl ::std::convert::TryFrom<::std::string::String> for PartyStatus { /// "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. @@ -7052,12 +7789,19 @@ 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" /// } -/// } +/// }, +/// "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")] @@ -7083,6 +7827,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 +7847,7 @@ impl ::std::default::Default for ProductStatus { last_affected: Default::default(), recommended: Default::default(), under_investigation: Default::default(), + unknown: Default::default(), } } } @@ -7167,7 +7915,8 @@ impl ProductStatus { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -7211,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, @@ -7359,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, @@ -7424,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")] @@ -7488,7 +8244,8 @@ impl Reference { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 ///} @@ -7559,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, @@ -7768,7 +8527,8 @@ impl ::std::convert::TryFrom<::std::string::String> for RelationshipCategory { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "url": { /// "title": "URL to the remediation", @@ -7776,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, @@ -7847,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, @@ -7908,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, @@ -7974,7 +8740,8 @@ impl Revision { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "text": { /// "title": "Textual description", @@ -8019,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, @@ -8044,14 +8814,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 ///} @@ -8156,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, @@ -9296,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, @@ -9653,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", @@ -9711,7 +10487,8 @@ impl<'de> ::serde::Deserialize<'de> for TitleOfThisDocument { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -9728,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")] @@ -9798,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, @@ -10148,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", @@ -10161,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 @@ -10179,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.", @@ -10214,7 +11032,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "product_ids": { /// "$ref": "#/$defs/products_t" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -10252,7 +11071,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -10270,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.", @@ -10287,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.", @@ -10306,7 +11138,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -10339,10 +11172,41 @@ 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" +/// } +/// }, +/// "additionalProperties": false +/// }, /// "ssvc_v1": { /// "type": "object" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "products": { /// "$ref": "#/$defs/products_t" @@ -10353,7 +11217,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1, /// "uniqueItems": true @@ -10408,8 +11273,14 @@ 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" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "references": { /// "title": "Vulnerability references", @@ -10502,7 +11373,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "type": "string", /// "minLength": 1 /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "url": { /// "title": "URL to the remediation", @@ -10510,7 +11382,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "type": "string", /// "format": "uri" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -10554,7 +11427,8 @@ impl<'de> ::serde::Deserialize<'de> for VersionT { /// "product_ids": { /// "$ref": "#/$defs/products_t" /// } -/// } +/// }, +/// "additionalProperties": false /// }, /// "minItems": 1 /// }, @@ -10564,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")] @@ -10585,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>, @@ -10628,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(), @@ -10744,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\\-_\\.])?$" ///} /// ``` /// @@ -10775,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())) } } @@ -11194,6 +12086,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 +12101,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 +12149,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 +12183,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 +12194,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), } } @@ -11503,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, @@ -11528,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()), @@ -11610,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>, @@ -11696,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?, @@ -11714,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), @@ -11785,6 +12715,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, @@ -11849,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>, @@ -12322,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, @@ -12336,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>>, @@ -12354,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, @@ -12366,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, @@ -12399,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?, }) @@ -12409,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), } @@ -12500,6 +13666,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 +13685,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 +13717,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 +13772,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 +13784,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 +13907,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 +13923,7 @@ pub mod builder { last_affected: Ok(Default::default()), recommended: Ok(Default::default()), under_investigation: Ok(Default::default()), + unknown: Ok(Default::default()), } } } @@ -12819,6 +14028,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 +14055,7 @@ pub mod builder { last_affected: value.last_affected?, recommended: value.recommended?, under_investigation: value.under_investigation?, + unknown: value.unknown?, }) } } @@ -12848,6 +14070,7 @@ pub mod builder { last_affected: Ok(value.last_affected), recommended: Ok(value.recommended), under_investigation: Ok(value.under_investigation), + unknown: Ok(value.unknown), } } } @@ -14120,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, @@ -14169,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()), @@ -14239,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>>, @@ -14369,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?, @@ -14390,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/csaf-lib/src/csaf/getter_traits.rs b/csaf-lib/src/csaf/getter_traits.rs index 2c8d0e5..e42e026 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; @@ -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; @@ -158,28 +169,31 @@ 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; - /// 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. /// @@ -272,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`. /// @@ -335,14 +349,28 @@ pub trait MetricTrait { fn get_products(&self) -> impl Iterator + '_; fn get_content(&self) -> &Self::ContentType; + + fn get_source(&self) -> &Option; } 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. -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 +589,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/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index 3b16988..dac6ada 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -1,6 +1,10 @@ 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_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_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 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..1387a5c --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_05.rs @@ -0,0 +1,46 @@ +use crate::csaf::getter_traits::{CsafTrait, ProductGroupTrait, 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); + } +} 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(), + }), + ]), + ); + } +} 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..a5946e6 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_07.rs @@ -0,0 +1,166 @@ +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}; + +/// Types of metrics known until CSAF 2.1 +#[derive(Hash, Eq, PartialEq, Clone)] +enum VulnerabilityMetrics { + SsvcV1, + CvssV2, + CvssV30, + CvssV31, + CvssV4, + Epss, +} + +/// Display implementation for VulnerabilityMetrics. +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"), + } + } +} + +/// 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", + CvssV2 => "cvss_v2", + CvssV30 => "cvss_v3", + CvssV31 => "cvss_v3", + CvssV4 => "cvss_v4", + Epss => "epss", + } +} + +/// 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> { + 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::<(VulnerabilityMetrics, &Option)>::new(); + if content.has_ssvc_v1() { + content_metrics.push((SsvcV1, m.get_source())); + } + if content.get_cvss_v2().is_some() { + 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, m.get_source())); + } else if version == "3.0" { + 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), + ), + }); + } + } + } + if content.get_cvss_v4().is_some() { + content_metrics.push((CvssV4, m.get_source())); + } + if content.get_epss().is_some() { + 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_src in content_metrics.iter() { + if metrics_set.contains(cm_src) { + return Err(ValidationError { + message: format!( + "Product {} already has another metric \"{}\" {} assigned.", + p, + 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_src.0.to_owned()) + ), + }); + } else { + metrics_set.insert(cm_src.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_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\" 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( + "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: format!("{}/cvss_v3", csaf_21_path_prefix), + }), + ("02", &ValidationError { + 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\" 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\" without a source assigned.".to_string(), + instance_path: format!("{}/cvss_v4", csaf_21_path_prefix), + }), + ("05", &ValidationError { + 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), + }), + ]), + ); + } +} 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(), + }), ]), ); } 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..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(), }) } @@ -109,29 +109,30 @@ 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( "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(), @@ -141,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(), + }), ]) ); - }*/ + } + */ } diff --git a/ssvc b/ssvc index 6557e21..34b5e9b 160000 --- a/ssvc +++ b/ssvc @@ -1 +1 @@ -Subproject commit 6557e21cac6951704b28034e9f137bde8f4049d5 +Subproject commit 34b5e9b3db970826a3122baa3b79f4dc51039aab