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) + ) + } + } +}