Skip to content

Add validation test 6.1.36 for remediation category conflicts #28

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 1 commit into from
Mar 24, 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
4 changes: 3 additions & 1 deletion csaf-lib/src/csaf/csaf2_1/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use crate::csaf::validations::test_6_1_01::test_6_1_01_missing_definition_of_pro
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 crate::csaf::validations::test_6_1_36::test_6_1_36_status_group_contradicting_remediation_categories;
use std::collections::HashMap;

impl Validatable<CommonSecurityAdvisoryFramework> for CommonSecurityAdvisoryFramework {
fn presets(&self) -> HashMap<ValidationPreset, Vec<&str>> {
let basic_tests = Vec::from(["6.1.1", "6.1.2", "6.1.34", "6.1.35"]);
let basic_tests = Vec::from(["6.1.1", "6.1.2", "6.1.34", "6.1.35", "6.1.36"]);
// More tests may be added in extend() here later
let extended_tests: Vec<&str> = basic_tests.clone();
// extended_tests.extend(["foo"].iter());
Expand All @@ -28,6 +29,7 @@ impl Validatable<CommonSecurityAdvisoryFramework> for CommonSecurityAdvisoryFram
("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),
("6.1.36", test_6_1_36_status_group_contradicting_remediation_categories as CsafTest),
])
}

Expand Down
55 changes: 54 additions & 1 deletion csaf-lib/src/csaf/getter_traits.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::BTreeSet;
use std::collections::{BTreeSet, HashSet};
use crate::csaf::csaf2_1::schema::CategoryOfTheRemediation;
use crate::csaf::helpers::resolve_product_groups;

Expand Down Expand Up @@ -120,6 +120,59 @@ pub trait ProductStatusTrait {

/// Returns a reference to the list of product IDs currently under investigation.
fn get_under_investigation(&self) -> Option<Vec<&String>>;

/// Combines all affected product IDs into a `HashSet`.
///
/// This method aggregates product IDs from these lists:
/// - First affected product IDs
/// - Last affected product IDs
/// - Known affected product IDs
///
/// # Returns
///
/// A `HashSet` containing all aggregated product IDs. If none of these lists are
/// populated, the returned `HashSet` will be empty.
fn get_all_affected(&self) -> HashSet<&String> {
let mut result = HashSet::new();

if let Some(first_affected) = self.get_first_affected() {
result.extend(first_affected);
}

if let Some(last_affected) = self.get_last_affected() {
result.extend(last_affected);
}

if let Some(known_affected) = self.get_known_affected() {
result.extend(known_affected);
}

result
}

/// Combines all fixed product IDs into a `HashSet`.
///
/// This method aggregates product IDs from these lists:
/// - First fixed product IDs
/// - Fixed product IDs
///
/// # Returns
///
/// A `HashSet` containing all aggregated product IDs. If none of these lists are
/// populated, the returned `HashSet` will be empty.
fn get_all_fixed(&self) -> HashSet<&String> {
let mut result = HashSet::new();

if let Some(first_fixed) = self.get_first_fixed() {
result.extend(first_fixed);
}

if let Some(fixed) = self.get_fixed() {
result.extend(fixed);
}

result
}
}

/// Trait representing an abstract metric in a CSAF document.
Expand Down
3 changes: 2 additions & 1 deletion csaf-lib/src/csaf/validations/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod test_6_1_01;
pub mod test_6_1_02;
pub mod test_6_1_34;
pub mod test_6_1_35;
pub mod test_6_1_35;
pub mod test_6_1_36;
24 changes: 13 additions & 11 deletions csaf-lib/src/csaf/validations/test_6_1_35.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ use crate::csaf::getter_traits::{CsafTrait, RemediationTrait, VulnerabilityTrait
use std::collections::BTreeMap;
use crate::csaf::validation::ValidationError;

static MUT_EX_MEASURES: &[CategoryOfTheRemediation] = &[
/// Totally exclusive categories that cannot be combined with any other category.
static EX_STATES: &[CategoryOfTheRemediation] = &[
CategoryOfTheRemediation::NoneAvailable,
CategoryOfTheRemediation::Workaround,
CategoryOfTheRemediation::Mitigation,
CategoryOfTheRemediation::OptionalPatch,
];

static MUT_EX_FIX_STATES: &[CategoryOfTheRemediation] = &[
CategoryOfTheRemediation::NoneAvailable,
/// Mutually exclusive states that cannot apply at the same time.
static MUT_EX_STATES: &[CategoryOfTheRemediation] = &[
CategoryOfTheRemediation::NoFixPlanned,
CategoryOfTheRemediation::FixPlanned,
CategoryOfTheRemediation::OptionalPatch,
CategoryOfTheRemediation::VendorFix,
];

Expand All @@ -32,11 +31,13 @@ pub fn test_6_1_35_contradicting_remediations(
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)
}) {
// Checks if current category is exclusive and a non-equal previous category was found.
if EX_STATES.contains(&cat) && exist_cat_set.first().is_some_and(|e_cat| e_cat != &cat)
// Checks if the (only) previous category is exclusive.
|| exist_cat_set.first().is_some_and(|e_cat| EX_STATES.contains(e_cat))
// Checks if the current category conflicts with any other in the group of mutually exclusive ones.
|| MUT_EX_STATES.contains(&cat) && exist_cat_set.iter().any(|e_cat| MUT_EX_STATES.contains(e_cat))
{
return Err(ValidationError {
message: format!(
"Product {} has contradicting remediations: {} and {}",
Expand Down Expand Up @@ -92,6 +93,7 @@ mod tests {
}),
].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)
Expand Down
118 changes: 118 additions & 0 deletions csaf-lib/src/csaf/validations/test_6_1_36.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use std::collections::HashSet;
use crate::csaf::csaf2_1::schema::CategoryOfTheRemediation;
use crate::csaf::getter_traits::{CsafTrait, ProductStatusTrait, RemediationTrait, VulnerabilityTrait};
use crate::csaf::validation::ValidationError;

/// Remediation categories that conflict with the product status "not affected".
const NOT_AFFECTED_CONFLICTS: &[CategoryOfTheRemediation] = &[
CategoryOfTheRemediation::Workaround,
CategoryOfTheRemediation::Mitigation,
CategoryOfTheRemediation::VendorFix,
CategoryOfTheRemediation::NoneAvailable,
];

/// Remediation categories that conflict with "fixed" product statuses.
const FIXED_CONFLICTS: &[CategoryOfTheRemediation] = &[
CategoryOfTheRemediation::NoneAvailable,
CategoryOfTheRemediation::FixPlanned,
CategoryOfTheRemediation::NoFixPlanned,
CategoryOfTheRemediation::VendorFix,
CategoryOfTheRemediation::Mitigation,
CategoryOfTheRemediation::Workaround,
];

pub fn test_6_1_36_status_group_contradicting_remediation_categories(
doc: &impl CsafTrait,
) -> Result<(), ValidationError> {
for (v_i, v) in doc.get_vulnerabilities().iter().enumerate() {
if let Some(product_status) = v.get_product_status() {
// Collect Product IDs that may cause conflicts
let affected_products = product_status.get_all_affected();
let not_affected_products = match product_status.get_known_not_affected() {
Some(products) => products.into_iter().collect(),
None => HashSet::new(),
};
let fixed_products = product_status.get_all_fixed();
// Iterate over remediations
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 {
if affected_products.contains(&p) && cat == CategoryOfTheRemediation::OptionalPatch {
return Err(ValidationError {
message: format!(
"Product {} is listed as affected but has conflicting remediation category {}",
p,
cat
),
instance_path: format!("/vulnerabilities/{}/remediations/{}", v_i, r_i),
});
}
if not_affected_products.contains(&p) && NOT_AFFECTED_CONFLICTS.contains(&cat) {
return Err(ValidationError {
message: format!(
"Product {} is listed as not affected but has conflicting remediation category {}",
p,
cat
),
instance_path: format!("/vulnerabilities/{}/remediations/{}", v_i, r_i),
});
}
if fixed_products.contains(&p) && FIXED_CONFLICTS.contains(&cat) {
return Err(ValidationError {
message: format!(
"Product {} is listed as fixed but has conflicting remediation category {}",
p,
cat
),
instance_path: format!("/vulnerabilities/{}/remediations/{}", v_i, r_i),
});
}
}
}
}
}
}
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_36::test_6_1_36_status_group_contradicting_remediation_categories;

#[test]
fn test_test_6_1_36() {
for x in ["11", "12", "13"].iter() {
let doc = load_document(format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-36-{}.json", x).as_str()).unwrap();
assert_eq!(
Ok(()),
test_6_1_36_status_group_contradicting_remediation_categories(&doc)
)
}
for (x, err) in [
("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()
}),
("02", ValidationError {
message: "Product CSAFPID-9080703 is listed as fixed but has conflicting remediation category none_available".to_string(),
instance_path: "/vulnerabilities/0/remediations/0".to_string()
}),
("03", ValidationError {
message: "Product CSAFPID-9080700 is listed as affected but has conflicting remediation category optional_patch".to_string(),
instance_path: "/vulnerabilities/0/remediations/0".to_string(),
}),
].iter() {
let doc = load_document(format!("../csaf/csaf_2.1/test/validator/data/mandatory/oasis_csaf_tc-csaf_2_1-2024-6-1-36-{}.json", x).as_str()).unwrap();
assert_eq!(
Err(err.clone()),
test_6_1_36_status_group_contradicting_remediation_categories(&doc)
)
}
}
}