csaf-rs 0.5.1

A parser for the CSAF standard written in Rust
use std::sync::LazyLock;

use jsonschema::Validator;
use serde_json::{Map, Value};

use crate::csaf::types::csaf_vuln_metric::CsafVulnerabilityMetric;
use crate::{
    csaf_traits::{ContentTrait, CsafTrait, MetricTrait, VulnerabilityTrait},
    validation::ValidationError,
};

static CVSS20_VALIDATOR: LazyLock<Validator> =
    LazyLock::new(|| create_validator(include_str!("../../assets/cvss-v2.0.json")));
static CVSS30_VALIDATOR: LazyLock<Validator> =
    LazyLock::new(|| create_validator(include_str!("../../assets/cvss-v3.0.json")));
static CVSS31_VALIDATOR: LazyLock<Validator> =
    LazyLock::new(|| create_validator(include_str!("../../assets/cvss-v3.1.json")));
static CVSS40_VALIDATOR: LazyLock<Validator> =
    LazyLock::new(|| create_draft_validator(include_str!("../../assets/cvss-v4.0.2.json")));

/// 6.1.8 Invalid CVSS
/// Invalid CVSS object according to scheme
pub fn test_6_1_08_invalid_cvss(doc: &impl CsafTrait) -> Result<(), Vec<ValidationError>> {
    let mut errors: Vec<ValidationError> = Vec::new();

    for (i_v, vulnerability) in doc.get_vulnerabilities().iter().enumerate() {
        if let Some(metrics) = vulnerability.get_metrics() {
            for (metric_index, metric) in metrics.iter().enumerate() {
                let content = metric.get_content();
                let instance_prefix = content.get_content_json_path(i_v, metric_index);
                if let Some(cvss2) = content.get_cvss_v2() {
                    evaluate_cvss(
                        cvss2,
                        &CVSS20_VALIDATOR,
                        &instance_prefix,
                        CsafVulnerabilityMetric::CvssV2("2.0".to_string()),
                        &mut errors,
                    );
                }
                if let Some(cvss3) = content.get_cvss_v3() {
                    // Use as_str because otherwise additional quotation marks would be included
                    if let Some(version) = cvss3.get("version").and_then(|v| v.as_str()) {
                        let metric_type = CsafVulnerabilityMetric::CvssV3(version.to_string());
                        if version == "3.0" {
                            evaluate_cvss(cvss3, &CVSS30_VALIDATOR, &instance_prefix, metric_type, &mut errors);
                        } else if version == "3.1" {
                            evaluate_cvss(cvss3, &CVSS31_VALIDATOR, &instance_prefix, metric_type, &mut errors);
                        }
                    }
                }
                if let Some(cvss4) = content.get_cvss_v4() {
                    evaluate_cvss(
                        cvss4,
                        &CVSS40_VALIDATOR,
                        &instance_prefix,
                        CsafVulnerabilityMetric::CvssV4("4.0".to_string()),
                        &mut errors,
                    );
                }
            }
        }
    }

    if errors.is_empty() { Ok(()) } else { Err(errors) }
}

crate::test_validation::impl_validator!(ValidatorForTest6_1_8, test_6_1_08_invalid_cvss);

fn create_validator(schema_str: &str) -> Validator {
    jsonschema::validator_for(&serde_json::from_str(schema_str).unwrap()).unwrap()
}

fn create_draft_validator(schema_str: &str) -> Validator {
    jsonschema::draft202012::new(&serde_json::from_str(schema_str).unwrap()).unwrap()
}

/// Run the CVSS through json schema validation, add every error during validation to `errors`
/// TODO: The metric prop is kinda weird, but this will be removed after CVSS validation is implemented.
fn evaluate_cvss(
    cvss_value: &Map<String, Value>,
    validator: &Validator,
    base_path: &str,
    metric: CsafVulnerabilityMetric,
    errors: &mut Vec<ValidationError>,
) {
    let value = serde_json::to_value(cvss_value).unwrap();
    for error in validator.iter_errors(&value) {
        errors.push(create_validation_error(error.to_string(), base_path, metric.clone()));
    }
}

fn create_validation_error(message: String, base: &str, metric: CsafVulnerabilityMetric) -> ValidationError {
    ValidationError {
        message,
        instance_path: format!("{}/{}", base, metric.get_metric_prop_name()),
    }
}

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

    #[test]
    fn test_test_6_1_08() {
        // CSAF 2.0 has 7 test cases (01-03, 11-14)
        TESTS_2_0.test_6_1_8.expect(
            Err(vec![create_validation_error(
                "\"baseSeverity\" is a required property".to_string(),
                "/vulnerabilities/0/scores/0",
                CsafVulnerabilityMetric::CvssV3("3.1".to_string()),
            )]),
            Err(vec![create_validation_error(
                "\"baseSeverity\" is a required property".to_string(),
                "/vulnerabilities/0/scores/0",
                CsafVulnerabilityMetric::CvssV3("3.0".to_string()),
            )]),
            Err(vec![create_validation_error(
                "\"version\" is a required property".to_string(),
                "/vulnerabilities/0/scores/0",
                CsafVulnerabilityMetric::CvssV2("2.0".to_string()),
            )]),
            Ok(()), // case_11
            Ok(()), // case_12
            Ok(()), // case_13
            Ok(()), // case_14
        );

        // CSAF 2.1 has 13 test cases (01-06, 11-17)
        TESTS_2_1.test_6_1_8.expect(
            Err(vec![create_validation_error(
                "\"baseSeverity\" is a required property".to_string(),
                "/vulnerabilities/0/metrics/0/content",
                CsafVulnerabilityMetric::CvssV3("3.1".to_string()),
            )]),
            Err(vec![create_validation_error(
                "\"baseSeverity\" is a required property".to_string(),
                "/vulnerabilities/0/metrics/0/content",
                CsafVulnerabilityMetric::CvssV3("3.0".to_string()),
            )]),
            Err(vec![create_validation_error(
                "\"version\" is a required property".to_string(),
                "/vulnerabilities/0/metrics/0/content",
                CsafVulnerabilityMetric::CvssV2("2.0".to_string()),
            )]),
            Err(vec![create_validation_error(
                "\"baseSeverity\" is a required property".to_string(),
                "/vulnerabilities/0/metrics/0/content",
                CsafVulnerabilityMetric::CvssV4("4.0".to_string()),
            )]),
            Err(vec![
                create_validation_error(
                    "Unevaluated properties are not allowed ('threatScore', 'threatSeverity' were unexpected)".to_string(),
                    "/vulnerabilities/0/metrics/0/content",
                    CsafVulnerabilityMetric::CvssV4("4.0".to_string()),
                ),
            ]),
            Err(vec![
                create_validation_error(
                    "Unevaluated properties are not allowed ('threatScore', 'threatSeverity', 'environmentalScore', 'environmentalSeverity' were unexpected)".to_string(),
                    "/vulnerabilities/0/metrics/0/content",
                    CsafVulnerabilityMetric::CvssV4("4.0".to_string()),
                ),
            ]),
            Ok(()), // case_11
            Ok(()), // case_12
            Ok(()), // case_13
            Ok(()), // case_14
            Ok(()), // case_15
            Ok(()), // case_16
            Ok(()), // case_17
        );
    }
}