use csaf::validation::{TestResultStatus, ValidationResult, validate_by_preset};
use crate::error::{CsafError, Result};
use crate::validation::{Severity, ValidationError};
fn detect_version(json: &str) -> Result<String> {
let value: serde_json::Value =
serde_json::from_str(json).map_err(|e| CsafError::Validation(e.to_string()))?;
value
.get("document")
.and_then(|d| d.get("csaf_version"))
.and_then(serde_json::Value::as_str)
.map(str::to_owned)
.ok_or_else(|| CsafError::Validation("missing document.csaf_version".to_owned()))
}
fn map_result(result: &ValidationResult) -> Vec<ValidationError> {
let mut out = Vec::new();
for test in &result.test_results {
let TestResultStatus::Failure {
errors, warnings, ..
} = &test.status
else {
continue;
};
for e in errors {
out.push(ValidationError {
path: e.instance_path.clone(),
severity: Severity::Error,
message: format!("[{}] {}", test.test_id, e.message),
});
}
for w in warnings {
out.push(ValidationError {
path: w.instance_path.clone(),
severity: Severity::Warning,
message: format!("[{}] {}", test.test_id, w.message),
});
}
}
out
}
pub fn validate_oasis_json(json: &str) -> Result<Vec<ValidationError>> {
let version = detect_version(json)?;
let result = match version.as_str() {
"2.0" => {
let doc = csaf::csaf2_0::loader::load_document_from_str(json)
.map_err(|e| CsafError::Validation(e.to_string()))?;
validate_by_preset(&doc, "2.0", "basic")
},
"2.1" => {
let doc = csaf::csaf2_1::loader::load_document_from_str(json)
.map_err(|e| CsafError::Validation(e.to_string()))?;
validate_by_preset(&doc, "2.1", "basic")
},
other => {
return Err(CsafError::Validation(format!(
"unsupported CSAF version for OASIS validation: {other}"
)));
},
};
Ok(map_result(&result))
}
#[must_use]
pub fn is_oasis_valid(json: &str) -> bool {
validate_oasis_json(json).is_ok_and(|errs| errs.iter().all(|e| e.severity != Severity::Error))
}
#[cfg(test)]
mod tests {
use super::*;
const GOOD: &str = include_str!("../../../test/csaf/2026/003/ndaal-sa-2026-003.json");
#[test]
fn malformed_json_is_rejected() {
let err = validate_oasis_json("not json").unwrap_err();
assert!(matches!(err, CsafError::Validation(_)));
}
#[test]
fn missing_version_is_rejected() {
let err = validate_oasis_json(r#"{"document":{}}"#).unwrap_err();
assert!(matches!(err, CsafError::Validation(_)));
}
#[test]
fn schema_invalid_document_reports_errors() {
let json = r#"{"document":{"csaf_version":"2.1"}}"#;
let findings = validate_oasis_json(json).expect("runs");
assert!(
findings.iter().any(|e| e.severity == Severity::Error),
"expected schema errors, got: {findings:?}"
);
}
#[test]
fn real_advisory_runs_through_oasis() {
let findings = validate_oasis_json(GOOD).expect("runs");
let errors = findings.iter().filter(|e| e.severity == Severity::Error).count();
let warnings = findings.len() - errors;
eprintln!("OASIS on ndaal-sa-2026-003: {errors} errors, {warnings} warnings");
for f in &findings {
eprintln!(" {:?} {} :: {}", f.severity, f.path, f.message);
}
}
}