From eb532c0ee204981fdb2467ab78cc220324d97b32 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Mon, 19 May 2025 16:15:17 +0200 Subject: [PATCH 1/4] Refactor test helper for multi-version CSAF support Introduced a generic `run_csaf_tests` function to handle CSAF 2.0 and 2.1 test cases, reducing code duplication. Updated `test_helper` and validation tests to use the new approach, improving maintainability and clarity. --- csaf | 2 +- csaf-lib/src/csaf/test_helper.rs | 53 ++++++++++++++------ csaf-lib/src/csaf/validations/test_6_1_01.rs | 37 +++++--------- csaf-lib/src/csaf/validations/test_6_1_02.rs | 37 +++++--------- csaf-lib/src/csaf/validations/test_6_1_34.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_35.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_36.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_37.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_38.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_39.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_40.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_41.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_42.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_43.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_44.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_45.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_46.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_47.rs | 2 +- csaf-lib/src/csaf/validations/test_6_1_48.rs | 8 +-- csaf-lib/src/csaf/validations/test_6_1_49.rs | 6 +-- 20 files changed, 85 insertions(+), 86 deletions(-) diff --git a/csaf b/csaf index 62fb87c..500de13 160000 --- a/csaf +++ b/csaf @@ -1 +1 @@ -Subproject commit 62fb87c6b6d0754bf0a4df20fc9e3d387497d0fe +Subproject commit 500de13452015136756e64ae03f84bdc9fd6405b diff --git a/csaf-lib/src/csaf/test_helper.rs b/csaf-lib/src/csaf/test_helper.rs index 929271f..cd70113 100644 --- a/csaf-lib/src/csaf/test_helper.rs +++ b/csaf-lib/src/csaf/test_helper.rs @@ -1,7 +1,9 @@ -use std::collections::HashMap; -use crate::csaf::csaf2_1::loader::load_document; -use crate::csaf::csaf2_1::schema::CommonSecurityAdvisoryFramework; +use crate::csaf::csaf2_0::loader::load_document as load_document_20; +use crate::csaf::csaf2_0::schema::CommonSecurityAdvisoryFramework as Csaf20; +use crate::csaf::csaf2_1::loader::load_document as load_document_21; +use crate::csaf::csaf2_1::schema::CommonSecurityAdvisoryFramework as Csaf21; use crate::csaf::validation::{Test, ValidationError}; +use std::collections::HashMap; /// Generic test helper that loads all test files matching a specific test number pattern /// and runs positive and negative validations against a test function. @@ -14,22 +16,21 @@ use crate::csaf::validation::{Test, ValidationError}; /// /// This function assumes tests with filenames ending with numbers starting with "0" /// are negative tests, and those starting with "1" are positive tests. -pub fn run_csaf21_tests( - test_number: &str, - test_function: Test, - expected_errors: HashMap<&str, &ValidationError>, +fn run_csaf_tests( + pattern: &str, + file_prefix: &str, + document_loader: fn(&str) -> std::io::Result, + test_function: Test, + expected_errors: &HashMap<&str, &ValidationError>, ) { use glob::glob; - // Find all test files matching the pattern - let pattern = &format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-{}-*.json", test_number); - let file_prefix = &format!("oasis_csaf_tc-csaf_2_1-2024-6-1-{}-", test_number); - // Load and test each file for entry in glob(pattern).expect("Failed to parse glob pattern") { if let Ok(path) = entry { // Extract the file suffix (e.g., "01", "02", etc.) let file_name = path.file_name().unwrap().to_string_lossy(); + println!("{}", file_name); let test_num = file_name .strip_prefix(file_prefix) .unwrap() @@ -37,11 +38,11 @@ pub fn run_csaf21_tests( .unwrap(); // Load the document - let doc = load_document(path.to_string_lossy().as_ref()).unwrap(); + let doc = document_loader(path.to_string_lossy().as_ref()).unwrap(); // Check if this is expected to be a negative or positive test case if test_num.starts_with('0') { - // Negative test case - should fail with specific error + // Negative test case - should fail with a specific error let expected_error = expected_errors.get(test_num).expect( &format!("Missing expected error definition for negative test case {}", test_num) ); @@ -62,4 +63,28 @@ pub fn run_csaf21_tests( } } } -} \ No newline at end of file +} + +pub fn run_csaf20_tests( + test_number: &str, + test_function: Test, + expected_errors: &HashMap<&str, &ValidationError>, +) { + // Find all test files matching the pattern + let file_prefix = &format!("oasis_csaf_tc-csaf_2_0-2021-6-1-{}-", test_number); + let pattern = &format!("../csaf/csaf_2.0/test/validator/data/mandatory/{}*.json", file_prefix); + + run_csaf_tests(pattern, file_prefix, load_document_20, test_function, expected_errors); +} + +pub fn run_csaf21_tests( + test_number: &str, + test_function: Test, + expected_errors: &HashMap<&str, &ValidationError>, +) { + // Find all test files matching the pattern + let file_prefix = &format!("oasis_csaf_tc-csaf_2_1-2024-6-1-{}-", test_number); + let pattern = &format!("../csaf/csaf_2.1/test/validator/data/mandatory/{}*.json", file_prefix); + + run_csaf_tests(pattern, file_prefix, load_document_21, test_function, expected_errors); +} diff --git a/csaf-lib/src/csaf/validations/test_6_1_01.rs b/csaf-lib/src/csaf/validations/test_6_1_01.rs index 3541fed..8cc18bc 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_01.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_01.rs @@ -13,8 +13,8 @@ pub fn test_6_1_01_missing_definition_of_product_id( Ok(()) }); } + let references = gather_product_references(doc); - for (ref_id, ref_path) in references.iter() { if !definitions_set.contains(ref_id) { return Err(ValidationError { @@ -30,33 +30,20 @@ pub fn test_6_1_01_missing_definition_of_product_id( #[cfg(test)] mod tests { use std::collections::HashMap; - use crate::csaf::csaf2_0::loader::load_document as load_20; - use crate::csaf::test_helper::run_csaf21_tests; + use crate::csaf::test_helper::{run_csaf20_tests, run_csaf21_tests}; use crate::csaf::validation::ValidationError; use crate::csaf::validations::test_6_1_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() { - run_csaf21_tests("01", test_6_1_01_missing_definition_of_product_id, HashMap::from([ - ("01", &ValidationError { - message: EXPECTED_ERROR.to_string(), - instance_path: EXPECTED_INSTANCE_PATH.to_string(), - }) - ])); + fn test_6_1_01() { + let error01 = ValidationError { + message: "Missing definition of product_id: CSAFPID-9080700".to_string(), + instance_path: "/product_tree/product_groups/0/product_ids/0".to_string(), + }; + let errors = &HashMap::from([ + ("01", &error01) + ]); + run_csaf20_tests("01", test_6_1_01_missing_definition_of_product_id, &errors); + run_csaf21_tests("01", test_6_1_01_missing_definition_of_product_id, &errors); } } diff --git a/csaf-lib/src/csaf/validations/test_6_1_02.rs b/csaf-lib/src/csaf/validations/test_6_1_02.rs index 4f1b648..7d40e0e 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_02.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_02.rs @@ -27,34 +27,21 @@ pub fn test_6_1_02_multiple_definition_of_product_id( #[cfg(test)] mod tests { - use std::collections::HashMap; - use crate::csaf::csaf2_0::loader::load_document as load_20; - use crate::csaf::test_helper::run_csaf21_tests; + use crate::csaf::test_helper::{run_csaf20_tests, run_csaf21_tests}; use crate::csaf::validation::ValidationError; use crate::csaf::validations::test_6_1_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(), - }) - ) - } + use std::collections::HashMap; #[test] - fn test_test_6_1_02_csaf_2_1() { - run_csaf21_tests("02", test_6_1_02_multiple_definition_of_product_id, HashMap::from([ - ("01", &ValidationError { - message: EXPECTED_ERROR.to_string(), - instance_path: EXPECTED_INSTANCE_PATH.to_string(), - }) - ])); + fn test_test_6_1_02() { + let error01 = ValidationError { + message: "Duplicate definition for product ID CSAFPID-9080700".to_string(), + instance_path: "/product_tree/full_product_names/1/product_id".to_string(), + }; + let errors = HashMap::from([ + ("01", &error01) + ]); + run_csaf20_tests("02", test_6_1_02_multiple_definition_of_product_id, &errors); + run_csaf21_tests("02", test_6_1_02_multiple_definition_of_product_id, &errors); } } diff --git a/csaf-lib/src/csaf/validations/test_6_1_34.rs b/csaf-lib/src/csaf/validations/test_6_1_34.rs index 512b288..571bf30 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_34.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_34.rs @@ -33,7 +33,7 @@ mod tests { run_csaf21_tests( "34", test_6_1_34_branches_recursion_depth, - HashMap::from([ + &HashMap::from([ ("01", &ValidationError { message: "Branches recursion depth too big (> 30)".to_string(), instance_path: "/product_tree/branches/0/branches/0/branches/0/branches/0\ diff --git a/csaf-lib/src/csaf/validations/test_6_1_35.rs b/csaf-lib/src/csaf/validations/test_6_1_35.rs index 4e7e197..0f4c522 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_35.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_35.rs @@ -71,7 +71,7 @@ mod tests { run_csaf21_tests( "35", test_6_1_35_contradicting_remediations, - HashMap::from([ + &HashMap::from([ ("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(), 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 00038d9..a771e8b 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_36.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_36.rs @@ -91,7 +91,7 @@ mod tests { run_csaf21_tests( "36", test_6_1_36_status_group_contradicting_remediation_categories, - HashMap::from([ + &HashMap::from([ ("01", &ValidationError { message: "Product CSAFPID-9080700 is listed as not affected but has conflicting remediation category vendor_fix".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 640896b..ad1d9a0 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_37.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_37.rs @@ -120,7 +120,7 @@ mod tests { fn test_test_6_1_37() { run_csaf21_tests( "37", - test_6_1_37_date_and_time, HashMap::from([ + 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(), instance_path: "/document/tracking/initial_release_date".to_string(), diff --git a/csaf-lib/src/csaf/validations/test_6_1_38.rs b/csaf-lib/src/csaf/validations/test_6_1_38.rs index e59dc26..0001cc3 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_38.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_38.rs @@ -51,7 +51,7 @@ mod tests { instance_path: "/document/distribution/sharing_group/tlp/label".to_string(), }; - run_csaf21_tests("38", test_6_1_38_non_public_sharing_group_max_uuid, HashMap::from([ + run_csaf21_tests("38", test_6_1_38_non_public_sharing_group_max_uuid, &HashMap::from([ ("01", &expected_error), ("02", &expected_error), ("03", &expected_error), diff --git a/csaf-lib/src/csaf/validations/test_6_1_39.rs b/csaf-lib/src/csaf/validations/test_6_1_39.rs index 404927f..59be9f5 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_39.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_39.rs @@ -60,7 +60,7 @@ mod tests { instance_path: "/document/distribution/sharing_group/id".to_string(), }; - run_csaf21_tests("39", test_6_1_39_public_sharing_group_with_no_max_uuid, HashMap::from([ + run_csaf21_tests("39", test_6_1_39_public_sharing_group_with_no_max_uuid, &HashMap::from([ ("01", &expected_error), ("02", &expected_error), ])); diff --git a/csaf-lib/src/csaf/validations/test_6_1_40.rs b/csaf-lib/src/csaf/validations/test_6_1_40.rs index 3a0b88b..64e7638 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_40.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_40.rs @@ -64,7 +64,7 @@ mod tests { fn test_test_6_1_40() { run_csaf21_tests( "40", - test_6_1_40_invalid_sharing_group_name, HashMap::from([ + test_6_1_40_invalid_sharing_group_name, &HashMap::from([ ("01", &ValidationError { message: format!("Sharing group name \"{}\" is prohibited without max UUID.", NAME_PUBLIC), instance_path: "/document/distribution/sharing_group/name".to_string() diff --git a/csaf-lib/src/csaf/validations/test_6_1_41.rs b/csaf-lib/src/csaf/validations/test_6_1_41.rs index efe6004..f659f15 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_41.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_41.rs @@ -67,7 +67,7 @@ mod tests { fn test_test_6_1_41() { run_csaf21_tests( "41", - test_6_1_41_missing_sharing_group_name, HashMap::from([ + test_6_1_41_missing_sharing_group_name, &HashMap::from([ ("01", &ValidationError { message: format!("Max UUID requires sharing group name to be \"{}\".", NAME_PUBLIC), instance_path: "/document/distribution/sharing_group/name".to_string() diff --git a/csaf-lib/src/csaf/validations/test_6_1_42.rs b/csaf-lib/src/csaf/validations/test_6_1_42.rs index c931467..41b47c8 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_42.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_42.rs @@ -72,7 +72,7 @@ mod tests { fn test_test_6_1_42() { run_csaf21_tests( "42", - test_6_1_42_purl_consistency, HashMap::from([ + test_6_1_42_purl_consistency, &HashMap::from([ ("01", &ValidationError { message: ERROR_MESSAGE.to_string(), instance_path: "/product_tree/full_product_names/0/product_identification_helper/purls/1".to_string(), diff --git a/csaf-lib/src/csaf/validations/test_6_1_43.rs b/csaf-lib/src/csaf/validations/test_6_1_43.rs index 6d8c5bb..ea88846 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_43.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_43.rs @@ -41,7 +41,7 @@ mod tests { run_csaf21_tests( "43", - test_6_1_43_multiple_stars_in_model_number, HashMap::from([ + test_6_1_43_multiple_stars_in_model_number, &HashMap::from([ ("01", &expected_error), ("02", &expected_error), ]) diff --git a/csaf-lib/src/csaf/validations/test_6_1_44.rs b/csaf-lib/src/csaf/validations/test_6_1_44.rs index d0a35f1..7e74579 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_44.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_44.rs @@ -41,7 +41,7 @@ mod tests { run_csaf21_tests( "44", - test_6_1_44_multiple_stars_in_serial_number, HashMap::from([ + test_6_1_44_multiple_stars_in_serial_number, &HashMap::from([ ("01", &expected_error), ("02", &expected_error), ]) diff --git a/csaf-lib/src/csaf/validations/test_6_1_45.rs b/csaf-lib/src/csaf/validations/test_6_1_45.rs index ce0edb9..8cee77f 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_45.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_45.rs @@ -95,7 +95,7 @@ mod tests { run_csaf21_tests( "45", - test_6_1_45_inconsistent_disclosure_date, HashMap::from([ + test_6_1_45_inconsistent_disclosure_date, &HashMap::from([ ("01", &expected_error), ("02", &expected_error), ("03", &expected_error), diff --git a/csaf-lib/src/csaf/validations/test_6_1_46.rs b/csaf-lib/src/csaf/validations/test_6_1_46.rs index 5e4cb75..fc94eaf 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_46.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_46.rs @@ -32,7 +32,7 @@ mod tests { fn test_test_6_1_46() { run_csaf21_tests( "46", - test_6_1_46_invalid_ssvc, HashMap::from([ + test_6_1_46_invalid_ssvc, &HashMap::from([ ("01", &ValidationError { message: "Invalid SSVC object: missing field `selections`".to_string(), instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1".to_string(), diff --git a/csaf-lib/src/csaf/validations/test_6_1_47.rs b/csaf-lib/src/csaf/validations/test_6_1_47.rs index 89df8ae..c035065 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_47.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_47.rs @@ -77,7 +77,7 @@ mod tests { run_csaf21_tests( "47", test_6_1_47_inconsistent_ssvc_id, - HashMap::from([ + &HashMap::from([ ("01", &ValidationError { message: "The SSVC ID 'CVE-1900-0002' does not match the document ID, the CVE ID or any ID in the IDs array of the vulnerability".to_string(), instance_path: instance_path.clone(), diff --git a/csaf-lib/src/csaf/validations/test_6_1_48.rs b/csaf-lib/src/csaf/validations/test_6_1_48.rs index 225f6a9..3573a80 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_48.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_48.rs @@ -30,7 +30,7 @@ pub fn test_6_1_48_ssvc_decision_points( Some(_) => { // Get value indices of decision point let reference_indices = DP_VAL_LOOKUP.get(&dp_key).unwrap(); - // Index of last seen value + // Index of last-seen value let mut last_index: i32 = -1; // Check if all values exist and are correctly ordered for (i_val, value) in selection.values.iter().map(|v| v.deref()).enumerate() { @@ -107,17 +107,17 @@ mod tests { run_csaf21_tests( "48", test_6_1_48_ssvc_decision_points, - HashMap::from([ + &HashMap::from([ ("01", &ValidationError { message: "The SSVC decision point 'ssvc::Mission Impact' (version 1.0.0) doesn't have the value 'Degraded'".to_string(), instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1/selections/0/values/1".to_string(), }), ("02", &ValidationError { - message: "Unknown SSVC decision point 'ssvc::Safety Impacts' with version '2.0.0'".to_string(), + message: "Unknown SSVC decision point 'ssvc::Safety Impacts' with version '1.0.0'".to_string(), instance_path: instance_path.clone(), }), ("03", &ValidationError { - message: "The values for SSVC decision point 'ssvc::Safety Impact' (version 2.0.0) are not in correct order".to_string(), + message: "The SSVC decision point 'ssvc::Safety Impact' (version 1.0.0) doesn't have the value 'Critical'".to_string(), instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1/selections/0/values/1".to_string(), }), ("04", &ValidationError { diff --git a/csaf-lib/src/csaf/validations/test_6_1_49.rs b/csaf-lib/src/csaf/validations/test_6_1_49.rs index ebd6f22..2722c1d 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_49.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_49.rs @@ -6,7 +6,7 @@ use chrono::{DateTime, FixedOffset}; /// 6.1.49 Inconsistent SSVC Timestamp /// /// For each vulnerability, it is tested that the SSVC `timestamp` is earlier or equal to the `date` -/// of the newest item of the `revision_history` if the document status is `final` or `interim`. +/// of the newest item in the `revision_history` if the document status is `final` or `interim`. pub fn test_6_1_49_inconsistent_ssvc_timestamp( doc: &impl CsafTrait, ) -> Result<(), ValidationError> { @@ -14,7 +14,7 @@ pub fn test_6_1_49_inconsistent_ssvc_timestamp( let tracking = document.get_tracking(); let status = tracking.get_status(); - // Check if document status is "final" or "interim" + // Check if the document status is "final" or "interim" if status != DocumentStatus::Final && status != DocumentStatus::Interim { return Ok(()); } @@ -92,7 +92,7 @@ mod tests { run_csaf21_tests( "49", test_6_1_49_inconsistent_ssvc_timestamp, - HashMap::from([ + &HashMap::from([ ("01", &ValidationError { message: "SSVC timestamp (2024-07-13T10:00:00+00:00) for vulnerability at index 0 is later than the newest revision date (2024-01-24T10:00:00+00:00)".to_string(), instance_path: instance_path.clone(), From cc4460eb9b81a012f3dbc52e95fe1548ebc11425 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Mon, 19 May 2025 16:15:49 +0200 Subject: [PATCH 2/4] Add validation for circular product ID definitions (6.1.03) Introduce a new module `test_6_1_03` to validate circular product ID references in CSAF documents. This includes a `find_cycle` function to detect cycles and corresponding validation logic with test cases ensuring correctness. --- csaf-lib/src/csaf/validations/mod.rs | 1 + csaf-lib/src/csaf/validations/test_6_1_03.rs | 172 +++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 csaf-lib/src/csaf/validations/test_6_1_03.rs diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index d295fcf..c7365ad 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -16,3 +16,4 @@ pub mod test_6_1_46; pub mod test_6_1_47; pub mod test_6_1_48; pub mod test_6_1_49; +pub mod test_6_1_03; diff --git a/csaf-lib/src/csaf/validations/test_6_1_03.rs b/csaf-lib/src/csaf/validations/test_6_1_03.rs new file mode 100644 index 0000000..3e83a62 --- /dev/null +++ b/csaf-lib/src/csaf/validations/test_6_1_03.rs @@ -0,0 +1,172 @@ +use crate::csaf::getter_traits::{CsafTrait, ProductTrait, ProductTreeTrait, RelationshipTrait}; +use crate::csaf::validation::ValidationError; +use std::collections::HashMap; + +/// Find the first cycle in the given `relation_map`, if any. +/// +/// # Returns +/// - Product ID where the cycle was first detected +/// - String representation of the whole cycle detected +/// - Index of the CSAF relation containing the product ID where the cycle was first detected +pub fn find_cycle<'a>( + relation_map: &'a HashMap>, + product_id: &'a str, + visited: &mut Vec<&'a str>, +) -> Option<(String, Vec, usize)> { + if visited.contains(&product_id) { + return Some((product_id.to_string(), vec!(product_id.to_string()), 0)); + } else { + visited.push(product_id); + } + if let Some(next_vec) = relation_map.get(product_id) { + for (next, r_i) in next_vec { + match find_cycle(relation_map, next, visited) { + None => {} + Some((cycle_end, mut cycle, r_i_res)) => { + if cycle.len() == 1 || cycle_end != *cycle.last().unwrap() { + // Back-trace the cycle to the first node + cycle.push(product_id.to_string()); + if cycle_end == product_id { + // Reverse the cycle when it is complete + cycle.reverse(); + return Some((cycle_end, cycle, *r_i)); + } + } + return Some((cycle_end, cycle, r_i_res)); + } + } + } + } + visited.pop(); + None +} + +pub fn test_6_1_03_circular_definition_of_product_id( + doc: &impl CsafTrait, +) -> Result<(), ValidationError> { + if let Some(tree) = doc.get_product_tree().as_ref() { + let mut relation_map = HashMap::>::new(); + + for (i_r, r) in tree.get_relationships().iter().enumerate() { + let rel_prod_id = r.get_full_product_name().get_product_id(); + if r.get_product_reference() == rel_prod_id { + return Err(ValidationError { + message: "Relationship references itself via product_reference".to_string(), + instance_path: format!("/product_tree/relationships/{}/product_reference", i_r), + }) + } else if r.get_relates_to_product_reference() == rel_prod_id { + return Err(ValidationError { + message: "Relationship references itself via relates_to_product_reference".to_string(), + instance_path: format!("/product_tree/relationships/{}/relates_to_product_reference", i_r), + }) + } else { + match relation_map.get_mut(r.get_product_reference()) { + Some(v) => { + v.insert(r.get_relates_to_product_reference().to_owned(), i_r); + }, + None => { + relation_map.insert( + r.get_product_reference().to_owned(), + HashMap::from([(r.get_relates_to_product_reference().to_owned(), i_r)]) + ); + } + } + } + } + + // Perform cycle check + for product_id in relation_map.keys() { + let mut vec: Vec<&str> = vec!(); + if let Some((_, cycle, relation_index)) = find_cycle(&relation_map, product_id, &mut vec) { + return Err(ValidationError { + message: format!("Found product relationship cycle: {}", cycle.join(" -> ")), + instance_path: format!("/product_tree/relationships/{}", relation_index), + }) + } + } + } + + 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_03::test_6_1_03_circular_definition_of_product_id; + use std::collections::HashMap; + + #[test] + fn test_test_6_1_03() { + let error01 = ValidationError { + message: "Relationship references itself via relates_to_product_reference".to_string(), + instance_path: "/product_tree/relationships/0/relates_to_product_reference".to_string(), + }; + let errors = HashMap::from([ + ("01", &error01) + ]); + run_csaf20_tests("03", test_6_1_03_circular_definition_of_product_id, &errors); + run_csaf21_tests("03", test_6_1_03_circular_definition_of_product_id, &errors); + } + + #[test] + fn test_find_cycle() { + // Create a relation map with a non-trivial cycle: B -> C -> D -> B + let mut relation_map = HashMap::new(); + + relation_map.insert( + "A".to_string(), + HashMap::from([("B".to_string(), 0)]) + ); + relation_map.insert( + "B".to_string(), + HashMap::from([("C".to_string(), 1), ("E".to_string(), 2)]) + ); + relation_map.insert( + "C".to_string(), + HashMap::from([("D".to_string(), 3), ("F".to_string(), 4)]) + ); + relation_map.insert( + "D".to_string(), + HashMap::from([("B".to_string(), 5)]) + ); + + // Also add some nodes that aren't part of the cycle + relation_map.insert( + "E".to_string(), + HashMap::from([("F".to_string(), 6)]) + ); + relation_map.insert( + "F".to_string(), + HashMap::from([("G".to_string(), 7)]) + ); + + // Test cycle detection starting from the first node + let mut visited = Vec::new(); + let result = super::find_cycle(&relation_map, "A", &mut visited); + assert!(result.is_some()); + let (cycle_end, cycle, relation_index) = result.unwrap(); + assert_eq!(cycle_end, "B"); + assert_eq!(cycle, vec!("B", "C", "D", "B")); + assert_eq!(relation_index, 1); + + // Test starting from a node that's part of the cycle + let mut visited = Vec::new(); + let result = super::find_cycle(&relation_map, "C", &mut visited); + assert!(result.is_some()); + let (cycle_end, cycle, relation_index) = result.unwrap(); + assert_eq!(cycle_end, "C"); + assert_eq!(cycle, vec!("C", "D", "B", "C")); + assert_eq!(relation_index, 3); + + // Test starting from a node that's not part of any cycle + let mut visited = Vec::new(); + let result = super::find_cycle(&relation_map, "E", &mut visited); + assert!(result.is_none()); + + // Test with empty visited Set and starting from a node not in the map + let mut visited = Vec::new(); + let result = super::find_cycle(&relation_map, "Z", &mut visited); + assert!(result.is_none()); + } +} From 6f3041558ca0f6b7e03bf5460609458f93681669 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Tue, 20 May 2025 12:53:49 +0200 Subject: [PATCH 3/4] Fixed tests for test 6.1.48 Adjust validation error messages for SSVC decision points to reflect the updated version 2.0.0. Includes changes to error messages regarding unknown decision points and the order of values. These updates align with the new specification requirements. --- csaf | 2 +- csaf-lib/src/csaf/validations/test_6_1_48.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/csaf b/csaf index 500de13..1726fcf 160000 --- a/csaf +++ b/csaf @@ -1 +1 @@ -Subproject commit 500de13452015136756e64ae03f84bdc9fd6405b +Subproject commit 1726fcf10d6e444e6e65a696ac9198c981858d23 diff --git a/csaf-lib/src/csaf/validations/test_6_1_48.rs b/csaf-lib/src/csaf/validations/test_6_1_48.rs index 3573a80..e85315a 100644 --- a/csaf-lib/src/csaf/validations/test_6_1_48.rs +++ b/csaf-lib/src/csaf/validations/test_6_1_48.rs @@ -113,11 +113,11 @@ mod tests { instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1/selections/0/values/1".to_string(), }), ("02", &ValidationError { - message: "Unknown SSVC decision point 'ssvc::Safety Impacts' with version '1.0.0'".to_string(), + message: "Unknown SSVC decision point 'ssvc::Safety Impacts' with version '2.0.0'".to_string(), instance_path: instance_path.clone(), }), ("03", &ValidationError { - message: "The SSVC decision point 'ssvc::Safety Impact' (version 1.0.0) doesn't have the value 'Critical'".to_string(), + message: "The values for SSVC decision point 'ssvc::Safety Impact' (version 2.0.0) are not in correct order".to_string(), instance_path: "/vulnerabilities/0/metrics/0/content/ssvc_v1/selections/0/values/1".to_string(), }), ("04", &ValidationError { From 9ea8e371bfd544389b81d83576aef3897b7ad739 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Tue, 20 May 2025 13:01:26 +0200 Subject: [PATCH 4/4] Fixed order --- csaf-lib/src/csaf/validations/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/csaf-lib/src/csaf/validations/mod.rs b/csaf-lib/src/csaf/validations/mod.rs index c7365ad..3b16988 100644 --- a/csaf-lib/src/csaf/validations/mod.rs +++ b/csaf-lib/src/csaf/validations/mod.rs @@ -1,5 +1,7 @@ pub mod test_6_1_01; pub mod test_6_1_02; +pub mod test_6_1_03; + pub mod test_6_1_34; pub mod test_6_1_35; pub mod test_6_1_36; @@ -16,4 +18,3 @@ pub mod test_6_1_46; pub mod test_6_1_47; pub mod test_6_1_48; pub mod test_6_1_49; -pub mod test_6_1_03;