From 5d9844c2bac730e4c8428483f60c6b9f14de58d5 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Fri, 21 Mar 2025 13:36:44 +0100 Subject: [PATCH] Refactor validations and improve error reporting Separated validations into individual modules for better modularity. Enhanced ValidationError structure for detailed error reporting, including message and instance path. Updated test cases and references to use the refactored structure. --- csaf-lib/src/csaf/csaf2_0/validation.rs | 33 +--- csaf-lib/src/csaf/csaf2_1/validation.rs | 84 +-------- csaf-lib/src/csaf/getter_traits.rs | 4 +- csaf-lib/src/csaf/helpers.rs | 20 +-- csaf-lib/src/csaf/mod.rs | 1 + csaf-lib/src/csaf/product_helpers.rs | 177 ++++++++++--------- csaf-lib/src/csaf/validation.rs | 118 ++----------- csaf-lib/src/csaf/validations/mod.rs | 4 + csaf-lib/src/csaf/validations/test_6_1_01.rs | 58 ++++++ csaf-lib/src/csaf/validations/test_6_1_02.rs | 70 ++++++++ csaf-lib/src/csaf/validations/test_6_1_34.rs | 85 +++++++++ csaf-lib/src/csaf/validations/test_6_1_35.rs | 101 +++++++++++ 12 files changed, 447 insertions(+), 308 deletions(-) create mode 100644 csaf-lib/src/csaf/validations/mod.rs create mode 100644 csaf-lib/src/csaf/validations/test_6_1_01.rs create mode 100644 csaf-lib/src/csaf/validations/test_6_1_02.rs create mode 100644 csaf-lib/src/csaf/validations/test_6_1_34.rs create mode 100644 csaf-lib/src/csaf/validations/test_6_1_35.rs diff --git a/csaf-lib/src/csaf/csaf2_0/validation.rs b/csaf-lib/src/csaf/csaf2_0/validation.rs index 5f371ca..f42212f 100644 --- a/csaf-lib/src/csaf/csaf2_0/validation.rs +++ b/csaf-lib/src/csaf/csaf2_0/validation.rs @@ -1,6 +1,8 @@ use super::schema::CommonSecurityAdvisoryFramework; -use crate::csaf::validation::{test_6_01_01_missing_definition_of_product_id, test_6_01_02_multiple_definition_of_product_id, Test, Validatable, ValidationPreset}; use std::collections::HashMap; +use crate::csaf::validation::{Test, Validatable, ValidationPreset}; +use crate::csaf::validations::test_6_1_01::test_6_1_01_missing_definition_of_product_id; +use crate::csaf::validations::test_6_1_02::test_6_1_02_multiple_definition_of_product_id; impl Validatable for CommonSecurityAdvisoryFramework { fn presets(&self) -> HashMap> { @@ -20,8 +22,8 @@ impl Validatable for CommonSecurityAdvisoryFram fn tests(&self) -> HashMap<&str, Test> { type CsafTest = Test; HashMap::from([ - ("6.1.1", test_6_01_01_missing_definition_of_product_id as CsafTest), - ("6.1.2", test_6_01_02_multiple_definition_of_product_id as CsafTest), + ("6.1.1", test_6_1_01_missing_definition_of_product_id as CsafTest), + ("6.1.2", test_6_1_02_multiple_definition_of_product_id as CsafTest), ]) } @@ -30,28 +32,3 @@ impl Validatable for CommonSecurityAdvisoryFram } } -#[cfg(test)] -mod tests { - use crate::csaf::csaf2_0::loader::load_document; - use crate::csaf::validation::{test_6_01_01_missing_definition_of_product_id, test_6_01_02_multiple_definition_of_product_id}; - - #[test] - fn test_test_6_01_01() { - let doc = load_document("../csaf/csaf_2.0/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_0-2021-6-1-01-01.json").unwrap(); - assert_eq!( - test_6_01_01_missing_definition_of_product_id(&doc), - Err(String::from("Missing definitions: [\"CSAFPID-9080700\", \"CSAFPID-9080701\"]")) - ) - } - - #[test] - fn test_test_6_01_02() { - let doc = load_document("../csaf/csaf_2.0/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_0-2021-6-1-02-01.json").unwrap(); - assert_eq!( - test_6_01_02_multiple_definition_of_product_id(&doc), - Err(String::from( - "Duplicate definitions: [\"CSAFPID-9080700\"]" - )) - ) - } -} diff --git a/csaf-lib/src/csaf/csaf2_1/validation.rs b/csaf-lib/src/csaf/csaf2_1/validation.rs index 6186f02..70b6a9a 100644 --- a/csaf-lib/src/csaf/csaf2_1/validation.rs +++ b/csaf-lib/src/csaf/csaf2_1/validation.rs @@ -1,5 +1,9 @@ use super::schema::CommonSecurityAdvisoryFramework; -use crate::csaf::validation::{test_6_01_01_missing_definition_of_product_id, test_6_01_02_multiple_definition_of_product_id, test_6_01_34_branches_recursion_depth, test_6_01_35_contradicting_remediations, Test, Validatable, ValidationPreset}; +use crate::csaf::validation::{Test, Validatable, ValidationPreset}; +use crate::csaf::validations::test_6_1_01::test_6_1_01_missing_definition_of_product_id; +use crate::csaf::validations::test_6_1_02::test_6_1_02_multiple_definition_of_product_id; +use crate::csaf::validations::test_6_1_34::test_6_1_34_branches_recursion_depth; +use crate::csaf::validations::test_6_1_35::test_6_1_35_contradicting_remediations; use std::collections::HashMap; impl Validatable for CommonSecurityAdvisoryFramework { @@ -20,10 +24,10 @@ impl Validatable for CommonSecurityAdvisoryFram fn tests(&self) -> HashMap<&str, Test> { type CsafTest = Test; HashMap::from([ - ("6.1.1", test_6_01_01_missing_definition_of_product_id as CsafTest), - ("6.1.2", test_6_01_02_multiple_definition_of_product_id as CsafTest), - ("6.1.34", test_6_01_34_branches_recursion_depth as CsafTest), - ("6.1.35", test_6_01_35_contradicting_remediations as CsafTest), + ("6.1.1", test_6_1_01_missing_definition_of_product_id as CsafTest), + ("6.1.2", test_6_1_02_multiple_definition_of_product_id as CsafTest), + ("6.1.34", test_6_1_34_branches_recursion_depth as CsafTest), + ("6.1.35", test_6_1_35_contradicting_remediations as CsafTest), ]) } @@ -31,73 +35,3 @@ impl Validatable for CommonSecurityAdvisoryFram self } } - -#[cfg(test)] -mod tests { - use crate::csaf::csaf2_1::loader::load_document; - use crate::csaf::validation::{test_6_01_01_missing_definition_of_product_id, test_6_01_02_multiple_definition_of_product_id, test_6_01_34_branches_recursion_depth, test_6_01_35_contradicting_remediations}; - - #[test] - fn test_test_6_01_01() { - let doc = load_document("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-01-01.json").unwrap(); - assert_eq!( - test_6_01_01_missing_definition_of_product_id(&doc), - Err(String::from("Missing definitions: [\"CSAFPID-9080700\", \"CSAFPID-9080701\"]")) - ) - } - - #[test] - fn test_test_6_01_02() { - let doc = load_document("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-02-01.json").unwrap(); - assert_eq!( - test_6_01_02_multiple_definition_of_product_id(&doc), - Err(String::from( - "Duplicate definitions: [\"CSAFPID-9080700\"]" - )) - ) - } - - #[test] - fn test_test_6_01_34() { - for x in ["11"].iter() { - let doc = load_document(format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-34-{}.json", x).as_str()).unwrap(); - assert_eq!( - Ok(()), - test_6_01_35_contradicting_remediations(&doc) - ) - } - for (x, err) in [ - ("01", "Branches recursion depth too big (> 30)"), - ("02", "Branches recursion depth too big (> 30)"), - ].iter() { - let doc = load_document(format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-34-{}.json", x).as_str()).unwrap(); - assert_eq!( - Err(format!("{}", err)), - test_6_01_34_branches_recursion_depth(&doc) - ) - } - } - - #[test] - fn test_test_6_01_35() { - for x in ["11", "12", "13", "14"].iter() { - let doc = load_document(format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-35-{}.json", x).as_str()).unwrap(); - assert_eq!( - Ok(()), - test_6_01_35_contradicting_remediations(&doc) - ) - } - for (x, err) in [ - ("01", "Product CSAFPID-9080700 has contradicting remediations: no_fix_planned and vendor_fix"), - ("02", "Product CSAFPID-9080700 has contradicting remediations: none_available and mitigation"), - ("03", "Product CSAFPID-9080702 has contradicting remediations: workaround, fix_planned and optional_patch"), - ("04", "Product CSAFPID-9080701 has contradicting remediations: mitigation, fix_planned and optional_patch"), - ].iter() { - let doc = load_document(format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-35-{}.json", x).as_str()).unwrap(); - assert_eq!( - Err(format!("{}", err)), - test_6_01_35_contradicting_remediations(&doc) - ) - } - } -} diff --git a/csaf-lib/src/csaf/getter_traits.rs b/csaf-lib/src/csaf/getter_traits.rs index c01d170..0621832 100644 --- a/csaf-lib/src/csaf/getter_traits.rs +++ b/csaf-lib/src/csaf/getter_traits.rs @@ -82,12 +82,12 @@ pub trait RemediationTrait { None } else { let mut product_set: BTreeSet = match self.get_product_ids() { - Some(product_ids) => product_ids.iter().map(|p| p.to_string()).collect(), + Some(product_ids) => product_ids.iter().map(|id| (*id).to_owned()).collect(), None => BTreeSet::new(), }; if let Some(product_groups) = self.get_group_ids() { if let Some(product_ids) = resolve_product_groups(doc, product_groups) { - product_set.extend(product_ids.iter().map(|p| p.to_string())); + product_set.extend(product_ids.iter().map(|id| id.to_owned())); } } Some(product_set) diff --git a/csaf-lib/src/csaf/helpers.rs b/csaf-lib/src/csaf/helpers.rs index 9dd47fe..9f4360a 100644 --- a/csaf-lib/src/csaf/helpers.rs +++ b/csaf-lib/src/csaf/helpers.rs @@ -1,23 +1,5 @@ use crate::csaf::getter_traits::{CsafTrait, ProductGroupTrait, ProductTreeTrait}; -use std::collections::{BTreeSet, HashMap}; - -pub fn find_duplicates(vec: Vec) -> Vec { - let mut occurrences = HashMap::new(); - let mut duplicates = Vec::new(); - - for item in vec.iter() { - let count = occurrences.entry(item.clone()).or_insert(0); - *count += 1; - } - - for (item, count) in occurrences { - if count > 1 { - duplicates.push(item); - } - } - - duplicates -} +use std::collections::BTreeSet; pub fn resolve_product_groups(doc: &impl CsafTrait, product_groups: Vec<&String>) -> Option> { doc.get_product_tree().map(|product_tree| { diff --git a/csaf-lib/src/csaf/mod.rs b/csaf-lib/src/csaf/mod.rs index fe218f6..962f04f 100644 --- a/csaf-lib/src/csaf/mod.rs +++ b/csaf-lib/src/csaf/mod.rs @@ -4,3 +4,4 @@ mod helpers; pub mod product_helpers; pub mod validation; pub mod getter_traits; +pub mod validations; diff --git a/csaf-lib/src/csaf/product_helpers.rs b/csaf-lib/src/csaf/product_helpers.rs index 57b6998..8c46d5c 100644 --- a/csaf-lib/src/csaf/product_helpers.rs +++ b/csaf-lib/src/csaf/product_helpers.rs @@ -1,21 +1,24 @@ use crate::csaf::getter_traits::{BranchTrait, CsafTrait, FullProductNameTrait, MetricTrait, ProductGroupTrait, ProductStatusTrait, ProductTreeTrait, RelationshipTrait, RemediationTrait, ThreatTrait, VulnerabilityTrait}; -use std::collections::HashSet; -pub fn gather_product_references(doc: &impl CsafTrait) -> HashSet { - let mut ids = HashSet::::new(); +pub fn gather_product_references(doc: &impl CsafTrait) -> Vec<(String, String)> { + let mut ids = Vec::<(String, String)>::new(); - if let Some(x) = doc.get_product_tree().as_ref() { + if let Some(pt) = doc.get_product_tree().as_ref() { // /product_tree/product_groups[]/product_ids[] - ids.extend(x.get_product_groups().iter().flat_map(|x| x.get_product_ids()).map(|x| x.to_owned())); - + for (g_i, g) in pt.get_product_groups().iter().enumerate() { + for (i_i, i) in g.get_product_ids().iter().enumerate() { + ids.push(((*i).to_owned(), format!("/product_tree/product_groups/{}/product_ids/{}", g_i, i_i))) + } + } // /product_tree/relationships[]/product_reference - ids.extend(x.get_relationships().iter().map(|x| x.get_product_reference().to_owned())); - // /product_tree/relationships[]/relates_to_product_reference - ids.extend(x.get_relationships().iter().map(|x| x.get_relates_to_product_reference().to_owned())); + for (r_i, r) in pt.get_relationships().iter().enumerate() { + ids.push((r.get_product_reference().to_owned(), format!("/product_tree/relationships/{}/product_reference", r_i))); + ids.push((r.get_relates_to_product_reference().to_owned(), format!("/product_tree/relationships/{}/relates_to_product_reference", r_i))); + } } - for vuln in doc.get_vulnerabilities().iter() { + for (v_i, v) in doc.get_vulnerabilities().iter().enumerate() { // /vulnerabilities[]/product_status/first_affected[] // /vulnerabilities[]/product_status/first_fixed[] // /vulnerabilities[]/product_status/fixed[] @@ -24,48 +27,73 @@ pub fn gather_product_references(doc: &impl CsafTrait) -> HashSet { // /vulnerabilities[]/product_status/last_affected[] // /vulnerabilities[]/product_status/recommended[] // /vulnerabilities[]/product_status/under_investigation[] - if let Some(status) = vuln.get_product_status().as_ref() { - if let Some(x) = status.get_first_affected().as_ref() { - ids.extend(x.iter().map(|x| (*x).clone())); + if let Some(status) = v.get_product_status().as_ref() { + if let Some(fa) = status.get_first_affected().as_ref() { + for (x_i, x) in fa.iter().enumerate() { + ids.push(((*x).to_owned(), format!("/vulnerabilities/{}/product_status/first_affected/{}", v_i, x_i))); + } } - if let Some(x) = status.get_first_fixed().as_ref() { - ids.extend(x.iter().map(|x| (*x).clone())); + if let Some(ff) = status.get_first_fixed().as_ref() { + for (x_i, x) in ff.iter().enumerate() { + ids.push(((*x).to_owned(), format!("/vulnerabilities/{}/product_status/first_fixed/{}", v_i, x_i))); + } } - if let Some(x) = status.get_fixed().as_ref() { - ids.extend(x.iter().map(|x| (*x).clone())); + if let Some(f) = status.get_fixed().as_ref() { + for (x_i, x) in f.iter().enumerate() { + ids.push(((*x).to_owned(), format!("/vulnerabilities/{}/product_status/fixed/{}", v_i, x_i))); + } } - if let Some(x) = status.get_known_affected().as_ref() { - ids.extend(x.iter().map(|x| (*x).clone())); + if let Some(ka) = status.get_known_affected().as_ref() { + for (x_i, x) in ka.iter().enumerate() { + ids.push(((*x).to_owned(), format!("/vulnerabilities/{}/product_status/known_affected/{}", v_i, x_i))); + } } - if let Some(x) = status.get_last_affected().as_ref() { - ids.extend(x.iter().map(|x| (*x).clone())); + if let Some(kna) = status.get_known_not_affected().as_ref() { + for (x_i, x) in kna.iter().enumerate() { + ids.push(((*x).to_owned(), format!("/vulnerabilities/{}/product_status/known_not_affected/{}", v_i, x_i))); + } } - if let Some(x) = status.get_recommended().as_ref() { - ids.extend(x.iter().map(|x| (*x).clone())); + if let Some(la) = status.get_last_affected().as_ref() { + for (x_i, x) in la.iter().enumerate() { + ids.push(((*x).to_owned(), format!("/vulnerabilities/{}/product_status/last_affected/{}", v_i, x_i))); + } } - if let Some(x) = status.get_under_investigation().as_ref() { - ids.extend(x.iter().map(|x| (*x).clone())); + if let Some(r) = status.get_recommended().as_ref() { + for (x_i, x) in r.iter().enumerate() { + ids.push(((*x).to_owned(), format!("/vulnerabilities/{}/product_status/recommended/{}", v_i, x_i))); + } + } + if let Some(ui) = status.get_under_investigation().as_ref() { + for (x_i, x) in ui.iter().enumerate() { + ids.push(((*x).to_owned(), format!("/vulnerabilities/{}/product_status/under_investigation/{}", v_i, x_i))); + } } } // /vulnerabilities[]/remediations[]/product_ids[] - for rem in vuln.get_remediations().iter() { - if let Some(x) = rem.get_product_ids().as_ref() { - ids.extend(x.iter().map(|x| (*x).clone())); + for (rem_i, rem) in v.get_remediations().iter().enumerate() { + if let Some(product_ids) = rem.get_product_ids().as_ref() { + for (x_i, x) in product_ids.iter().enumerate() { + ids.push(((*x).to_owned(), format!("/vulnerabilities/{}/remediations/{}/product_ids/{}", v_i, rem_i, x_i))); + } } } // /vulnerabilities[]/metrics[]/products[] - if let Some(metrics) = vuln.get_metrics().as_ref() { - metrics.iter().for_each(|metric| { - ids.extend(metric.get_products().iter().map(|x| (*x).clone())) - }); + if let Some(metrics) = v.get_metrics().as_ref() { + for (metric_i, metric) in metrics.iter().enumerate() { + for (x_i, x) in metric.get_products().iter().enumerate() { + ids.push(((*x).to_owned(), format!("/vulnerabilities/{}/metrics/{}/products/{}", v_i, metric_i, x_i))); + } + } } // /vulnerabilities[]/threats[]/product_ids[] - for threat in vuln.get_threats().iter() { - if let Some(x) = threat.get_product_ids().as_ref() { - ids.extend(x.iter().map(|x| (*x).clone())); + for (threat_i, threat) in v.get_threats().iter().enumerate() { + if let Some(product_ids) = threat.get_product_ids().as_ref() { + for (x_i, x) in product_ids.iter().enumerate() { + ids.push(((*x).to_owned(), format!("/vulnerabilities/{}/threats/{}/product_ids/{}", v_i, threat_i, x_i))); + } } } } @@ -73,72 +101,53 @@ pub fn gather_product_references(doc: &impl CsafTrait) -> HashSet { ids } -pub fn gather_product_definitions_from_branch(branch: &impl BranchTrait) -> Vec { - let mut ids = Vec::::new(); - +fn gather_product_definitions_from_branch( + branch: &impl BranchTrait, + ids: &mut Vec<(String, String)>, + path: &str +) { // Gather from /product/product_id if let Some(product) = branch.get_product() { - ids.push(product.get_product_id().to_owned()); - } - - // Go into the branch - if let Some(x) = branch.get_branches().as_ref() { - ids.extend( - x.iter() - .flat_map(|x| gather_product_definitions_from_branch(x)), - ) + ids.push(( + product.get_product_id().to_owned(), + format!("{}/product/product_id", path) + )); } - ids -} - -pub fn check_branch_depth(branch: &impl BranchTrait, max_depth: u32, depth: u32) -> bool { - // Recurse into sub-branches. - if let Some(x) = branch.get_branches().as_ref() { - if depth == max_depth { - // Since we are inspecting the children, they will have a depth of max_depth + 1. - return false - } - if !x.iter().all(|x| check_branch_depth(x, max_depth, depth + 1)) { - // Check recursively if any sub-branch exceeds the recursion limit. - return false + // Go into the sub-branches + if let Some(branches) = branch.get_branches().as_ref() { + for (i, b) in branches.iter().enumerate() { + gather_product_definitions_from_branch(b, ids, &format!("{}/branches/{}", path, i)); } } - true } -pub fn check_branch_depth_tree(tree: &impl ProductTreeTrait, max_depth: u32) -> bool { - // All children of the root branch have depth 1, perform recursive depth check on them. - if let Some(x) = tree.get_branches().as_ref() { - x.iter().all(|x| check_branch_depth(x, max_depth, 1)) - } else { - true - } -} - -pub fn gather_product_definitions(doc: &impl CsafTrait) -> Vec { - let mut ids = Vec::::new(); +pub fn gather_product_definitions(doc: &impl CsafTrait) -> Vec<(String, String)> { + let mut ids = Vec::<(String, String)>::new(); if let Some(tree) = doc.get_product_tree().as_ref() { // /product_tree/branches[](/branches[])*/product/product_id - if let Some(branch) = tree.get_branches().as_ref() { - for sub_branch in branch.iter() { - ids.extend( - gather_product_definitions_from_branch(sub_branch).iter() - .map(|x| x.to_owned()) - ); + if let Some(branches) = tree.get_branches().as_ref() { + for (i, branch) in branches.iter().enumerate() { + gather_product_definitions_from_branch(branch, &mut ids, &format!("/product_tree/branches/{}", i)); } } // /product_tree/full_product_names[]/product_id - ids.extend(tree.get_full_product_names().iter().map(|x| x.get_product_id().to_owned())); + for (i, fpn) in tree.get_full_product_names().iter().enumerate() { + ids.push(( + fpn.get_product_id().to_owned(), + format!("/product_tree/full_product_names/{}/product_id", i) + )); + } // /product_tree/relationships[]/full_product_name/product_id - ids.extend( - tree.get_relationships() - .iter() - .map(|x| x.get_full_product_name().get_product_id().to_owned()), - ); + for (i, rel) in tree.get_relationships().iter().enumerate() { + ids.push(( + rel.get_full_product_name().get_product_id().to_owned(), + format!("/product_tree/relationships/{}/full_product_name/product_id", i) + )); + } } ids diff --git a/csaf-lib/src/csaf/validation.rs b/csaf-lib/src/csaf/validation.rs index 1bbd78b..c636504 100644 --- a/csaf-lib/src/csaf/validation.rs +++ b/csaf-lib/src/csaf/validation.rs @@ -1,11 +1,22 @@ -use crate::csaf::csaf2_1::schema::CategoryOfTheRemediation; -use crate::csaf::getter_traits::{CsafTrait, RemediationTrait, VulnerabilityTrait}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::str::FromStr; -use crate::csaf::helpers::find_duplicates; -use crate::csaf::product_helpers::{check_branch_depth_tree, gather_product_definitions, gather_product_references}; -pub enum ValidationError {} +#[derive(Debug, PartialEq, Eq, Hash, Clone, serde::Serialize)] +pub struct ValidationError { + pub message: String, + #[serde(rename = "instancePath")] + pub instance_path: String, +} + +impl std::fmt::Display for ValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "ValidationError: {} at {}", + self.message, self.instance_path + ) + } +} #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub enum ValidationPreset { @@ -36,7 +47,7 @@ pub trait Validate { } pub type Test = - fn(&VersionedDocument) -> Result<(), String>; + fn(&VersionedDocument) -> Result<(), ValidationError>; /// Represents something which is validatable according to the CSAF standard. /// This trait MUST be implemented by the struct that represents a CSAF document @@ -87,96 +98,3 @@ pub fn validate_by_test( println!("Test with ID {} is missing implementation", test_id); } } - -pub fn test_6_01_01_missing_definition_of_product_id( - doc: &impl CsafTrait, -) -> Result<(), String> { - let definitions = gather_product_definitions(doc); - let definitions_set = HashSet::::from_iter(definitions.iter().map(|x| x.clone().clone())); - let references = gather_product_references(doc); - - let mut missing = references.difference(&definitions_set).collect::>(); - missing.sort(); - - if missing.is_empty() { - Ok(()) - } else { - Err(format!("Missing definitions: {:?}", missing)) - } -} - -pub fn test_6_01_02_multiple_definition_of_product_id( - doc: &impl CsafTrait, -) -> Result<(), String> { - let definitions = gather_product_definitions(doc); - let duplicates = find_duplicates(definitions); - - if duplicates.is_empty() { - Ok(()) - } else { - Err(format!("Duplicate definitions: {:?}", duplicates)) - } -} - -pub fn test_6_01_34_branches_recursion_depth( - doc: &impl CsafTrait, -) -> Result<(), String> { - if let Some(x) = doc.get_product_tree().as_ref() { - if !check_branch_depth_tree(x, 30) { - return Err(format!("Branches recursion depth too big (> {:?})", 30)); - } - } - Ok(()) -} - -static MUT_EX_MEASURES: &[CategoryOfTheRemediation] = &[ - CategoryOfTheRemediation::NoneAvailable, - CategoryOfTheRemediation::Workaround, - CategoryOfTheRemediation::Mitigation, -]; - -static MUT_EX_FIX_STATES: &[CategoryOfTheRemediation] = &[ - CategoryOfTheRemediation::NoneAvailable, - CategoryOfTheRemediation::NoFixPlanned, - CategoryOfTheRemediation::FixPlanned, - CategoryOfTheRemediation::OptionalPatch, - CategoryOfTheRemediation::VendorFix, -]; - -pub fn test_6_01_35_contradicting_remediations( - target: &impl CsafTrait, -) -> Result<(), String> { - for v in target.get_vulnerabilities().iter() { - // Data struct to store observed remediation categories per product IT - let mut product_categories: HashMap> = HashMap::new(); - for r in v.get_remediations().iter() { - // Only handle Remediations having product IDs associated - if let Some(product_ids) = r.get_all_product_ids(target) { - // Category of current remediation - let cat = r.get_category(); - // Iterate over product IDs - for p in product_ids { - // Check if product ID has categories associated - if let Some(exist_cat_set) = product_categories.get(&p) { - // Check if any seen category conflicts with the current one - if exist_cat_set.iter().any(|e_cat| { - MUT_EX_MEASURES.contains(e_cat) && MUT_EX_MEASURES.contains(&cat) - || MUT_EX_FIX_STATES.contains(e_cat) && MUT_EX_FIX_STATES.contains(&cat) - }) { - return Err(format!( - "Product {} has contradicting remediations: {} and {}", - p, exist_cat_set.iter().map(|c| c.to_string()).collect::>().join(", "), cat - )); - } - let mut new_cat_vec = exist_cat_set.clone(); - new_cat_vec.push(cat.clone()); - product_categories.insert(p, new_cat_vec); - } else { - product_categories.insert(p, Vec::from([cat.clone()])); - } - } - } - } - } - Ok(()) -} diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs new file mode 100644 index 0000000..223a8c0 --- /dev/null +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -0,0 +1,4 @@ +pub mod test_6_1_01; +pub mod test_6_1_02; +pub mod test_6_1_34; +pub mod test_6_1_35; \ No newline at end of file diff --git a/csaf-lib/src/csaf/validations/test_6_1_01.rs b/csaf-lib/src/csaf/validations/test_6_1_01.rs new file mode 100644 index 0000000..07723f3 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_01.rs @@ -0,0 +1,58 @@ +use crate::csaf::getter_traits::CsafTrait; +use crate::csaf::product_helpers::{gather_product_definitions, gather_product_references}; +use std::collections::HashSet; +use crate::csaf::validation::ValidationError; + +pub fn test_6_1_01_missing_definition_of_product_id( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + let definitions = gather_product_definitions(doc); + let definitions_set = HashSet::::from_iter(definitions.iter().map(|x| x.1.to_owned())); + let references = gather_product_references(doc); + + for (ref_id, ref_path) in references.iter() { + if !definitions_set.contains(ref_id) { + return Err(ValidationError { + message: format!("Missing definition of product_id: {}", ref_id), + instance_path: ref_path.to_string(), + }) + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::csaf::csaf2_0::loader::load_document as load_20; + use crate::csaf::csaf2_1::loader::load_document as load_21; + use crate::csaf::validation::ValidationError; + use crate::csaf::validations::test_6_1_01::test_6_1_01_missing_definition_of_product_id; + + static EXPECTED_ERROR: &str = "Missing definition of product_id: CSAFPID-9080700"; + static EXPECTED_INSTANCE_PATH: &str = "/product_tree/product_groups/0/product_ids/0"; + + #[test] + fn test_6_1_01_csaf_2_0() { + let doc = load_20("../csaf/csaf_2.0/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_0-2021-6-1-01-01.json").unwrap(); + assert_eq!( + test_6_1_01_missing_definition_of_product_id(&doc), + Err(ValidationError { + message: EXPECTED_ERROR.to_string(), + instance_path: EXPECTED_INSTANCE_PATH.to_string(), + }) + ); + } + + #[test] + fn test_6_1_01_csaf_2_1() { + let doc = load_21("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-01-01.json").unwrap(); + assert_eq!( + test_6_1_01_missing_definition_of_product_id(&doc), + Err(ValidationError { + message: EXPECTED_ERROR.to_string(), + instance_path: EXPECTED_INSTANCE_PATH.to_string(), + }) + ); + } +} diff --git a/csaf-lib/src/csaf/validations/test_6_1_02.rs b/csaf-lib/src/csaf/validations/test_6_1_02.rs new file mode 100644 index 0000000..fca1a0d --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_02.rs @@ -0,0 +1,70 @@ +use crate::csaf::getter_traits::CsafTrait; +use crate::csaf::product_helpers::gather_product_definitions; +use crate::csaf::validation::ValidationError; +use std::collections::HashMap; + +pub fn test_6_1_02_multiple_definition_of_product_id( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + let definitions: Vec<_> = gather_product_definitions(doc); + let duplicates = find_duplicates(definitions); + + if let Some(duplicate) = duplicates.first() { + Err(ValidationError { + message: format!("Duplicate definition for product ID {}", duplicate.0), + instance_path: duplicate.1[1].to_owned(), + }) + } else { + Ok(()) + } +} + +fn find_duplicates(vec: Vec<(String, String)>) -> Vec<(String, Vec)> { + // Map to store each key with all of its paths + let mut conflicts = HashMap::new(); + + for (key, path) in vec { + // Add this path to the list for this key + conflicts.entry(key).or_insert_with(Vec::new).push(path); + } + + // Filter to keep only entries with multiple paths (actual duplicates) + conflicts.into_iter() + .filter(|(_, paths)| paths.len() > 1) + .collect() +} + +#[cfg(test)] +mod tests { + use crate::csaf::csaf2_0::loader::load_document as load_20; + use crate::csaf::csaf2_1::loader::load_document as load_21; + use crate::csaf::validation::ValidationError; + use crate::csaf::validations::test_6_1_02::test_6_1_02_multiple_definition_of_product_id; + + static EXPECTED_ERROR: &str = "Duplicate definition for product ID CSAFPID-9080700"; + static EXPECTED_INSTANCE_PATH: &str = "/product_tree/full_product_names/1/product_id"; + + #[test] + fn test_test_6_1_02_csaf_2_0() { + let doc = load_20("../csaf/csaf_2.0/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_0-2021-6-1-02-01.json").unwrap(); + assert_eq!( + test_6_1_02_multiple_definition_of_product_id(&doc), + Err(ValidationError { + message: EXPECTED_ERROR.to_string(), + instance_path: EXPECTED_INSTANCE_PATH.to_string(), + }) + ) + } + + #[test] + fn test_test_6_1_02_csaf_2_1() { + let doc = load_21("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-02-01.json").unwrap(); + assert_eq!( + test_6_1_02_multiple_definition_of_product_id(&doc), + Err(ValidationError { + message: EXPECTED_ERROR.to_string(), + instance_path: EXPECTED_INSTANCE_PATH.to_string(), + }) + ) + } +} diff --git a/csaf-lib/src/csaf/validations/test_6_1_34.rs b/csaf-lib/src/csaf/validations/test_6_1_34.rs new file mode 100644 index 0000000..33b7b0d --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_34.rs @@ -0,0 +1,85 @@ +use crate::csaf::getter_traits::{BranchTrait, CsafTrait, ProductTreeTrait}; +use crate::csaf::validation::ValidationError; + +static MAX_DEPTH: u32 = 30; + +pub fn test_6_1_34_branches_recursion_depth( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + if let Some(tree) = doc.get_product_tree().as_ref() { + if let Some(path) = find_excessive_branch_depth(tree.get_branches(), MAX_DEPTH) { + return Err(ValidationError { + message: format!("Branches recursion depth too big (> {})", MAX_DEPTH), + instance_path: format!("/product_tree{}", path) + }); + } + } + Ok(()) +} + +fn find_excessive_branch_depth(branches: Option<&Vec>, remaining_depth: u32) -> Option { + if let Some(branches) = branches { + for (i, branch) in branches.iter().enumerate() { + if let Some(subpath) = find_excessive_branch_depth_rec(branch, remaining_depth) { + return Some(format!("/branches/{}{}", i, subpath)); + } + } + } + None +} + +fn find_excessive_branch_depth_rec(branch: &impl BranchTrait, remaining_depth: u32) -> Option { + if let Some(branches) = branch.get_branches() { + // If we've reached depth limit and there are branches, we've found a violation + if remaining_depth == 1 { + return Some("/branches/0".to_string()); + } + + // Otherwise, check the branches with one less remaining depth + return find_excessive_branch_depth(Some(branches), remaining_depth - 1); + } + + None +} + +#[cfg(test)] +mod tests { + use crate::csaf::csaf2_1::loader::load_document; + use crate::csaf::validation::ValidationError; + use crate::csaf::validations::test_6_1_34::test_6_1_34_branches_recursion_depth; + + #[test] + fn test_test_6_1_34() { + for x in ["11"].iter() { + let doc = load_document(format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-34-{}.json", x).as_str()).unwrap(); + assert_eq!( + Ok(()), + test_6_1_34_branches_recursion_depth(&doc) + ) + } + for (x, err) in [ + ("01", ValidationError { + message: "Branches recursion depth too big (> 30)".to_string(), + instance_path: "/product_tree/branches/0/branches/0/branches/0/branches/0\ + /branches/0/branches/0/branches/0/branches/0/branches/0/branches/0/branches/0\ + /branches/0/branches/0/branches/0/branches/0/branches/0/branches/0/branches/0\ + /branches/0/branches/0/branches/0/branches/0/branches/0/branches/0/branches/0\ + /branches/0/branches/0/branches/0/branches/0/branches/0/branches/0".to_string() + }), + ("02", ValidationError { + message: "Branches recursion depth too big (> 30)".to_string(), + instance_path: "/product_tree/branches/0/branches/0/branches/1/branches/0\ + /branches/0/branches/0/branches/0/branches/0/branches/0/branches/0/branches/0\ + /branches/0/branches/0/branches/0/branches/0/branches/0/branches/0/branches/0\ + /branches/0/branches/0/branches/0/branches/0/branches/0/branches/0/branches/0\ + /branches/0/branches/0/branches/0/branches/0/branches/0/branches/0".to_string() + }), + ].iter() { + let doc = load_document(format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-34-{}.json", x).as_str()).unwrap(); + assert_eq!( + Err(err.to_owned()), + test_6_1_34_branches_recursion_depth(&doc) + ) + } + } +} diff --git a/csaf-lib/src/csaf/validations/test_6_1_35.rs b/csaf-lib/src/csaf/validations/test_6_1_35.rs new file mode 100644 index 0000000..941fe54 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_35.rs @@ -0,0 +1,101 @@ +use crate::csaf::csaf2_1::schema::CategoryOfTheRemediation; +use crate::csaf::getter_traits::{CsafTrait, RemediationTrait, VulnerabilityTrait}; +use std::collections::BTreeMap; +use crate::csaf::validation::ValidationError; + +static MUT_EX_MEASURES: &[CategoryOfTheRemediation] = &[ + CategoryOfTheRemediation::NoneAvailable, + CategoryOfTheRemediation::Workaround, + CategoryOfTheRemediation::Mitigation, +]; + +static MUT_EX_FIX_STATES: &[CategoryOfTheRemediation] = &[ + CategoryOfTheRemediation::NoneAvailable, + CategoryOfTheRemediation::NoFixPlanned, + CategoryOfTheRemediation::FixPlanned, + CategoryOfTheRemediation::OptionalPatch, + CategoryOfTheRemediation::VendorFix, +]; + +pub fn test_6_1_35_contradicting_remediations( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + for (v_i, v) in doc.get_vulnerabilities().iter().enumerate() { + // Data struct to store observed remediation categories per product IT + let mut product_categories: BTreeMap> = BTreeMap::new(); + for (r_i, r) in v.get_remediations().iter().enumerate() { + // Only handle Remediations having product IDs associated + if let Some(product_ids) = r.get_all_product_ids(doc) { + // Category of current remediation + let cat = r.get_category(); + // Iterate over product IDs + for p in product_ids { + // Check if product ID has categories associated + if let Some(exist_cat_set) = product_categories.get_mut(&p) { + // Check if any seen category conflicts with the current one + if exist_cat_set.iter().any(|e_cat| { + MUT_EX_MEASURES.contains(e_cat) && MUT_EX_MEASURES.contains(&cat) + || MUT_EX_FIX_STATES.contains(e_cat) && MUT_EX_FIX_STATES.contains(&cat) + }) { + return Err(ValidationError { + message: format!( + "Product {} has contradicting remediations: {} and {}", + p, + exist_cat_set.iter().map(|c| c.to_string()).collect::>().join(", "), + cat + ), + instance_path: format!("/vulnerabilities/{}/remediations/{}", v_i, r_i), + }); + } + exist_cat_set.push(cat); + } else { + product_categories.insert(p, Vec::from([cat])); + } + } + } + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::csaf::csaf2_1::loader::load_document; + use crate::csaf::validation::ValidationError; + use crate::csaf::validations::test_6_1_35::test_6_1_35_contradicting_remediations; + + #[test] + fn test_test_6_1_35() { + for x in ["11", "12", "13", "14"].iter() { + let doc = load_document(format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-35-{}.json", x).as_str()).unwrap(); + assert_eq!( + Ok(()), + test_6_1_35_contradicting_remediations(&doc) + ) + } + for (x, err) in [ + ("01", ValidationError { + message: "Product CSAFPID-9080700 has contradicting remediations: no_fix_planned and vendor_fix".to_string(), + instance_path: "/vulnerabilities/0/remediations/1".to_string() + }), + ("02", ValidationError { + message: "Product CSAFPID-9080700 has contradicting remediations: none_available and mitigation".to_string(), + instance_path: "/vulnerabilities/0/remediations/1".to_string() + }), + ("03", ValidationError { + message: "Product CSAFPID-9080702 has contradicting remediations: workaround, fix_planned and optional_patch".to_string(), + instance_path: "/vulnerabilities/0/remediations/2".to_string(), + }), + ("04", ValidationError { + message: "Product CSAFPID-9080701 has contradicting remediations: mitigation, fix_planned and optional_patch".to_string(), + instance_path: "/vulnerabilities/0/remediations/2".to_string(), + }), + ].iter() { + let doc = load_document(format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-35-{}.json", x).as_str()).unwrap(); + assert_eq!( + Err(err.clone()), + test_6_1_35_contradicting_remediations(&doc) + ) + } + } +}