Skip to content

Commit 8e07a62

Browse files
authored
Add validation test 6.1.36 for remediation category conflicts (#28)
Introduces a new validation test (6.1.36) to ensure product status groups do not conflict with associated remediation categories. Refactors parts of test 6.1.35 to improve exclusivity checks and avoid redundancy. Adds utility methods to aggregate product IDs across product status categories.
1 parent 3735c1a commit 8e07a62

File tree

5 files changed

+190
-14
lines changed

5 files changed

+190
-14
lines changed

csaf-lib/src/csaf/csaf2_1/validation.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ use crate::csaf::validations::test_6_1_01::test_6_1_01_missing_definition_of_pro
44
use crate::csaf::validations::test_6_1_02::test_6_1_02_multiple_definition_of_product_id;
55
use crate::csaf::validations::test_6_1_34::test_6_1_34_branches_recursion_depth;
66
use crate::csaf::validations::test_6_1_35::test_6_1_35_contradicting_remediations;
7+
use crate::csaf::validations::test_6_1_36::test_6_1_36_status_group_contradicting_remediation_categories;
78
use std::collections::HashMap;
89

910
impl Validatable<CommonSecurityAdvisoryFramework> for CommonSecurityAdvisoryFramework {
1011
fn presets(&self) -> HashMap<ValidationPreset, Vec<&str>> {
11-
let basic_tests = Vec::from(["6.1.1", "6.1.2", "6.1.34", "6.1.35"]);
12+
let basic_tests = Vec::from(["6.1.1", "6.1.2", "6.1.34", "6.1.35", "6.1.36"]);
1213
// More tests may be added in extend() here later
1314
let extended_tests: Vec<&str> = basic_tests.clone();
1415
// extended_tests.extend(["foo"].iter());
@@ -28,6 +29,7 @@ impl Validatable<CommonSecurityAdvisoryFramework> for CommonSecurityAdvisoryFram
2829
("6.1.2", test_6_1_02_multiple_definition_of_product_id as CsafTest),
2930
("6.1.34", test_6_1_34_branches_recursion_depth as CsafTest),
3031
("6.1.35", test_6_1_35_contradicting_remediations as CsafTest),
32+
("6.1.36", test_6_1_36_status_group_contradicting_remediation_categories as CsafTest),
3133
])
3234
}
3335

csaf-lib/src/csaf/getter_traits.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::BTreeSet;
1+
use std::collections::{BTreeSet, HashSet};
22
use crate::csaf::csaf2_1::schema::CategoryOfTheRemediation;
33
use crate::csaf::helpers::resolve_product_groups;
44

@@ -120,6 +120,59 @@ pub trait ProductStatusTrait {
120120

121121
/// Returns a reference to the list of product IDs currently under investigation.
122122
fn get_under_investigation(&self) -> Option<Vec<&String>>;
123+
124+
/// Combines all affected product IDs into a `HashSet`.
125+
///
126+
/// This method aggregates product IDs from these lists:
127+
/// - First affected product IDs
128+
/// - Last affected product IDs
129+
/// - Known affected product IDs
130+
///
131+
/// # Returns
132+
///
133+
/// A `HashSet` containing all aggregated product IDs. If none of these lists are
134+
/// populated, the returned `HashSet` will be empty.
135+
fn get_all_affected(&self) -> HashSet<&String> {
136+
let mut result = HashSet::new();
137+
138+
if let Some(first_affected) = self.get_first_affected() {
139+
result.extend(first_affected);
140+
}
141+
142+
if let Some(last_affected) = self.get_last_affected() {
143+
result.extend(last_affected);
144+
}
145+
146+
if let Some(known_affected) = self.get_known_affected() {
147+
result.extend(known_affected);
148+
}
149+
150+
result
151+
}
152+
153+
/// Combines all fixed product IDs into a `HashSet`.
154+
///
155+
/// This method aggregates product IDs from these lists:
156+
/// - First fixed product IDs
157+
/// - Fixed product IDs
158+
///
159+
/// # Returns
160+
///
161+
/// A `HashSet` containing all aggregated product IDs. If none of these lists are
162+
/// populated, the returned `HashSet` will be empty.
163+
fn get_all_fixed(&self) -> HashSet<&String> {
164+
let mut result = HashSet::new();
165+
166+
if let Some(first_fixed) = self.get_first_fixed() {
167+
result.extend(first_fixed);
168+
}
169+
170+
if let Some(fixed) = self.get_fixed() {
171+
result.extend(fixed);
172+
}
173+
174+
result
175+
}
123176
}
124177

125178
/// Trait representing an abstract metric in a CSAF document.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod test_6_1_01;
22
pub mod test_6_1_02;
33
pub mod test_6_1_34;
4-
pub mod test_6_1_35;
4+
pub mod test_6_1_35;
5+
pub mod test_6_1_36;

csaf-lib/src/csaf/validations/test_6_1_35.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@ use crate::csaf::getter_traits::{CsafTrait, RemediationTrait, VulnerabilityTrait
33
use std::collections::BTreeMap;
44
use crate::csaf::validation::ValidationError;
55

6-
static MUT_EX_MEASURES: &[CategoryOfTheRemediation] = &[
6+
/// Totally exclusive categories that cannot be combined with any other category.
7+
static EX_STATES: &[CategoryOfTheRemediation] = &[
78
CategoryOfTheRemediation::NoneAvailable,
8-
CategoryOfTheRemediation::Workaround,
9-
CategoryOfTheRemediation::Mitigation,
9+
CategoryOfTheRemediation::OptionalPatch,
1010
];
1111

12-
static MUT_EX_FIX_STATES: &[CategoryOfTheRemediation] = &[
13-
CategoryOfTheRemediation::NoneAvailable,
12+
/// Mutually exclusive states that cannot apply at the same time.
13+
static MUT_EX_STATES: &[CategoryOfTheRemediation] = &[
1414
CategoryOfTheRemediation::NoFixPlanned,
1515
CategoryOfTheRemediation::FixPlanned,
16-
CategoryOfTheRemediation::OptionalPatch,
1716
CategoryOfTheRemediation::VendorFix,
1817
];
1918

@@ -32,11 +31,13 @@ pub fn test_6_1_35_contradicting_remediations(
3231
for p in product_ids {
3332
// Check if product ID has categories associated
3433
if let Some(exist_cat_set) = product_categories.get_mut(&p) {
35-
// Check if any seen category conflicts with the current one
36-
if exist_cat_set.iter().any(|e_cat| {
37-
MUT_EX_MEASURES.contains(e_cat) && MUT_EX_MEASURES.contains(&cat)
38-
|| MUT_EX_FIX_STATES.contains(e_cat) && MUT_EX_FIX_STATES.contains(&cat)
39-
}) {
34+
// Checks if current category is exclusive and a non-equal previous category was found.
35+
if EX_STATES.contains(&cat) && exist_cat_set.first().is_some_and(|e_cat| e_cat != &cat)
36+
// Checks if the (only) previous category is exclusive.
37+
|| exist_cat_set.first().is_some_and(|e_cat| EX_STATES.contains(e_cat))
38+
// Checks if the current category conflicts with any other in the group of mutually exclusive ones.
39+
|| MUT_EX_STATES.contains(&cat) && exist_cat_set.iter().any(|e_cat| MUT_EX_STATES.contains(e_cat))
40+
{
4041
return Err(ValidationError {
4142
message: format!(
4243
"Product {} has contradicting remediations: {} and {}",
@@ -92,6 +93,7 @@ mod tests {
9293
}),
9394
].iter() {
9495
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();
96+
9597
assert_eq!(
9698
Err(err.clone()),
9799
test_6_1_35_contradicting_remediations(&doc)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
use std::collections::HashSet;
2+
use crate::csaf::csaf2_1::schema::CategoryOfTheRemediation;
3+
use crate::csaf::getter_traits::{CsafTrait, ProductStatusTrait, RemediationTrait, VulnerabilityTrait};
4+
use crate::csaf::validation::ValidationError;
5+
6+
/// Remediation categories that conflict with the product status "not affected".
7+
const NOT_AFFECTED_CONFLICTS: &[CategoryOfTheRemediation] = &[
8+
CategoryOfTheRemediation::Workaround,
9+
CategoryOfTheRemediation::Mitigation,
10+
CategoryOfTheRemediation::VendorFix,
11+
CategoryOfTheRemediation::NoneAvailable,
12+
];
13+
14+
/// Remediation categories that conflict with "fixed" product statuses.
15+
const FIXED_CONFLICTS: &[CategoryOfTheRemediation] = &[
16+
CategoryOfTheRemediation::NoneAvailable,
17+
CategoryOfTheRemediation::FixPlanned,
18+
CategoryOfTheRemediation::NoFixPlanned,
19+
CategoryOfTheRemediation::VendorFix,
20+
CategoryOfTheRemediation::Mitigation,
21+
CategoryOfTheRemediation::Workaround,
22+
];
23+
24+
pub fn test_6_1_36_status_group_contradicting_remediation_categories(
25+
doc: &impl CsafTrait,
26+
) -> Result<(), ValidationError> {
27+
for (v_i, v) in doc.get_vulnerabilities().iter().enumerate() {
28+
if let Some(product_status) = v.get_product_status() {
29+
// Collect Product IDs that may cause conflicts
30+
let affected_products = product_status.get_all_affected();
31+
let not_affected_products = match product_status.get_known_not_affected() {
32+
Some(products) => products.into_iter().collect(),
33+
None => HashSet::new(),
34+
};
35+
let fixed_products = product_status.get_all_fixed();
36+
// Iterate over remediations
37+
for (r_i, r) in v.get_remediations().iter().enumerate() {
38+
// Only handle Remediations having product IDs associated
39+
if let Some(product_ids) = r.get_all_product_ids(doc) {
40+
// Category of current remediation
41+
let cat = r.get_category();
42+
// Iterate over product IDs
43+
for p in product_ids {
44+
if affected_products.contains(&p) && cat == CategoryOfTheRemediation::OptionalPatch {
45+
return Err(ValidationError {
46+
message: format!(
47+
"Product {} is listed as affected but has conflicting remediation category {}",
48+
p,
49+
cat
50+
),
51+
instance_path: format!("/vulnerabilities/{}/remediations/{}", v_i, r_i),
52+
});
53+
}
54+
if not_affected_products.contains(&p) && NOT_AFFECTED_CONFLICTS.contains(&cat) {
55+
return Err(ValidationError {
56+
message: format!(
57+
"Product {} is listed as not affected but has conflicting remediation category {}",
58+
p,
59+
cat
60+
),
61+
instance_path: format!("/vulnerabilities/{}/remediations/{}", v_i, r_i),
62+
});
63+
}
64+
if fixed_products.contains(&p) && FIXED_CONFLICTS.contains(&cat) {
65+
return Err(ValidationError {
66+
message: format!(
67+
"Product {} is listed as fixed but has conflicting remediation category {}",
68+
p,
69+
cat
70+
),
71+
instance_path: format!("/vulnerabilities/{}/remediations/{}", v_i, r_i),
72+
});
73+
}
74+
}
75+
}
76+
}
77+
}
78+
}
79+
Ok(())
80+
}
81+
82+
#[cfg(test)]
83+
mod tests {
84+
use crate::csaf::csaf2_1::loader::load_document;
85+
use crate::csaf::validation::ValidationError;
86+
use crate::csaf::validations::test_6_1_36::test_6_1_36_status_group_contradicting_remediation_categories;
87+
88+
#[test]
89+
fn test_test_6_1_36() {
90+
for x in ["11", "12", "13"].iter() {
91+
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();
92+
assert_eq!(
93+
Ok(()),
94+
test_6_1_36_status_group_contradicting_remediation_categories(&doc)
95+
)
96+
}
97+
for (x, err) in [
98+
("01", ValidationError {
99+
message: "Product CSAFPID-9080700 is listed as not affected but has conflicting remediation category vendor_fix".to_string(),
100+
instance_path: "/vulnerabilities/0/remediations/0".to_string()
101+
}),
102+
("02", ValidationError {
103+
message: "Product CSAFPID-9080703 is listed as fixed but has conflicting remediation category none_available".to_string(),
104+
instance_path: "/vulnerabilities/0/remediations/0".to_string()
105+
}),
106+
("03", ValidationError {
107+
message: "Product CSAFPID-9080700 is listed as affected but has conflicting remediation category optional_patch".to_string(),
108+
instance_path: "/vulnerabilities/0/remediations/0".to_string(),
109+
}),
110+
].iter() {
111+
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();
112+
assert_eq!(
113+
Err(err.clone()),
114+
test_6_1_36_status_group_contradicting_remediation_categories(&doc)
115+
)
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)