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 let mut mirror = Mirror {
20 rules: Vec::new(),
21 lists: Vec::new(),
22 items: Vec::new(),
23 };
24
25 let rules_path = super::rules_dir(dir);
26 if let Some(paths) = mirror_root_files(&rules_path)? {
27 for path in paths {
28 let body = read_regular_file(&path)?;
29 let (mut rules, lists, items) = decode_rule_file(&body, Format::from_path(&path))?;
30 mirror.rules.append(&mut rules);
31 for mut list in lists {
32 let list_items = split_items(&mut list)?;
33 mirror.lists.push(list);
34 mirror.items.extend(list_items);
35 }
36 for item in &items {
37 validate_top_level_item(item)?;
38 }
39 mirror.items.extend(items);
40 }
41 }
42
43 let lists_path = super::exceptions_dir(dir);
44 if let Some(paths) = mirror_root_files(&lists_path)? {
45 for path in paths {
46 let body = read_regular_file(&path)?;
47 let mut list = decode_list_file(&body, Format::from_path(&path))?;
48 let items = split_items(&mut list)?;
49 mirror.lists.push(list);
50 mirror.items.extend(items);
51 }
52 }
53
54 normalize::sort_rules(&mut mirror.rules);
55 normalize::sort_lists(&mut mirror.lists);
56 normalize::sort_items(&mut mirror.items);
57 Ok(mirror)
58}
59
60fn validate_top_level_item(item: &ExceptionItem) -> Result<()> {
61 let item_id = item.item_id()?;
62 if item_id.is_empty() {
63 return Err(Error::new(
64 ErrorKind::Error,
65 "exception item field item_id must be a non-empty string",
66 ));
67 }
68 let list_id = item.list_id()?;
69 if list_id.is_empty() {
70 return Err(Error::new(
71 ErrorKind::Error,
72 "exception item field list_id must be a non-empty string",
73 ));
74 }
75 match item.as_map().get("namespace_type") {
76 None => Ok(()),
77 Some(Value::String(value)) if !value.is_empty() => Ok(()),
78 Some(_) => Err(Error::new(
79 ErrorKind::Error,
80 "exception item field namespace_type must be a non-empty string",
81 )),
82 }
83}
84
85pub fn read_local(dir: &Path) -> Result<Vec<Rule>> {
87 Ok(read_mirror(dir)?.rules)
88}
89
90fn mirror_root_files(dir: &Path) -> Result<Option<Vec<PathBuf>>> {
100 match std::fs::symlink_metadata(dir) {
101 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
102 Err(e) => Err(Error::new(
103 ErrorKind::Error,
104 format!("reading {}: {e}", dir.display()),
105 )),
106 Ok(metadata) => {
107 if metadata.file_type().is_symlink() || !metadata.is_dir() {
108 return Err(Error::new(
109 ErrorKind::Error,
110 format!("mirror directory {} is not a real directory", dir.display()),
111 ));
112 }
113 Ok(Some(mirror_files(dir)?))
114 }
115 }
116}
117
118fn mirror_files(dir: &Path) -> Result<Vec<PathBuf>> {
120 let mut out = Vec::new();
121 for entry in std::fs::read_dir(dir)
122 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", dir.display())))?
123 {
124 if let Some(path) = mirror_entry_path(dir, entry)? {
125 out.push(path);
126 }
127 }
128 out.sort();
129 Ok(out)
130}
131
132fn mirror_entry_path(
136 dir: &Path,
137 entry: std::io::Result<std::fs::DirEntry>,
138) -> Result<Option<PathBuf>> {
139 let entry = entry
140 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", dir.display())))?;
141 let path = entry.path();
142 if !super::is_rule_file(&path) {
143 return Ok(None);
144 }
145 let file_type = entry
148 .file_type()
149 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display())))?;
150 if !file_type.is_file() {
151 return Err(Error::new(
152 ErrorKind::Error,
153 format!("mirror entry {} is not a regular file", path.display()),
154 ));
155 }
156 Ok(Some(path))
157}
158
159#[cfg(unix)]
168fn read_regular_file(path: &Path) -> Result<String> {
169 use std::io::Read;
170 use std::os::unix::fs::OpenOptionsExt;
171 let mut file = std::fs::OpenOptions::new()
172 .read(true)
173 .custom_flags(libc::O_NOFOLLOW)
174 .open(path)
175 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display())))?;
176 let mut body = String::new();
177 file.read_to_string(&mut body)
178 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display())))?;
179 Ok(body)
180}
181
182#[cfg(not(unix))]
183fn read_regular_file(path: &Path) -> Result<String> {
184 let metadata = std::fs::symlink_metadata(path)
185 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display())))?;
186 if metadata.file_type().is_symlink() {
187 return Err(Error::new(
188 ErrorKind::Error,
189 format!("mirror entry {} is not a regular file", path.display()),
190 ));
191 }
192 std::fs::read_to_string(path)
193 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display())))
194}
195
196fn decode_rule_file(
204 body: &str,
205 format: Format,
206) -> Result<(Vec<Rule>, Vec<ExceptionList>, Vec<ExceptionItem>)> {
207 match format {
208 Format::Ndjson => {
209 let bundle = crate::codec::decode_bundle(body)?;
210 Ok((bundle.rules, bundle.lists, bundle.items))
211 }
212 Format::Yaml => {
213 let values: Vec<Value> = serde_yaml_ng::from_str(body)
214 .map_err(|e| Error::new(ErrorKind::Error, format!("parsing YAML: {e}")))?;
215 let mut rules = Vec::new();
216 let mut lists = Vec::new();
217 let mut items = Vec::new();
218 for value in values {
219 if value.get("rule_id").is_some() {
223 rules.push(Rule::from_value(value)?);
224 } else if value.get("item_id").is_some() {
225 items.push(ExceptionItem::from_value(value)?);
226 } else if value.get("list_id").is_some() {
227 lists.push(ExceptionList::from_value(value)?);
228 } else {
229 return Err(Error::new(
230 ErrorKind::Error,
231 "a mirror file entry has neither rule_id, item_id, nor list_id",
232 ));
233 }
234 }
235 Ok((rules, lists, items))
236 }
237 }
238}
239
240pub(crate) fn encode_rule_file(
243 rule: &Rule,
244 inline_lists: &[ExceptionList],
245 format: Format,
246) -> Result<String> {
247 let mut objects = Vec::with_capacity(1 + inline_lists.len());
248 objects.push(rule.clone().into_value());
249 for list in inline_lists {
250 objects.push(list.clone().into_value());
251 }
252 match format {
253 Format::Yaml => serde_yaml_ng::to_string(&objects)
254 .map_err(|e| Error::new(ErrorKind::Error, format!("encoding YAML: {e}"))),
255 Format::Ndjson => {
256 let mut out = String::new();
257 for object in &objects {
258 out.push_str(
259 &serde_json::to_string(object).map_err(|e| {
260 Error::new(ErrorKind::Error, format!("encoding NDJSON: {e}"))
261 })?,
262 );
263 out.push('\n');
264 }
265 Ok(out)
266 }
267 }
268}
269
270pub(crate) fn encode_list_file(list: &ExceptionList, format: Format) -> Result<String> {
273 match format {
274 Format::Yaml => serde_yaml_ng::to_string(list.as_map())
275 .map_err(|e| Error::new(ErrorKind::Error, format!("encoding YAML: {e}"))),
276 Format::Ndjson => Ok(format!(
277 "{}\n",
278 serde_json::to_string(list.as_map())
279 .map_err(|e| Error::new(ErrorKind::Error, format!("encoding NDJSON: {e}")))?
280 )),
281 }
282}
283
284fn decode_list_file(body: &str, format: Format) -> Result<ExceptionList> {
285 let value = match format {
286 Format::Yaml => serde_yaml_ng::from_str(body)
287 .map_err(|e| Error::new(ErrorKind::Error, format!("parsing exception list: {e}")))?,
288 Format::Ndjson => {
289 let mut lines = body.lines().filter(|line| !line.trim().is_empty());
290 let line = lines
291 .next()
292 .ok_or_else(|| Error::new(ErrorKind::Error, "empty exception list file"))?;
293 if lines.next().is_some() {
294 return Err(Error::new(
295 ErrorKind::Error,
296 "an exception mirror file must contain exactly one nonblank NDJSON object",
297 ));
298 }
299 serde_json::from_str(line.trim())
300 .map_err(|e| Error::new(ErrorKind::Error, format!("parsing exception list: {e}")))?
301 }
302 };
303 ExceptionList::from_value(value)
304}
305
306fn split_items(list: &mut ExceptionList) -> Result<Vec<ExceptionItem>> {
309 let items = match list.as_map_mut().remove("items") {
310 None => return Ok(Vec::new()),
311 Some(Value::Array(items)) => items,
312 Some(_) => {
313 return Err(Error::new(
314 ErrorKind::Error,
315 "exception list field items must be an array",
316 ));
317 }
318 };
319 let list_id = list.list_id()?.to_string();
320 let namespace = list.namespace_type().to_string();
321 items
322 .into_iter()
323 .map(|value| {
324 let mut item = ExceptionItem::from_value(value)?;
325 item.as_map_mut()
330 .insert("list_id".into(), Value::String(list_id.clone()));
331 item.as_map_mut()
332 .insert("namespace_type".into(), Value::String(namespace.clone()));
333 Ok(item)
334 })
335 .collect()
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
347 fn mirror_entry_path_reports_an_entry_read_error_naming_the_directory() {
348 let dir = Path::new("/mirror");
349 let err = mirror_entry_path(
350 dir,
351 Err(std::io::Error::new(
352 std::io::ErrorKind::PermissionDenied,
353 "denied",
354 )),
355 )
356 .unwrap_err();
357 assert_eq!(err.kind, ErrorKind::Error);
358 assert!(err.message.contains("/mirror"), "{}", err.message);
359 }
360}