1use super::reports::Mirror;
4use crate::codec::Format;
5use crate::model::{ExceptionItem, ExceptionList, Rule};
6use crate::normalize;
7use elasticctl_core::{Error, ErrorKind, Result};
8use serde_json::Value;
9use std::path::{Path, PathBuf};
10
11pub fn read_mirror(dir: &Path) -> Result<Mirror> {
19 refuse_value_lists(dir)?;
20 let mut mirror = Mirror {
21 rules: Vec::new(),
22 lists: Vec::new(),
23 items: Vec::new(),
24 };
25
26 let rules_path = super::rules_dir(dir);
27 if let Some(paths) = mirror_root_files(&rules_path)? {
28 for path in paths {
29 let body = read_regular_file(&path)?;
30 let (mut rules, lists, items) = decode_rule_file(&body, Format::from_path(&path))?;
31 mirror.rules.append(&mut rules);
32 for mut list in lists {
33 let list_items = split_items(&mut list)?;
34 mirror.lists.push(list);
35 mirror.items.extend(list_items);
36 }
37 for item in &items {
38 validate_top_level_item(item)?;
39 }
40 mirror.items.extend(items);
41 }
42 }
43
44 let lists_path = super::exceptions_dir(dir);
45 if let Some(paths) = mirror_root_files(&lists_path)? {
46 for path in paths {
47 let body = read_regular_file(&path)?;
48 let mut list = decode_list_file(&body, Format::from_path(&path))?;
49 let items = split_items(&mut list)?;
50 mirror.lists.push(list);
51 mirror.items.extend(items);
52 }
53 }
54
55 normalize::sort_rules(&mut mirror.rules);
56 normalize::sort_lists(&mut mirror.lists);
57 normalize::sort_items(&mut mirror.items);
58 Ok(mirror)
59}
60
61pub(crate) fn refuse_value_lists(dir: &Path) -> Result<()> {
65 match std::fs::symlink_metadata(dir.join("value-lists")) {
66 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
67 Ok(_) => Err(Error::new(
68 ErrorKind::Unsupported,
69 "this mirror holds value-lists/, which needs elasticctl 0.10 or later",
70 )),
71 Err(e) => Err(Error::new(
72 ErrorKind::Error,
73 format!("checking the mirror for value-lists/: {e}"),
74 )),
75 }
76}
77
78fn validate_top_level_item(item: &ExceptionItem) -> Result<()> {
79 let item_id = item.item_id()?;
80 if item_id.is_empty() {
81 return Err(Error::new(
82 ErrorKind::Error,
83 "exception item field item_id must be a non-empty string",
84 ));
85 }
86 let list_id = item.list_id()?;
87 if list_id.is_empty() {
88 return Err(Error::new(
89 ErrorKind::Error,
90 "exception item field list_id must be a non-empty string",
91 ));
92 }
93 match item.as_map().get("namespace_type") {
94 None => Ok(()),
95 Some(Value::String(value)) if !value.is_empty() => Ok(()),
96 Some(_) => Err(Error::new(
97 ErrorKind::Error,
98 "exception item field namespace_type must be a non-empty string",
99 )),
100 }
101}
102
103pub fn read_local(dir: &Path) -> Result<Vec<Rule>> {
105 Ok(read_mirror(dir)?.rules)
106}
107
108fn mirror_root_files(dir: &Path) -> Result<Option<Vec<PathBuf>>> {
118 match std::fs::symlink_metadata(dir) {
119 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
120 Err(e) => Err(Error::new(
121 ErrorKind::Error,
122 format!("reading {}: {e}", dir.display()),
123 )),
124 Ok(metadata) => {
125 if metadata.file_type().is_symlink() || !metadata.is_dir() {
126 return Err(Error::new(
127 ErrorKind::Error,
128 format!("mirror directory {} is not a real directory", dir.display()),
129 ));
130 }
131 Ok(Some(mirror_files(dir)?))
132 }
133 }
134}
135
136fn mirror_files(dir: &Path) -> Result<Vec<PathBuf>> {
138 let mut out = Vec::new();
139 for entry in std::fs::read_dir(dir)
140 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", dir.display())))?
141 {
142 if let Some(path) = mirror_entry_path(dir, entry)? {
143 out.push(path);
144 }
145 }
146 out.sort();
147 Ok(out)
148}
149
150fn mirror_entry_path(
154 dir: &Path,
155 entry: std::io::Result<std::fs::DirEntry>,
156) -> Result<Option<PathBuf>> {
157 let entry = entry
158 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", dir.display())))?;
159 let path = entry.path();
160 if !super::is_rule_file(&path) {
161 return Ok(None);
162 }
163 let file_type = entry
166 .file_type()
167 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display())))?;
168 if !file_type.is_file() {
169 return Err(Error::new(
170 ErrorKind::Error,
171 format!("mirror entry {} is not a regular file", path.display()),
172 ));
173 }
174 Ok(Some(path))
175}
176
177#[cfg(unix)]
186fn read_regular_file(path: &Path) -> Result<String> {
187 use std::io::Read;
188 use std::os::unix::fs::OpenOptionsExt;
189 let mut file = std::fs::OpenOptions::new()
190 .read(true)
191 .custom_flags(libc::O_NOFOLLOW)
192 .open(path)
193 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display())))?;
194 let mut body = String::new();
195 file.read_to_string(&mut body)
196 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display())))?;
197 Ok(body)
198}
199
200#[cfg(not(unix))]
201fn read_regular_file(path: &Path) -> Result<String> {
202 let metadata = std::fs::symlink_metadata(path)
203 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display())))?;
204 if metadata.file_type().is_symlink() {
205 return Err(Error::new(
206 ErrorKind::Error,
207 format!("mirror entry {} is not a regular file", path.display()),
208 ));
209 }
210 std::fs::read_to_string(path)
211 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display())))
212}
213
214fn decode_rule_file(
222 body: &str,
223 format: Format,
224) -> Result<(Vec<Rule>, Vec<ExceptionList>, Vec<ExceptionItem>)> {
225 match format {
226 Format::Ndjson => {
227 let bundle = crate::codec::decode_bundle(body)?;
228 Ok((bundle.rules, bundle.lists, bundle.items))
229 }
230 Format::Yaml => {
231 let values: Vec<Value> = serde_yaml_ng::from_str(body)
232 .map_err(|e| Error::new(ErrorKind::Error, format!("parsing YAML: {e}")))?;
233 let mut rules = Vec::new();
234 let mut lists = Vec::new();
235 let mut items = Vec::new();
236 for value in values {
237 if value.get("rule_id").is_some() {
241 rules.push(Rule::from_value(value)?);
242 } else if value.get("item_id").is_some() {
243 items.push(ExceptionItem::from_value(value)?);
244 } else if value.get("list_id").is_some() {
245 lists.push(ExceptionList::from_value(value)?);
246 } else {
247 return Err(Error::new(
248 ErrorKind::Error,
249 "a mirror file entry has neither rule_id, item_id, nor list_id",
250 ));
251 }
252 }
253 Ok((rules, lists, items))
254 }
255 }
256}
257
258pub(crate) fn encode_rule_file(
261 rule: &Rule,
262 inline_lists: &[ExceptionList],
263 format: Format,
264) -> Result<String> {
265 let mut objects = Vec::with_capacity(1 + inline_lists.len());
266 objects.push(rule.clone().into_value());
267 for list in inline_lists {
268 objects.push(list.clone().into_value());
269 }
270 match format {
271 Format::Yaml => serde_yaml_ng::to_string(&objects)
272 .map_err(|e| Error::new(ErrorKind::Error, format!("encoding YAML: {e}"))),
273 Format::Ndjson => {
274 let mut out = String::new();
275 for object in &objects {
276 out.push_str(
277 &serde_json::to_string(object).map_err(|e| {
278 Error::new(ErrorKind::Error, format!("encoding NDJSON: {e}"))
279 })?,
280 );
281 out.push('\n');
282 }
283 Ok(out)
284 }
285 }
286}
287
288pub(crate) fn encode_list_file(list: &ExceptionList, format: Format) -> Result<String> {
291 match format {
292 Format::Yaml => serde_yaml_ng::to_string(list.as_map())
293 .map_err(|e| Error::new(ErrorKind::Error, format!("encoding YAML: {e}"))),
294 Format::Ndjson => Ok(format!(
295 "{}\n",
296 serde_json::to_string(list.as_map())
297 .map_err(|e| Error::new(ErrorKind::Error, format!("encoding NDJSON: {e}")))?
298 )),
299 }
300}
301
302fn decode_list_file(body: &str, format: Format) -> Result<ExceptionList> {
303 let value = match format {
304 Format::Yaml => serde_yaml_ng::from_str(body)
305 .map_err(|e| Error::new(ErrorKind::Error, format!("parsing exception list: {e}")))?,
306 Format::Ndjson => {
307 let mut lines = body.lines().filter(|line| !line.trim().is_empty());
308 let line = lines
309 .next()
310 .ok_or_else(|| Error::new(ErrorKind::Error, "empty exception list file"))?;
311 if lines.next().is_some() {
312 return Err(Error::new(
313 ErrorKind::Error,
314 "an exception mirror file must contain exactly one nonblank NDJSON object",
315 ));
316 }
317 serde_json::from_str(line.trim())
318 .map_err(|e| Error::new(ErrorKind::Error, format!("parsing exception list: {e}")))?
319 }
320 };
321 ExceptionList::from_value(value)
322}
323
324fn split_items(list: &mut ExceptionList) -> Result<Vec<ExceptionItem>> {
327 let items = match list.as_map_mut().remove("items") {
328 None => return Ok(Vec::new()),
329 Some(Value::Array(items)) => items,
330 Some(_) => {
331 return Err(Error::new(
332 ErrorKind::Error,
333 "exception list field items must be an array",
334 ));
335 }
336 };
337 let list_id = list.list_id()?.to_string();
338 let namespace = list.namespace_type().to_string();
339 items
340 .into_iter()
341 .map(|value| {
342 let mut item = ExceptionItem::from_value(value)?;
343 item.as_map_mut()
348 .insert("list_id".into(), Value::String(list_id.clone()));
349 item.as_map_mut()
350 .insert("namespace_type".into(), Value::String(namespace.clone()));
351 Ok(item)
352 })
353 .collect()
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
365 fn mirror_entry_path_reports_an_entry_read_error_naming_the_directory() {
366 let dir = Path::new("/mirror");
367 let err = mirror_entry_path(
368 dir,
369 Err(std::io::Error::new(
370 std::io::ErrorKind::PermissionDenied,
371 "denied",
372 )),
373 )
374 .unwrap_err();
375 assert_eq!(err.kind, ErrorKind::Error);
376 assert!(err.message.contains("/mirror"), "{}", err.message);
377 }
378}