elasticctl_api/rules/
preview_execution.rs1use super::BASE;
2use crate::model::Rule;
3use elasticctl_core::{Error, ErrorKind, Result, Transport, urlencode};
4use serde_json::{Value, json};
5
6#[derive(Debug, Clone, Default, PartialEq)]
7pub struct PreviewResult {
8 pub preview_id: Option<String>,
9 pub errors: Vec<String>,
10 pub warnings: Vec<String>,
11}
12
13pub async fn preview(
14 t: &Transport,
15 rule: &Rule,
16 invocation_count: u32,
17 timeframe_end: &str,
18) -> Result<PreviewResult> {
19 let mut body = rule.as_map().clone();
20 for k in [
21 "rule_id",
22 "id",
23 "immutable",
24 "rule_source",
25 "revision",
26 "version",
27 ] {
28 body.remove(k);
29 }
30 body.insert("invocationCount".into(), json!(invocation_count));
31 body.insert("timeframeEnd".into(), json!(timeframe_end));
32
33 let response = t
34 .post(&format!("{BASE}/preview"), Some(&Value::Object(body)))
35 .await?;
36
37 let collect = |key: &str| -> Vec<String> {
38 response["logs"]
39 .as_array()
40 .map(|logs| {
41 logs.iter()
42 .filter_map(|l| l.get(key)?.as_array())
43 .flatten()
44 .filter_map(|v| v.as_str().map(str::to_owned))
45 .collect()
46 })
47 .unwrap_or_default()
48 };
49
50 Ok(PreviewResult {
51 preview_id: response["previewId"].as_str().map(str::to_owned),
52 errors: collect("errors"),
53 warnings: collect("warnings"),
54 })
55}
56
57pub const PREVIEW_ALERTS_INDEX_PREFIX: &str = ".preview.alerts-security.alerts-";
58
59#[derive(Debug, Clone, Default, PartialEq)]
60pub struct PreviewHits {
61 pub total: u64,
62 pub sample: Vec<Value>,
63}
64
65pub async fn preview_hits(
66 t: &Transport,
67 space: &str,
68 preview_id: &str,
69 sample: usize,
70) -> Result<PreviewHits> {
71 let space = if space.is_empty() { "default" } else { space };
72 let index = urlencode(&format!("{PREVIEW_ALERTS_INDEX_PREFIX}{space}"));
73 let body = json!({
74 "size": sample,
75 "track_total_hits": true,
76 "query": {"term": {"kibana.alert.rule.uuid": preview_id}},
77 "sort": [{"@timestamp": {"order": "desc"}}]
78 });
79
80 let response = t
81 .post_absolute_es(&format!("/{index}/_search?ignore_unavailable=true"), &body)
82 .await?;
83
84 decode_preview_hits_checked(&response)
85}
86
87pub fn decode_preview_hits_checked(response: &Value) -> Result<PreviewHits> {
88 let map = response
89 .as_object()
90 .ok_or_else(|| preview_error("response", "must be a JSON object"))?;
91 let hits = map
92 .get("hits")
93 .and_then(Value::as_object)
94 .ok_or_else(|| preview_error("hits", "must be an object"))?;
95 let total = hits
96 .get("total")
97 .and_then(|total| total.get("value"))
98 .and_then(Value::as_u64)
99 .ok_or_else(|| preview_error("hits.total.value", "must be an unsigned integer"))?;
100 let sample = hits
101 .get("hits")
102 .and_then(Value::as_array)
103 .ok_or_else(|| preview_error("hits.hits", "must be an array"))?
104 .iter()
105 .map(|h| {
106 json!({
107 "_id": h.get("_id").cloned().unwrap_or(Value::Null),
108 "_source": h.get("_source").cloned().unwrap_or(Value::Null),
109 })
110 })
111 .collect();
112
113 Ok(PreviewHits { total, sample })
114}
115
116fn preview_error(field: &str, detail: impl std::fmt::Display) -> Error {
117 Error::new(
118 ErrorKind::Http,
119 format!("decoding preview-hits response field {field}: {detail}"),
120 )
121}
122
123pub fn decode_preview_hits(response: &Value) -> PreviewHits {
124 decode_preview_hits_checked(response).unwrap_or_default()
125}