pub mod v2;
pub mod v3;
pub mod v4;
use std::fmt;
use crate::csaf_traits::ContentTrait;
use crate::validation::ValidationError;
use cvss_rs::Cvss;
use cvss_rs::Severity;
use cvss_rs::Version;
use serde_json::Value;
pub fn validate_content_scores(
content: &impl ContentTrait,
instance_path: &str,
errors: &mut Option<Vec<ValidationError>>,
) {
if let Some(cvss_v2_map) = content.get_cvss_v2() {
validate_scores(cvss_v2_map, instance_path, errors, Version::V2);
}
if let Some(cvss_v3_map) = content.get_cvss_v3() {
validate_scores(cvss_v3_map, instance_path, errors, Version::V3_0);
}
if let Some(cvss_v4_map) = content.get_cvss_v4() {
validate_scores(cvss_v4_map, instance_path, errors, Version::V4);
}
}
pub fn validate_content_consistency(
content: &impl ContentTrait,
instance_path: &str,
errors: &mut Option<Vec<ValidationError>>,
) {
if let Some(cvss_map) = content.get_cvss_v2() {
validate_consistency(cvss_map, instance_path, errors, Version::V2);
}
if let Some(cvss_map) = content.get_cvss_v3() {
validate_consistency(cvss_map, instance_path, errors, Version::V3_0);
}
if let Some(cvss_map) = content.get_cvss_v4() {
validate_consistency(cvss_map, instance_path, errors, Version::V4);
}
}
fn validate_scores(
cvss_map: &serde_json::Map<String, Value>,
instance_path: &str,
errors: &mut Option<Vec<ValidationError>>,
expected_version: Version,
) {
let Some(cvss_deserialized) = deserialize_cvss(cvss_map, instance_path, errors) else {
return;
};
match (expected_version, cvss_deserialized) {
(Version::V2, Cvss::V2(cvss2)) => {
v2::validate_scores(&cvss2, cvss_map, instance_path, errors);
},
(Version::V3_0, Cvss::V3_0(cvss3) | Cvss::V3_1(cvss3)) => {
v3::validate_scores(&cvss3, instance_path, errors);
},
(Version::V4, Cvss::V4(cvss4)) => {
v4::validate_scores(&cvss4, instance_path, errors);
},
(expected, found) => {
errors.get_or_insert_default().push(create_deserialization_error(
format!(
"Deserialized CVSS metric {} does not match expected version {expected}",
found.version()
),
instance_path.to_string(),
));
},
}
}
fn validate_consistency(
cvss_map: &serde_json::Map<String, Value>,
instance_path: &str,
errors: &mut Option<Vec<ValidationError>>,
expected_version: Version,
) {
let Some(cvss_deserialized) = deserialize_cvss(cvss_map, instance_path, errors) else {
return;
};
match (expected_version, cvss_deserialized) {
(Version::V2, Cvss::V2(cvss2)) => {
v2::validate_consistency(&cvss2, instance_path, errors);
},
(Version::V3_0, Cvss::V3_0(cvss3) | Cvss::V3_1(cvss3)) => {
v3::validate_consistency(&cvss3, instance_path, errors);
},
(Version::V4, Cvss::V4(cvss4)) => {
v4::validate_consistency(&cvss4, instance_path, errors);
},
(expected, found) => {
errors.get_or_insert_default().push(create_deserialization_error(
format!(
"Deserialized CVSS metric {} does not match expected version {expected}",
found.version()
),
instance_path.to_string(),
));
},
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScoreType {
Base,
Temporal,
Environmental,
}
impl fmt::Display for ScoreType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScoreType::Base => write!(f, "Base"),
ScoreType::Temporal => write!(f, "Temporal"),
ScoreType::Environmental => write!(f, "Environmental"),
}
}
}
pub fn create_deserialization_error(error_message: String, instance_path: String) -> ValidationError {
ValidationError {
message: format!("Error deserializing CVSS metric: {error_message}"),
instance_path,
}
}
pub fn create_vector_parse_error(
vector_string: &str,
version: Version,
parse_error: &cvss_rs::ParseError,
instance_path: &str,
) -> ValidationError {
let version_str = match version {
Version::V2 => "2.0",
Version::V4 => "4.0",
_ => "3.x",
};
ValidationError {
message: format!("Could not parse vector string \"{vector_string}\" as CVSS {version_str}: {parse_error}"),
instance_path: instance_path.to_string(),
}
}
pub fn deserialize_cvss(
cvss_map: &serde_json::Map<String, Value>,
instance_path: &str,
errors: &mut Option<Vec<ValidationError>>,
) -> Option<Cvss> {
match serde_json::from_value(Value::Object(cvss_map.to_owned())) {
Ok(cvss) => Some(cvss),
Err(e) => {
errors
.get_or_insert_default()
.push(create_deserialization_error(e.to_string(), instance_path.to_string()));
None
},
}
}
pub fn create_score_mismatch_error(
calculated: f64,
actual: f64,
score_type: ScoreType,
instance_path: &str,
) -> ValidationError {
ValidationError {
message: format!(
"{score_type} score does not match the expected value calculated from the vector. \
Expected: {calculated}, found: {actual}"
),
instance_path: instance_path.to_string(),
}
}
pub fn check_score_mismatch(
actual: f64,
calculated: f64,
score_type: ScoreType,
instance_path: &str,
errors: &mut Option<Vec<ValidationError>>,
) {
if (actual * 10.0).round() as i8 != (calculated * 10.0).round() as i8 {
errors.get_or_insert_default().push(create_score_mismatch_error(
calculated,
actual,
score_type,
instance_path,
));
}
}
pub fn create_severity_mismatch_error(
calculated: &Severity,
actual: &Severity,
score_type: ScoreType,
instance_path: &str,
) -> ValidationError {
ValidationError {
message: format!(
"{score_type} severity does not match the expected value calculated from the vector. \
Expected: {calculated:?}, found: {actual:?}"
),
instance_path: instance_path.to_string(),
}
}
pub fn check_severity_mismatch(
actual: &Severity,
calculated: &Severity,
score_type: ScoreType,
instance_path: &str,
errors: &mut Option<Vec<ValidationError>>,
) {
if actual != calculated {
errors.get_or_insert_default().push(create_severity_mismatch_error(
calculated,
actual,
score_type,
instance_path,
));
}
}
pub fn map_score_to_severity(score: Option<f64>) -> Option<Severity> {
let scaled = (score? * 10.0).round() as i8;
Some(match scaled {
0 => Severity::None,
1..=39 => Severity::Low,
40..=69 => Severity::Medium,
70..=89 => Severity::High,
90..=100 => Severity::Critical,
_ => return None,
})
}
pub fn create_field_value_mismatch_error<T: std::fmt::Display>(
field_name: &str,
json_val: &T,
vec_val: &T,
instance_path: &str,
) -> ValidationError {
ValidationError {
message: format!(
"Property \"{field_name}\" does not match the value from the vector string. \
Expected: {vec_val}, found: {json_val}"
),
instance_path: instance_path.to_string(),
}
}
pub fn create_field_missing_in_vector_error<T: std::fmt::Display>(
field_name: &str,
json_val: &T,
instance_path: &str,
) -> ValidationError {
ValidationError {
message: format!(
"Property \"{field_name}\" is present in the object ({json_val}) but missing in the vector string"
),
instance_path: instance_path.to_string(),
}
}
pub fn create_field_missing_in_object_error<T: std::fmt::Display>(
field_name: &str,
vec_val: &T,
instance_path: &str,
) -> ValidationError {
ValidationError {
message: format!(
"Property \"{field_name}\" is missing in the object but present in the vector string ({vec_val})"
),
instance_path: instance_path.to_string(),
}
}
fn is_not_defined(val: &impl std::fmt::Debug) -> bool {
format!("{val:?}") == "NotDefined"
}
pub fn check_optional_field_mismatch<T: PartialEq + std::fmt::Display + std::fmt::Debug>(
field_name: &str,
json_value: &Option<T>,
vector_value: &Option<T>,
instance_path: &str,
errors: &mut Option<Vec<ValidationError>>,
) {
let json_effective = json_value.as_ref().filter(|v| !is_not_defined(*v));
let vector_effective = vector_value.as_ref().filter(|v| !is_not_defined(*v));
match (json_effective, vector_effective) {
(Some(json_val), Some(vec_val)) if json_val != vec_val => {
errors.get_or_insert_default().push(create_field_value_mismatch_error(
field_name,
json_val,
vec_val,
instance_path,
));
},
(Some(json_val), None) => {
errors
.get_or_insert_default()
.push(create_field_missing_in_vector_error(
field_name,
json_val,
instance_path,
));
},
(None, Some(vec_val)) => {
errors
.get_or_insert_default()
.push(create_field_missing_in_object_error(field_name, vec_val, instance_path));
},
_ => {},
}
}