Skip to main content

elasticctl_api/
prebuilt.rs

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}