Skip to content

Test helper improvements & Test 6.1.3 #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion csaf
Submodule csaf updated 159 files
53 changes: 39 additions & 14 deletions csaf-lib/src/csaf/test_helper.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -14,34 +16,33 @@ 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<CommonSecurityAdvisoryFramework>,
expected_errors: HashMap<&str, &ValidationError>,
fn run_csaf_tests<CsafType>(
pattern: &str,
file_prefix: &str,
document_loader: fn(&str) -> std::io::Result<CsafType>,
test_function: Test<CsafType>,
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()
.strip_suffix(".json")
.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)
);
Expand All @@ -62,4 +63,28 @@ pub fn run_csaf21_tests(
}
}
}
}
}

pub fn run_csaf20_tests(
test_number: &str,
test_function: Test<Csaf20>,
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<Csaf21>,
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);
}
2 changes: 2 additions & 0 deletions csaf-lib/src/csaf/validations/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
37 changes: 12 additions & 25 deletions csaf-lib/src/csaf/validations/test_6_1_01.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
}
37 changes: 12 additions & 25 deletions csaf-lib/src/csaf/validations/test_6_1_02.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
172 changes: 172 additions & 0 deletions csaf-lib/src/csaf/validations/test_6_1_03.rs
Original file line number Diff line number Diff line change
@@ -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<String, HashMap<String, usize>>,
product_id: &'a str,
visited: &mut Vec<&'a str>,
) -> Option<(String, Vec<String>, 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::<String, HashMap<String, usize>>::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());
}
}
2 changes: 1 addition & 1 deletion csaf-lib/src/csaf/validations/test_6_1_34.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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\
Expand Down
2 changes: 1 addition & 1 deletion csaf-lib/src/csaf/validations/test_6_1_35.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Loading