csaf-rs 0.5.1

A parser for the CSAF standard written in Rust
use crate::csaf_traits::{CsafTrait, ProductStatusGroup, ProductStatusGroupMap, RemediationTrait, VulnerabilityTrait};
use crate::schema::csaf2_1::schema::CategoryOfTheRemediation;
use crate::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,
];

fn create_affected_conflict_error(
    product_id: &str,
    category: &CategoryOfTheRemediation,
    v_i: usize,
    r_i: usize,
) -> ValidationError {
    ValidationError {
        message: format!(
            "Product {product_id} is listed as affected but has conflicting remediation category {category}"
        ),
        instance_path: format!("/vulnerabilities/{v_i}/remediations/{r_i}"),
    }
}

fn create_not_affected_conflict_error(
    product_id: &str,
    category: &CategoryOfTheRemediation,
    v_i: usize,
    r_i: usize,
) -> ValidationError {
    ValidationError {
        message: format!(
            "Product {product_id} is listed as not affected but has conflicting remediation category {category}"
        ),
        instance_path: format!("/vulnerabilities/{v_i}/remediations/{r_i}"),
    }
}

fn create_fixed_conflict_error(
    product_id: &str,
    category: &CategoryOfTheRemediation,
    v_i: usize,
    r_i: usize,
) -> ValidationError {
    ValidationError {
        message: format!("Product {product_id} is listed as fixed but has conflicting remediation category {category}"),
        instance_path: format!("/vulnerabilities/{v_i}/remediations/{r_i}"),
    }
}

pub fn test_6_1_36_status_group_contradicting_remediation_categories(
    doc: &impl CsafTrait,
) -> Result<(), Vec<ValidationError>> {
    for (v_i, v) in doc.get_vulnerabilities().iter().enumerate() {
        if let Some(product_status) = v.get_product_status() {
            let status_map = ProductStatusGroupMap::from(product_status);
            // 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 status_map.contains(&ProductStatusGroup::Affected, &p)
                            && cat == CategoryOfTheRemediation::OptionalPatch
                        {
                            return Err(vec![create_affected_conflict_error(&p, &cat, v_i, r_i)]);
                        }
                        if status_map.contains(&ProductStatusGroup::NotAffected, &p)
                            && NOT_AFFECTED_CONFLICTS.contains(&cat)
                        {
                            return Err(vec![create_not_affected_conflict_error(&p, &cat, v_i, r_i)]);
                        }
                        if status_map.contains(&ProductStatusGroup::Fixed, &p) && FIXED_CONFLICTS.contains(&cat) {
                            return Err(vec![create_fixed_conflict_error(&p, &cat, v_i, r_i)]);
                        }
                    }
                }
            }
        }
    }
    Ok(())
}

crate::test_validation::impl_validator!(
    csaf2_1,
    ValidatorForTest6_1_36,
    test_6_1_36_status_group_contradicting_remediation_categories
);

#[cfg(test)]
mod tests {
    use super::*;
    use crate::csaf2_1::testcases::TESTS_2_1;

    #[test]
    fn test_test_6_1_36() {
        // Only CSAF 2.1 has this test with 8 test cases (4 error cases, 4 success cases)
        TESTS_2_1.test_6_1_36.expect(
            Err(vec![create_not_affected_conflict_error(
                "CSAFPID-9080700",
                &CategoryOfTheRemediation::VendorFix,
                0,
                0,
            )]),
            Err(vec![create_fixed_conflict_error(
                "CSAFPID-9080703",
                &CategoryOfTheRemediation::NoneAvailable,
                0,
                0,
            )]),
            Err(vec![create_affected_conflict_error(
                "CSAFPID-9080700",
                &CategoryOfTheRemediation::OptionalPatch,
                0,
                0,
            )]),
            Err(vec![create_fixed_conflict_error(
                "CSAFPID-9080700",
                &CategoryOfTheRemediation::NoFixPlanned,
                0,
                0,
            )]),
            Ok(()),
            Ok(()),
            Ok(()),
            Ok(()),
        );
    }
}