1use crate::ops::MutationPlan;
9use crate::rules::{self, RuleFilter, RuleSource};
10use elasticctl_core::{Error, ErrorKind, Feature, Result, Transport};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14const PREPACKAGED_STATUS: &str = "/api/detection_engine/rules/prepackaged/_status";
15const PREPACKAGED: &str = "/api/detection_engine/rules/prepackaged";
16
17#[derive(Deserialize)]
18struct StatusWire {
19 rules_installed: u64,
20 rules_custom_installed: u64,
21 rules_not_installed: u64,
22 rules_not_updated: u64,
23 timelines_installed: u64,
24 timelines_not_installed: u64,
25 timelines_not_updated: u64,
26}
27
28#[derive(Deserialize)]
29struct InstallOutcomeWire {
30 rules_installed: u64,
31 rules_updated: u64,
32 timelines_installed: u64,
33 timelines_updated: u64,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize)]
38pub struct PrebuiltStatus {
39 pub installed: u64,
40 pub not_installed: u64,
41 pub not_updated: u64,
42 pub custom_installed: u64,
43 pub customized: u64,
45 pub timelines_installed: u64,
46 pub timelines_not_installed: u64,
47 pub timelines_not_updated: u64,
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize)]
52pub struct PrebuiltInstallOutcome {
53 pub applied: bool,
54 pub rules_installed: u64,
55 pub rules_updated: u64,
56 pub timelines_installed: u64,
57 pub timelines_updated: u64,
58}
59
60pub async fn status(t: &Transport) -> Result<PrebuiltStatus> {
61 t.require_feature(Feature::PrebuiltRules).await?;
62 let body = t.get(PREPACKAGED_STATUS).await?;
63 let mut status = decode_status(body, 0)?;
64 status.customized = customized_count(t).await?;
65 Ok(status)
66}
67
68fn decode_status(body: Value, customized: u64) -> Result<PrebuiltStatus> {
69 validate_counters(
70 &body,
71 PREPACKAGED_STATUS,
72 &[
73 "rules_installed",
74 "rules_custom_installed",
75 "rules_not_installed",
76 "rules_not_updated",
77 "timelines_installed",
78 "timelines_not_installed",
79 "timelines_not_updated",
80 ],
81 )?;
82 let body: StatusWire = serde_json::from_value(body)
83 .map_err(|error| response_decode_error(PREPACKAGED_STATUS, error))?;
84 Ok(PrebuiltStatus {
85 installed: body.rules_installed,
86 not_installed: body.rules_not_installed,
87 not_updated: body.rules_not_updated,
88 custom_installed: body.rules_custom_installed,
89 customized,
90 timelines_installed: body.timelines_installed,
91 timelines_not_installed: body.timelines_not_installed,
92 timelines_not_updated: body.timelines_not_updated,
93 })
94}
95
96async fn customized_count(t: &Transport) -> Result<u64> {
101 let filter = RuleFilter {
102 source: RuleSource::Customized,
103 ..Default::default()
104 };
105 let (_, total) = rules::find_page(t, &filter, 1, 1).await?;
106 Ok(total)
107}
108
109pub async fn plan_install(t: &Transport) -> Result<(MutationPlan, PrebuiltStatus)> {
110 let s = status(t).await?;
111
112 let plan = MutationPlan {
118 preview_action: format!(
119 "Install {} missing and update {} outdated prebuilt rule(s)",
120 s.not_installed, s.not_updated
121 ),
122 preview_details: vec![
123 format!("{} missing rule(s) to install", s.not_installed),
124 format!("{} outdated rule(s) to update", s.not_updated),
125 ],
126 targets: Vec::new(),
128 };
129
130 Ok((plan, s))
131}
132
133pub async fn apply_install(t: &Transport) -> Result<PrebuiltInstallOutcome> {
134 t.require_feature(Feature::PrebuiltRules).await?;
135 let body = t.put(PREPACKAGED, &Value::Null).await?;
138 decode_install_outcome(body)
139}
140
141fn decode_install_outcome(body: Value) -> Result<PrebuiltInstallOutcome> {
142 validate_counters(
143 &body,
144 PREPACKAGED,
145 &[
146 "rules_installed",
147 "rules_updated",
148 "timelines_installed",
149 "timelines_updated",
150 ],
151 )?;
152 let body: InstallOutcomeWire =
153 serde_json::from_value(body).map_err(|error| response_decode_error(PREPACKAGED, error))?;
154 Ok(PrebuiltInstallOutcome {
155 applied: true,
156 rules_installed: body.rules_installed,
157 rules_updated: body.rules_updated,
158 timelines_installed: body.timelines_installed,
159 timelines_updated: body.timelines_updated,
160 })
161}
162
163fn response_decode_error(endpoint: &str, error: serde_json::Error) -> Error {
164 Error::new(
165 ErrorKind::Http,
166 format!("invalid prebuilt response from {endpoint}: {error}"),
167 )
168}
169
170fn validate_counters(body: &Value, endpoint: &str, fields: &[&str]) -> Result<()> {
171 let object = body.as_object().ok_or_else(|| {
172 Error::new(
173 ErrorKind::Http,
174 format!("invalid prebuilt response from {endpoint}: expected an object"),
175 )
176 })?;
177 for field in fields {
178 if object.get(*field).and_then(Value::as_u64).is_none() {
179 return Err(Error::new(
180 ErrorKind::Http,
181 format!(
182 "invalid prebuilt response from {endpoint}: field `{field}` must be a non-negative integer"
183 ),
184 ));
185 }
186 }
187 Ok(())
188}