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