csaf-rs 0.5.1

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

use crate::csaf::macros::skip_if_document_status_is_not::skip_if_document_status_is_not;
use crate::csaf::types::csaf_datetime::CsafDateTime::{Invalid, Valid};
use crate::csaf_traits::{
    ContentTrait, CsafTrait, DocumentTrait, MetricTrait, TrackingTrait, VulnerabilityTrait, WithDate,
};
use crate::validation::ValidationError;
use chrono::{DateTime, FixedOffset};

fn create_invalid_revision_date_error(date_str: &str, i_r: usize) -> ValidationError {
    ValidationError {
        message: format!("Invalid date format in revision history: {date_str}"),
        instance_path: format!("/document/tracking/revision_history/{i_r}/date"),
    }
}

static EMPTY_REVISION_HISTORY_ERROR: LazyLock<ValidationError> = LazyLock::new(|| ValidationError {
    message: "Revision history must not be empty for status final or interim".to_string(),
    instance_path: "/document/tracking/revision_history".to_string(),
});

fn create_ssvc_timestamp_too_late_error(
    ssvc_timestamp: &str,
    i_v: usize,
    newest_revision_date: &str,
    i_m: usize,
) -> ValidationError {
    ValidationError {
        message: format!(
            "SSVC timestamp ({ssvc_timestamp}) for vulnerability at index {i_v} is later than the newest revision date ({newest_revision_date})"
        ),
        instance_path: format!("/vulnerabilities/{i_v}/metrics/{i_m}/content/ssvc_v2/timestamp"),
    }
}

fn create_invalid_ssvc_error(error: impl std::fmt::Display, i_v: usize, i_m: usize) -> ValidationError {
    ValidationError {
        message: format!("Invalid SSVC object: {error}"),
        instance_path: format!("/vulnerabilities/{i_v}/metrics/{i_m}/content/ssvc_v2"),
    }
}

/// 6.1.49 Inconsistent SSVC Timestamp
///
/// For each vulnerability, it is tested that the SSVC `timestamp` is earlier or equal to the `date`
/// of the newest item in the `revision_history` if the document status is `final` or `interim`.
pub fn test_6_1_49_inconsistent_ssvc_timestamp(doc: &impl CsafTrait) -> Result<(), Vec<ValidationError>> {
    let document = doc.get_document();
    let tracking = document.get_tracking();

    skip_if_document_status_is_not!(tracking.get_status(), Final, Interim);

    // Parse the date of each revision and find the newest one
    let mut newest_revision_date: Option<DateTime<FixedOffset>> = None;
    for (i_r, revision) in tracking.get_revision_history().iter().enumerate() {
        // TODO: Rewrite this after revision history refactor
        let date = match revision.get_date() {
            Valid(date) => date.get_raw_string().to_owned(),
            Invalid(err) => err.get_raw_string().to_owned(),
        };
        match DateTime::parse_from_rfc3339(date.as_str()) {
            Ok(parsed_date) => {
                newest_revision_date = match newest_revision_date {
                    None => Some(parsed_date),
                    Some(newest_date) => Some(newest_date.max(parsed_date)),
                };
            },
            Err(_) => {
                return Err(vec![create_invalid_revision_date_error(date.as_str(), i_r)]);
            },
        }
    }

    let newest_revision_date = match newest_revision_date {
        Some(date) => date,
        // No entries in revision history
        None => {
            return Err(vec![EMPTY_REVISION_HISTORY_ERROR.clone()]);
        },
    };

    // Check each vulnerability's SSVC timestamp
    for (i_v, vulnerability) in doc.get_vulnerabilities().iter().enumerate() {
        if let Some(metrics) = vulnerability.get_metrics() {
            for (i_m, metric) in metrics.iter().enumerate() {
                let content = metric.get_content();
                if content.has_ssvc_v2() {
                    match content.get_ssvc_v2() {
                        Ok(ssvc) => {
                            if ssvc.timestamp.fixed_offset() > newest_revision_date {
                                return Err(vec![create_ssvc_timestamp_too_late_error(
                                    &ssvc.timestamp.to_rfc3339(),
                                    i_v,
                                    &newest_revision_date.to_rfc3339(),
                                    i_m,
                                )]);
                            }
                        },
                        Err(err) => {
                            return Err(vec![create_invalid_ssvc_error(err, i_v, i_m)]);
                        },
                    }
                }
            }
        }
    }

    Ok(())
}

crate::test_validation::impl_validator!(csaf2_1, ValidatorForTest6_1_49, test_6_1_49_inconsistent_ssvc_timestamp);

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

    #[test]
    fn test_test_6_1_49() {
        // Only CSAF 2.1 has this test with 6 test cases (3 error cases, 3 success cases)
        TESTS_2_1.test_6_1_49.expect(
            Err(vec![create_ssvc_timestamp_too_late_error(
                "2024-07-13T10:00:00+00:00",
                0,
                "2024-01-24T10:00:00+00:00",
                0,
            )]),
            Err(vec![create_ssvc_timestamp_too_late_error(
                "2024-02-29T10:30:00+00:00",
                0,
                "2024-02-29T10:00:00+00:00",
                0,
            )]),
            Err(vec![create_ssvc_timestamp_too_late_error(
                "2024-02-29T10:30:00+00:00",
                0,
                "2024-02-29T10:00:00+00:00",
                0,
            )]),
            Ok(()),
            Ok(()),
            Ok(()),
        );
    }
}