1use std::collections::BTreeSet;
15
16use serde_json::Value;
17
18use crate::error::{Result, ValidationError, ValidationInfo, ValidationWarning};
19use crate::schemas::registry;
20
21#[derive(Debug, Clone)]
23pub struct ValidationResult {
24 pub valid: bool,
25 pub errors: Vec<ValidationError>,
26 pub warnings: Vec<ValidationWarning>,
27 pub info: Option<Vec<ValidationInfo>>,
28}
29
30#[derive(Debug, Clone, Copy, Default)]
32pub struct ValidateOptions {
33 pub strict: bool,
36 pub show_missing_optional: bool,
39}
40
41pub fn validate(data: &Value, options: ValidateOptions) -> Result<ValidationResult> {
47 let validator = registry().master_validator(options.strict)?;
51
52 let mut errors: Vec<ValidationError> = Vec::new();
53 let mut seen_errors: BTreeSet<(String, String)> = BTreeSet::new();
54 for err in validator.iter_errors(data) {
55 let field = err
56 .instance_path()
57 .as_str()
58 .trim_start_matches('/')
59 .replace('/', ".");
60 let message = err.to_string();
61 let key = (field.clone(), message.clone());
62 if seen_errors.insert(key) {
63 errors.push(ValidationError::new(field, message));
64 }
65 }
66
67 let mut warnings: Vec<ValidationWarning> = Vec::new();
71 let category = data.get("category").and_then(Value::as_str).unwrap_or("");
72 let type_name = data.get("type").and_then(Value::as_str).unwrap_or("");
73
74 if !category.is_empty() && !type_name.is_empty() {
75 if let Value::Object(obj) = data {
76 let type_fields = registry().type_known_fields(category, type_name);
77 for key in obj.keys() {
78 if key == "_internal" || is_known_field(key, type_fields) {
79 continue;
80 }
81 warnings.push(ValidationWarning::new(
82 key.clone(),
83 format!("Unknown field '{key}' is not defined in the XARF schema"),
84 ));
85 }
86 }
87 }
88
89 if options.strict && !warnings.is_empty() {
91 for w in warnings.drain(..) {
92 let key = (w.field.clone(), w.message.clone());
93 if seen_errors.insert(key) {
94 errors.push(ValidationError::new(w.field, w.message));
95 }
96 }
97 }
98
99 let info = if options.show_missing_optional && !category.is_empty() && !type_name.is_empty() {
103 Some(collect_missing_optional(data, category, type_name))
104 } else {
105 None
106 };
107
108 Ok(ValidationResult {
109 valid: errors.is_empty(),
110 errors,
111 warnings,
112 info,
113 })
114}
115
116fn is_known_field(key: &str, type_fields: Option<&[String]>) -> bool {
120 type_fields
121 .map(|fs| fs.binary_search_by(|n| n.as_str().cmp(key)).is_ok())
122 .unwrap_or_else(|| CORE_FIELD_NAMES.binary_search(&key).is_ok())
123}
124
125const CORE_FIELD_NAMES: &[&str] = &[
128 "_internal",
129 "category",
130 "confidence",
131 "description",
132 "evidence",
133 "evidence_source",
134 "legacy_version",
135 "report_id",
136 "reporter",
137 "sender",
138 "source_identifier",
139 "source_port",
140 "tags",
141 "timestamp",
142 "type",
143 "xarf_version",
144];
145
146fn collect_missing_optional(data: &Value, category: &str, type_name: &str) -> Vec<ValidationInfo> {
150 let mut info: Vec<ValidationInfo> = Vec::new();
151 let Value::Object(obj) = data else {
152 return info;
153 };
154 let reg = registry();
155
156 for meta in reg.core_optional_fields() {
157 if obj.contains_key(&meta.name) {
158 continue;
159 }
160 info.push(meta_to_info(meta));
161 }
162
163 if let Some(opt) = reg.type_optional_fields(category, type_name) {
164 for meta in opt {
165 if obj.contains_key(&meta.name) {
166 continue;
167 }
168 info.push(meta_to_info(meta));
169 }
170 }
171
172 info
173}
174
175fn meta_to_info(meta: &crate::schemas::FieldMeta) -> ValidationInfo {
176 let prefix = if meta.recommended {
177 "RECOMMENDED"
178 } else {
179 "OPTIONAL"
180 };
181 ValidationInfo::new(meta.name.clone(), format!("{prefix}: {}", meta.description))
182}
183
184pub fn quick_errors(data: &Value, strict: bool) -> Result<Vec<ValidationError>> {
187 Ok(validate(
188 data,
189 ValidateOptions {
190 strict,
191 show_missing_optional: false,
192 },
193 )?
194 .errors)
195}