use std::collections::BTreeSet;
use serde_json::Value;
use crate::error::{Result, ValidationError, ValidationInfo, ValidationWarning};
use crate::schemas::registry;
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationWarning>,
pub info: Option<Vec<ValidationInfo>>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ValidateOptions {
pub strict: bool,
pub show_missing_optional: bool,
}
pub fn validate(data: &Value, options: ValidateOptions) -> Result<ValidationResult> {
let validator = registry().master_validator(options.strict)?;
let mut errors: Vec<ValidationError> = Vec::new();
let mut seen_errors: BTreeSet<(String, String)> = BTreeSet::new();
for err in validator.iter_errors(data) {
let field = err
.instance_path()
.as_str()
.trim_start_matches('/')
.replace('/', ".");
let message = err.to_string();
let key = (field.clone(), message.clone());
if seen_errors.insert(key) {
errors.push(ValidationError::new(field, message));
}
}
let mut warnings: Vec<ValidationWarning> = Vec::new();
let category = data.get("category").and_then(Value::as_str).unwrap_or("");
let type_name = data.get("type").and_then(Value::as_str).unwrap_or("");
if !category.is_empty() && !type_name.is_empty() {
if let Value::Object(obj) = data {
let type_fields = registry().type_known_fields(category, type_name);
for key in obj.keys() {
if key == "_internal" || is_known_field(key, type_fields) {
continue;
}
warnings.push(ValidationWarning::new(
key.clone(),
format!("Unknown field '{key}' is not defined in the XARF schema"),
));
}
}
}
if options.strict && !warnings.is_empty() {
for w in warnings.drain(..) {
let key = (w.field.clone(), w.message.clone());
if seen_errors.insert(key) {
errors.push(ValidationError::new(w.field, w.message));
}
}
}
let info = if options.show_missing_optional && !category.is_empty() && !type_name.is_empty() {
Some(collect_missing_optional(data, category, type_name))
} else {
None
};
Ok(ValidationResult {
valid: errors.is_empty(),
errors,
warnings,
info,
})
}
fn is_known_field(key: &str, type_fields: Option<&[String]>) -> bool {
type_fields
.map(|fs| fs.binary_search_by(|n| n.as_str().cmp(key)).is_ok())
.unwrap_or_else(|| CORE_FIELD_NAMES.binary_search(&key).is_ok())
}
const CORE_FIELD_NAMES: &[&str] = &[
"_internal",
"category",
"confidence",
"description",
"evidence",
"evidence_source",
"legacy_version",
"report_id",
"reporter",
"sender",
"source_identifier",
"source_port",
"tags",
"timestamp",
"type",
"xarf_version",
];
fn collect_missing_optional(data: &Value, category: &str, type_name: &str) -> Vec<ValidationInfo> {
let mut info: Vec<ValidationInfo> = Vec::new();
let Value::Object(obj) = data else {
return info;
};
let reg = registry();
for meta in reg.core_optional_fields() {
if obj.contains_key(&meta.name) {
continue;
}
info.push(meta_to_info(meta));
}
if let Some(opt) = reg.type_optional_fields(category, type_name) {
for meta in opt {
if obj.contains_key(&meta.name) {
continue;
}
info.push(meta_to_info(meta));
}
}
info
}
fn meta_to_info(meta: &crate::schemas::FieldMeta) -> ValidationInfo {
let prefix = if meta.recommended {
"RECOMMENDED"
} else {
"OPTIONAL"
};
ValidationInfo::new(meta.name.clone(), format!("{prefix}: {}", meta.description))
}
pub fn quick_errors(data: &Value, strict: bool) -> Result<Vec<ValidationError>> {
Ok(validate(
data,
ValidateOptions {
strict,
show_missing_optional: false,
},
)?
.errors)
}