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, which populate [`AgentPolicy::package_policies`].
264 /// Agent counts are opt-in through [`FindAgentPolicies::with_agent_count`].
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 )
275 }
276
277 pub fn get_agent_policy(&self, id: &str) -> GetAgentPolicy<'a> {
278 GetAgentPolicy(self.0.request(
279 Method::GET,
280 Scope::Space,
281 &["api", "fleet", "agent_policies", id],
282 ))
283 }
284
285 /// Creates a policy from a [`NewAgentPolicy`] or equivalent JSON.
286 pub fn create_agent_policy<B: Serialize + ?Sized>(&self, policy: &B) -> CreateAgentPolicy<'a> {
287 CreateAgentPolicy(
288 self.0
289 .request(
290 Method::POST,
291 Scope::Space,
292 &["api", "fleet", "agent_policies"],
293 )
294 .json(policy),
295 )
296 }
297
298 /// Updates submitted attributes and increments the policy's revision.
299 /// Name and namespace are required. Omitting `inactivity_timeout` resets
300 /// it to the server's default; prefer [`edit_agent_policy`](Self::edit_agent_policy)
301 /// with a retrieved policy's edit to retain it.
302 ///
303 /// The body must be a JSON object. Its optional `id` must be a string
304 /// matching the path argument; conflicts fail locally with
305 /// [`Error::InvalidRequest`]. This generic body does not bind an edit's
306 /// captured ID or project writable fields; use `edit_agent_policy` for that.
307 pub fn update_agent_policy<B: Serialize + ?Sized>(
308 &self,
309 id: &str,
310 policy: &B,
311 ) -> UpdateAgentPolicy<'a> {
312 UpdateAgentPolicy(
313 self.0
314 .request(
315 Method::PUT,
316 Scope::Space,
317 &["api", "fleet", "agent_policies", id],
318 )
319 .json(policy)
320 .check_body_id(id, false),
321 )
322 }
323
324 /// Applies changes bound to a retrieved policy's ID, retaining its timeout.
325 /// Optional settings are changed only when their setter was called.
326 pub fn edit_agent_policy(&self, edit: &AgentPolicyEdit) -> UpdateAgentPolicy<'a> {
327 self.update_agent_policy(&edit.id, edit)
328 }
329
330 pub fn delete_agent_policy(&self, id: &str) -> DeleteAgentPolicy<'a> {
331 DeleteAgentPolicy(
332 self.0
333 .request(
334 Method::POST,
335 Scope::Space,
336 &["api", "fleet", "agent_policies", "delete"],
337 )
338 .nonempty("agentPolicyId", id)
339 .field("agentPolicyId", id),
340 )
341 }
342
343 pub fn copy_agent_policy(&self, id: &str, name: &str) -> CopyAgentPolicy<'a> {
344 CopyAgentPolicy(
345 self.0
346 .request(
347 Method::POST,
348 Scope::Space,
349 &["api", "fleet", "agent_policies", id, "copy"],
350 )
351 .field("name", name),
352 )
353 }
354
355 /// The agent configuration as YAML.
356 pub fn download_agent_policy(&self, id: &str) -> DownloadAgentPolicy<'a> {
357 DownloadAgentPolicy(self.0.request(
358 Method::GET,
359 Scope::Space,
360 &["api", "fleet", "agent_policies", id, "download"],
361 ))
362 }
363}
364
365endpoint! {
366 /// `GET /api/fleet/agent_policies`
367 FindAgentPolicies => FleetPage<AgentPolicy>
368}
369
370impl FindAgentPolicies<'_> {
371 /// Populates package policies. Enabled by default.
372 pub fn full(self, full: bool) -> Self {
373 Self(self.0.param("full", full))
374 }
375
376 /// Populates [`AgentPolicy::agents`]. Kibana 8.19 rejects this parameter,
377 /// including when it is false.
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>);