Skip to main content

security_client_rs/fleet/
agent_policies.rs

1//! Agent policies, which group the package policies an agent runs.
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6
7use super::{Fleet, FleetPage, Item};
8use crate::{
9    Error, Result, Scope,
10    http::{Method, Raw},
11    pagination::paginated,
12    request::endpoint,
13};
14
15/// An agent policy returned by Fleet, grouping the integrations assigned to agents.
16///
17/// Use [`Self::edit`] to change a retrieved policy while retaining its inactivity
18/// timeout. `Debug` output counts package policies instead of printing them,
19/// since their variables can hold credentials.
20#[derive(Clone, Deserialize, Serialize)]
21#[non_exhaustive]
22pub struct AgentPolicy {
23    pub id: String,
24    pub name: String,
25    /// Namespace used for the policy's data streams, independent of the Kibana space.
26    pub namespace: String,
27    /// Configuration delivery revision. Fleet does not use it to reject concurrent edits.
28    pub revision: u64,
29    #[serde(default)]
30    pub description: Option<String>,
31    #[serde(default)]
32    pub status: Option<String>,
33    /// Present when listed with agent counts.
34    #[serde(default)]
35    pub agents: Option<u64>,
36    /// Populated when listed with full policies.
37    #[serde(default)]
38    pub package_policies: Vec<Value>,
39    /// Unmodeled response fields. An edit resubmits only the supported writable fields.
40    #[serde(flatten)]
41    pub extra: Map<String, Value>,
42}
43
44impl AgentPolicy {
45    /// Changes to this policy, bound to its ID. Required name and namespace
46    /// start from the retrieved values. The inactivity timeout is retained
47    /// because Kibana resets it to two weeks when it is omitted from an update.
48    /// Other settings are left unchanged unless their setter is called.
49    ///
50    /// Returns [`Error::InvalidRequest`] if `extra["inactivity_timeout"]` is
51    /// missing or is not an unsigned integer. Fetch the policy again rather
52    /// than guessing its timeout.
53    ///
54    /// Fleet agent-policy updates do not enforce optimistic concurrency;
55    /// [`revision`](Self::revision) tracks configuration delivery to agents.
56    ///
57    /// Renaming retains the timeout and leaves other optional settings unset:
58    ///
59    /// ```
60    /// use security_client_rs::fleet::AgentPolicy;
61    /// use serde_json::{from_value, json, to_value};
62    ///
63    /// let policy: AgentPolicy = from_value(json!({
64    ///     "id": "policy-a", "name": "Endpoints", "namespace": "default",
65    ///     "revision": 3, "inactivity_timeout": 3600, "description": "Keep this"
66    /// }))?;
67    /// let body = to_value(policy.edit()?.name("SOC endpoints"))?;
68    /// assert_eq!(body, json!({
69    ///     "name": "SOC endpoints", "namespace": "default", "inactivity_timeout": 3600
70    /// }));
71    /// # Ok::<(), Box<dyn std::error::Error>>(())
72    /// ```
73    ///
74    /// To apply an edit:
75    ///
76    /// ```no_run
77    /// # async fn rename(client: &security_client_rs::Kibana) -> security_client_rs::Result<()> {
78    /// let fleet = client.fleet();
79    /// let policy = fleet.get_agent_policy("policy-id").send().await?.json().await?.item;
80    /// let edit = policy.edit()?.name("SOC endpoints").clear_data_output_id();
81    /// fleet.edit_agent_policy(&edit).send().await?;
82    /// # Ok(())
83    /// # }
84    /// ```
85    pub fn edit(&self) -> Result<AgentPolicyEdit> {
86        let timeout = self
87            .extra
88            .get("inactivity_timeout")
89            .and_then(Value::as_u64)
90            .ok_or_else(|| {
91                Error::InvalidRequest(
92                    "agent policy edit requires a retrieved inactivity_timeout".into(),
93                )
94            })?;
95        Ok(AgentPolicyEdit {
96            id: self.id.clone(),
97            policy: NewAgentPolicy::new(&self.name, &self.namespace).inactivity_timeout(timeout),
98        })
99    }
100}
101
102impl fmt::Debug for AgentPolicy {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        f.debug_struct("AgentPolicy")
105            .field("id", &self.id)
106            .field("name", &self.name)
107            .field("namespace", &self.namespace)
108            .field("revision", &self.revision)
109            .field("status", &self.status)
110            .field("agents", &self.agents)
111            .field("package_policies", &self.package_policies.len())
112            .finish_non_exhaustive()
113    }
114}
115
116/// An agent policy definition for [`Fleet::create_agent_policy`] and
117/// [`Fleet::update_agent_policy`].
118/// For changes to a retrieved policy, prefer [`AgentPolicy::edit`] to retain
119/// its inactivity timeout instead of accepting the server's default.
120/// Optional settings are omitted until their setter is called.
121#[derive(Clone, Debug, Serialize)]
122pub struct NewAgentPolicy {
123    name: String,
124    namespace: String,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    description: Option<String>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    monitoring_enabled: Option<Vec<String>>,
129    /// Omitted, `null` for Fleet's default output, or an output ID.
130    #[allow(clippy::option_option)]
131    #[serde(skip_serializing_if = "Option::is_none")]
132    data_output_id: Option<Option<String>>,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    inactivity_timeout: Option<u64>,
135}
136
137impl NewAgentPolicy {
138    pub fn new(name: impl Into<String>, namespace: impl Into<String>) -> Self {
139        Self {
140            name: name.into(),
141            namespace: namespace.into(),
142            description: None,
143            monitoring_enabled: None,
144            data_output_id: None,
145            inactivity_timeout: None,
146        }
147    }
148
149    pub fn description(mut self, description: impl Into<String>) -> Self {
150        self.description = Some(description.into());
151        self
152    }
153
154    /// Agent self-monitoring to collect: `logs`, `metrics` and `traces`. Empty disables it.
155    pub fn monitoring_enabled<I: IntoIterator<Item = S>, S: Into<String>>(
156        mut self,
157        kinds: I,
158    ) -> Self {
159        self.monitoring_enabled = Some(kinds.into_iter().map(Into::into).collect());
160        self
161    }
162
163    /// Selects a data output explicitly. Without this setter or
164    /// [`clear_data_output_id`](Self::clear_data_output_id), the field is omitted.
165    /// Kibana requires a Platinum license to set a per-policy output.
166    pub fn data_output_id(mut self, id: impl Into<String>) -> Self {
167        self.data_output_id = Some(Some(id.into()));
168        self
169    }
170
171    /// Sends `data_output_id: null` to use Fleet's default output.
172    /// On update, omission keeps the existing selection; null clears it.
173    ///
174    /// ```
175    /// use security_client_rs::fleet::NewAgentPolicy;
176    /// use serde_json::{json, to_value};
177    ///
178    /// let policy = NewAgentPolicy::new("SOC endpoints", "default");
179    /// assert!(to_value(&policy)?.get("data_output_id").is_none());
180    /// let selected = policy.data_output_id("output-id");
181    /// assert_eq!(to_value(&selected)?["data_output_id"], json!("output-id"));
182    /// let cleared = selected.clear_data_output_id();
183    /// assert_eq!(to_value(&cleared)?["data_output_id"], json!(null));
184    /// # Ok::<(), serde_json::Error>(())
185    /// ```
186    pub fn clear_data_output_id(mut self) -> Self {
187        self.data_output_id = Some(None);
188        self
189    }
190
191    /// Seconds without check-in before an agent is considered inactive.
192    pub fn inactivity_timeout(mut self, seconds: u64) -> Self {
193        self.inactivity_timeout = Some(seconds);
194        self
195    }
196}
197
198/// Changes to a retrieved agent policy, created by [`AgentPolicy::edit`] and
199/// sent with [`Fleet::edit_agent_policy`].
200///
201/// The request includes name, namespace and the retained inactivity timeout,
202/// plus explicitly changed settings. It omits response metadata, package
203/// policies and unknown fields. Unset optional fields keep their server values.
204/// Fleet merges submitted attributes and does not check the policy's revision
205/// for concurrent updates.
206#[derive(Clone, Debug, Serialize)]
207pub struct AgentPolicyEdit {
208    #[serde(skip)]
209    id: String,
210    #[serde(flatten)]
211    policy: NewAgentPolicy,
212}
213
214impl AgentPolicyEdit {
215    pub fn name(mut self, name: impl Into<String>) -> Self {
216        self.policy.name = name.into();
217        self
218    }
219
220    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
221        self.policy.namespace = namespace.into();
222        self
223    }
224
225    /// Sets the description. An empty string clears its text.
226    pub fn description(mut self, description: impl Into<String>) -> Self {
227        self.policy = self.policy.description(description);
228        self
229    }
230
231    /// Agent self-monitoring to collect: `logs`, `metrics` and `traces`.
232    /// Empty disables it; leaving this unset retains the existing selection.
233    pub fn monitoring_enabled<I: IntoIterator<Item = S>, S: Into<String>>(
234        mut self,
235        kinds: I,
236    ) -> Self {
237        self.policy = self.policy.monitoring_enabled(kinds);
238        self
239    }
240
241    /// Selects a data output explicitly. Leaving this unset retains the
242    /// existing output selection. Kibana requires a Platinum license to set it.
243    pub fn data_output_id(mut self, id: impl Into<String>) -> Self {
244        self.policy = self.policy.data_output_id(id);
245        self
246    }
247
248    /// Clears the output selection with JSON null, restoring Fleet's default.
249    pub fn clear_data_output_id(mut self) -> Self {
250        self.policy = self.policy.clear_data_output_id();
251        self
252    }
253
254    /// Seconds without check-in before an agent is considered inactive.
255    /// Leaving this unset retains the retrieved timeout.
256    pub fn inactivity_timeout(mut self, seconds: u64) -> Self {
257        self.policy = self.policy.inactivity_timeout(seconds);
258        self
259    }
260}
261
262impl<'a> Fleet<'a> {
263    /// Requests full policies with agent counts, which populate
264    /// [`AgentPolicy::package_policies`] and [`AgentPolicy::agents`].
265    pub fn find_agent_policies(&self) -> FindAgentPolicies<'a> {
266        FindAgentPolicies(
267            self.0
268                .request(
269                    Method::GET,
270                    Scope::Space,
271                    &["api", "fleet", "agent_policies"],
272                )
273                .param("full", true)
274                .param("withAgentCount", true),
275        )
276    }
277
278    pub fn get_agent_policy(&self, id: &str) -> GetAgentPolicy<'a> {
279        GetAgentPolicy(self.0.request(
280            Method::GET,
281            Scope::Space,
282            &["api", "fleet", "agent_policies", id],
283        ))
284    }
285
286    /// Creates a policy from a [`NewAgentPolicy`] or equivalent JSON.
287    pub fn create_agent_policy<B: Serialize + ?Sized>(&self, policy: &B) -> CreateAgentPolicy<'a> {
288        CreateAgentPolicy(
289            self.0
290                .request(
291                    Method::POST,
292                    Scope::Space,
293                    &["api", "fleet", "agent_policies"],
294                )
295                .json(policy),
296        )
297    }
298
299    /// Updates submitted attributes and increments the policy's revision.
300    /// Name and namespace are required. Omitting `inactivity_timeout` resets
301    /// it to the server's default; prefer [`edit_agent_policy`](Self::edit_agent_policy)
302    /// with a retrieved policy's edit to retain it.
303    ///
304    /// The body must be a JSON object. Its optional `id` must be a string
305    /// matching the path argument; conflicts fail locally with
306    /// [`Error::InvalidRequest`]. This generic body does not bind an edit's
307    /// captured ID or project writable fields; use `edit_agent_policy` for that.
308    pub fn update_agent_policy<B: Serialize + ?Sized>(
309        &self,
310        id: &str,
311        policy: &B,
312    ) -> UpdateAgentPolicy<'a> {
313        UpdateAgentPolicy(
314            self.0
315                .request(
316                    Method::PUT,
317                    Scope::Space,
318                    &["api", "fleet", "agent_policies", id],
319                )
320                .json(policy)
321                .check_body_id(id, false),
322        )
323    }
324
325    /// Applies changes bound to a retrieved policy's ID, retaining its timeout.
326    /// Optional settings are changed only when their setter was called.
327    pub fn edit_agent_policy(&self, edit: &AgentPolicyEdit) -> UpdateAgentPolicy<'a> {
328        self.update_agent_policy(&edit.id, edit)
329    }
330
331    pub fn delete_agent_policy(&self, id: &str) -> DeleteAgentPolicy<'a> {
332        DeleteAgentPolicy(
333            self.0
334                .request(
335                    Method::POST,
336                    Scope::Space,
337                    &["api", "fleet", "agent_policies", "delete"],
338                )
339                .nonempty("agentPolicyId", id)
340                .field("agentPolicyId", id),
341        )
342    }
343
344    pub fn copy_agent_policy(&self, id: &str, name: &str) -> CopyAgentPolicy<'a> {
345        CopyAgentPolicy(
346            self.0
347                .request(
348                    Method::POST,
349                    Scope::Space,
350                    &["api", "fleet", "agent_policies", id, "copy"],
351                )
352                .field("name", name),
353        )
354    }
355
356    /// The agent configuration as YAML.
357    pub fn download_agent_policy(&self, id: &str) -> DownloadAgentPolicy<'a> {
358        DownloadAgentPolicy(self.0.request(
359            Method::GET,
360            Scope::Space,
361            &["api", "fleet", "agent_policies", id, "download"],
362        ))
363    }
364}
365
366endpoint! {
367    /// `GET /api/fleet/agent_policies`
368    FindAgentPolicies => FleetPage<AgentPolicy>
369}
370
371impl FindAgentPolicies<'_> {
372    /// Populates package policies. Enabled by default.
373    pub fn full(self, full: bool) -> Self {
374        Self(self.0.param("full", full))
375    }
376
377    /// Populates agent counts. Enabled by default.
378    pub fn with_agent_count(self, enabled: bool) -> Self {
379        Self(self.0.param("withAgentCount", enabled))
380    }
381}
382
383endpoint! {
384    /// `GET /api/fleet/agent_policies/{agentPolicyId}`
385    GetAgentPolicy => Item<AgentPolicy>
386}
387
388endpoint! {
389    /// `POST /api/fleet/agent_policies`
390    CreateAgentPolicy => Item<AgentPolicy>
391}
392
393impl CreateAgentPolicy<'_> {
394    /// Also adds the System integration to the new policy.
395    pub fn sys_monitoring(self, enabled: bool) -> Self {
396        Self(self.0.param("sys_monitoring", enabled))
397    }
398}
399
400endpoint! {
401    /// `PUT /api/fleet/agent_policies/{agentPolicyId}`
402    UpdateAgentPolicy => Item<AgentPolicy>
403}
404
405endpoint! {
406    /// `POST /api/fleet/agent_policies/delete`
407    DeleteAgentPolicy => Value
408}
409
410impl DeleteAgentPolicy<'_> {
411    /// Bypasses server validation checks that would otherwise prevent deletion.
412    pub fn force(self, force: bool) -> Self {
413        Self(self.0.field("force", force))
414    }
415}
416
417endpoint! {
418    /// `POST /api/fleet/agent_policies/{agentPolicyId}/copy`
419    CopyAgentPolicy => Item<AgentPolicy>
420}
421
422impl CopyAgentPolicy<'_> {
423    pub fn description(self, description: &str) -> Self {
424        Self(self.0.field("description", description))
425    }
426}
427
428endpoint! {
429    /// `GET /api/fleet/agent_policies/{agentPolicyId}/download`
430    DownloadAgentPolicy => Raw
431}
432
433impl DownloadAgentPolicy<'_> {
434    /// Produces a configuration for a standalone, non-Fleet agent.
435    pub fn standalone(self, enabled: bool) -> Self {
436        Self(self.0.param("standalone", enabled))
437    }
438
439    /// Produces a Kubernetes manifest.
440    pub fn kubernetes(self, enabled: bool) -> Self {
441        Self(self.0.param("kubernetes", enabled))
442    }
443}
444
445page_setters!(FindAgentPolicies);
446sort_setters!(FindAgentPolicies);
447paginated!(FindAgentPolicies => FleetPage<AgentPolicy>);