Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 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-rs/src/csaf2_0/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ impl Validatable for CommonSecurityAdvisoryFramework {
"6.2.16" => Some(ValidatorForTest6_2_16.validate(self)),
"6.2.17" => Some(ValidatorForTest6_2_17.validate(self)),
"6.2.18" => Some(ValidatorForTest6_2_18.validate(self)),
"6.2.19" => None, // Some(ValidatorForTest6_2_19.validate(self)),
"6.2.19" => Some(ValidatorForTest6_2_19.validate(self)),
"6.2.20" => {
// see below in RawValidatable
return TestResult {
Expand Down
5 changes: 5 additions & 0 deletions csaf-rs/src/cvss/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,11 @@ fn is_not_defined(val: &impl std::fmt::Debug) -> bool {
format!("{val:?}") == "NotDefined"
}

/// Returns true if a score is 0.0 after rounding to one decimal place (rounds via integer scaling)
pub(crate) fn is_zero_score(actual: f64) -> bool {
(actual * 10.0).round() as i8 == 0
}

/// Compares an optional field from the deserialized JSON object against the value parsed from the
/// vector string. `Some(NotDefined)` is treated as equivalent to `None`.
pub fn check_optional_field_mismatch<T: PartialEq + std::fmt::Display + std::fmt::Debug>(
Expand Down
2 changes: 1 addition & 1 deletion csaf-rs/src/validations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ pub mod test_6_2_15;
pub mod test_6_2_16;
pub mod test_6_2_17;
pub mod test_6_2_18;
// pub mod test_6_2_19;
pub mod test_6_2_19;
pub mod test_6_2_20;
pub mod test_6_2_21;
pub mod test_6_2_22;
Expand Down
235 changes: 235 additions & 0 deletions csaf-rs/src/validations/test_6_2_19.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
use std::str::FromStr;

use crate::csaf_traits::{
ContentTrait, CsafTrait, MetricTrait, ProductStatusAndPath, ProductStatusGroup, ProductStatusGroupMap,
VulnerabilityTrait,
};
use crate::cvss::{deserialize_cvss, is_zero_score};
use crate::validation::ValidationError;
use cvss_rs::Cvss;
use cvss_rs::v2_0::{CvssV2, TargetDistribution};
use cvss_rs::v3::{CvssV3, Impact};

fn create_cvss_for_fixed_products_error(
product_id: &str,
statuses: &[ProductStatusAndPath],
path: &str,
) -> ValidationError {
let status_list: Vec<String> = statuses.iter().map(|s| s.status.to_string()).collect();
ValidationError {
message: format!(
"Product '{}' is listed as fixed (status(es): '{}') but has a CVSS environmental score that is not 0.0",
product_id,
status_list.join(", ")
),
instance_path: path.to_string(),
}
}

/// Checks if a CVSS v2 score has an environmental score of 0.
fn cvss_v2_has_env_score_zero(cvss_v2: CvssV2) -> bool {
let has_target_distribution_none =
|cvss_v2: &CvssV2| -> bool { matches!(cvss_v2.target_distribution, Some(TargetDistribution::None)) };

// check env score provided in json
if let Some(env_score) = cvss_v2.environmental_score
&& is_zero_score(env_score)
{
return true;
}

// check if json contains prop that would set env score to zero
if has_target_distribution_none(&cvss_v2) {
return true;
}

// generate cvss object from vector
match CvssV2::from_str(&cvss_v2.vector_string) {
Err(_) => false, // #409 nondeterminable
// check if vector contains prop that would set env score to zero
Ok(from_vector) => has_target_distribution_none(&from_vector),
}
Comment thread
peinjoh marked this conversation as resolved.
}

/// Checks if a CVSS v3 score has an environmental score of 0.
fn cvss_v3_has_env_score_zero(cvss_v3: CvssV3) -> bool {
let has_all_modified_impacts_none = |cvss_v3: &CvssV3| -> bool {
matches!(
(
&cvss_v3.modified_availability_impact,
&cvss_v3.modified_confidentiality_impact,
&cvss_v3.modified_integrity_impact
),
(Some(Impact::None), Some(Impact::None), Some(Impact::None))
)
};

// check env score provided in json
if let Some(env_score) = cvss_v3.environmental_score
&& is_zero_score(env_score)
{
return true;
}

// check if json contains prop that would set env score to zero
if has_all_modified_impacts_none(&cvss_v3) {
return true;
}

// generate cvss object from vector
match CvssV3::from_str(&cvss_v3.vector_string) {
Err(_) => false, // #409 nondeterminable
// check if vector contains prop that would set env score to zero
Ok(from_vector) => has_all_modified_impacts_none(&from_vector),
}
}
Comment thread
peinjoh marked this conversation as resolved.

/// Returns true if all CVSS scores in this content have environmental score of 0.
/// If both v2 and v3 are present, both must have an environmental score of 0.
fn content_has_all_cvss_env_score_zero(content: &impl ContentTrait) -> bool {
// check if cvss_v2 prop is set
Comment thread
peinjoh marked this conversation as resolved.
if let Some(cvss_v2) = content.get_cvss_v2() {
// deserialize cvss, we only care about result, not errors
let Some(deserialized) = deserialize_cvss(cvss_v2, "", &mut None) else {
// TODO: Nondeterminable #409, could not deserialize
return false;
};
let v2_is_zero = match deserialized {
Cvss::V2(v2) => cvss_v2_has_env_score_zero(v2),
// TODO: Nondeterminable #409 - deserialized into wrong version
_ => false,
};
if !v2_is_zero {
return false;
}
}

// check if the cvss_v3 prop is set
if let Some(cvss_v3) = content.get_cvss_v3() {
// deserialize cvss, we only care about result, not errors
let Some(deserialized) = deserialize_cvss(cvss_v3, "", &mut None) else {
// TODO: Nondeterminable #409, could not deserialize
return false;
};
let v3_is_zero = match deserialized {
Cvss::V3_0(v3) | Cvss::V3_1(v3) => cvss_v3_has_env_score_zero(v3),
// TODO: Nondeterminable #409 - deserialized into wrong version
_ => false,
};
if !v3_is_zero {
return false;
}
}

// true if there are no CVSS scores, or all present CVSS scores have env score of zero
true
}

/// 6.2.19 CVSS for Fixed Products
///
/// For each item in the fixed products group (first_fixed and fixed) it MUST be tested that
/// a CVSS applying to this product has an environmental score of 0.
/// The test SHALL pass if none of the Product IDs listed within product status fixed or
/// first_fixed is found in products of any item of the scores element.
pub fn test_6_2_19_cvss_for_fixed_products(doc: &impl CsafTrait) -> Result<(), Vec<ValidationError>> {
let mut errors: Option<Vec<ValidationError>> = None;
for (v_i, vuln) in doc.get_vulnerabilities().iter().enumerate() {
// collect fixed product IDs using the aggregation map
let status_map = match vuln.get_product_status() {
Some(product_status) => ProductStatusGroupMap::from(product_status),
// there are no product statuses
None => continue,
};
let fixed_products = match status_map.get(&ProductStatusGroup::Fixed) {
Some(products) => products,
// there are no products with status group fixed
None => continue,
};

// check each metric/score
if let Some(metrics) = vuln.get_metrics() {
let metrics_path = vuln.get_metrics_path();
for (m_i, metric) in metrics.iter().enumerate() {
let content = metric.get_content();
for (p_i, product_id) in metric.get_products().enumerate() {
// if the metric/score is relevant to a product
if let Some(statuses) = fixed_products.get(product_id) {
// and the product does not have an env score of zero, generate an error
if !content_has_all_cvss_env_score_zero(content) {
errors
.get_or_insert_default()
.push(create_cvss_for_fixed_products_error(
product_id,
statuses,
&format!("/vulnerabilities/{v_i}/{metrics_path}/{m_i}/products/{p_i}"),
));
}
}
}
}
}
}

errors.map_or(Ok(()), Err)
}

crate::test_validation::impl_validator!(ValidatorForTest6_2_19, test_6_2_19_cvss_for_fixed_products);
Comment thread
peinjoh marked this conversation as resolved.

#[cfg(test)]
mod tests {
use super::*;
use crate::csaf_traits::ProductStatus;
use crate::csaf2_0::testcases::TESTS_2_0;

#[test]
fn test_test_6_2_19() {
// Test data only contains two paths, so we can share the error messages
let err_fixed = Err(vec![create_cvss_for_fixed_products_error(
"CSAFPID-9080700",
&[ProductStatusAndPath {
status: ProductStatus::Fixed,
index: 0,
}],
"/vulnerabilities/0/scores/0/products/0",
)]);
let err_first_fixed = Err(vec![create_cvss_for_fixed_products_error(
"CSAFPID-9080700",
&[ProductStatusAndPath {
status: ProductStatus::FirstFixed,
index: 0,
}],
"/vulnerabilities/0/scores/0/products/0",
)]);

// Case 01: CVSS v3.1, no metric that sets to 0, status fixed
// Case 02: CVSS v3.1, JSON modifiedAvailabilityImpact is not set to None, status fixed
// Case 03: CVSS v2, JSON targetDistribution is not set to None, status fixed
// Case 04: CVSS v2, no metric that sets to 0, status fixed
// Case 05: CVSS v3.0, no metric that sets to 0, status first_fixed
// Case 06: CVSS v3.0, JSON modifiedAvailabilityImpact is not set to None, status fixed

// Case 11: CVSS v3.1, all modifiedImpact metrics are None in vector, status fixed
// Case 12: CVSS v3.1, all modifiedImpact metrics are None in JSON, status fixed
// Case 13: CVSS v2, targetDistribution is None in JSON, status fixed
// Case 14: CVSS v2, targetDistribution is None in vector, status fixed
// Case 15: CVSS v3.0, all modifiedImpact metrics are None in vector, status first_fixed
// Case 16: CVSS v3.1, all modifiedImpact metrics are None in JSON, status fixed
// Case 17: product status known_affected

TESTS_2_0.test_6_2_19.expect(
err_fixed.clone(),
err_fixed.clone(),
err_fixed.clone(),
err_fixed.clone(),
err_first_fixed,
err_fixed,
Ok(()),
Ok(()),
Ok(()),
Ok(()),
Ok(()),
Ok(()),
Ok(()),
);
}
}
Loading