Skip to main content

security_client_rs/fleet/
package_policies.rs

1//! Package policies: an integration's configuration, assigned to agent policies.
2use std::{collections::BTreeMap, fmt};
3
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6
7use super::{Fleet, FleetPage, Item};
8use crate::{Scope, http::Method, pagination::paginated, request::endpoint};
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
11#[non_exhaustive]
12pub struct PackageRef {
13    pub name: String,
14    pub version: String,
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub title: Option<String>,
17}
18
19impl PackageRef {
20    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
21        Self {
22            name: name.into(),
23            version: version.into(),
24            title: None,
25        }
26    }
27}
28
29/// A package policy as Kibana returns it. Reads use Fleet's full format;
30/// creation and simplified replacement responses use the simplified format.
31///
32/// `Debug` output omits inputs, variables and other fields, which can hold credentials.
33#[derive(Clone, Deserialize, Serialize)]
34#[non_exhaustive]
35pub struct PackagePolicy {
36    pub id: String,
37    pub name: String,
38    pub namespace: String,
39    #[serde(default)]
40    pub description: Option<String>,
41    #[serde(default)]
42    pub enabled: Option<bool>,
43    /// Configuration revision, distinct from the opaque concurrency [`version`](Self::version).
44    pub revision: u64,
45    pub package: PackageRef,
46    #[serde(default)]
47    pub policy_ids: Vec<String>,
48    /// Full-format inputs are an array; simplified inputs are an object keyed by input name.
49    #[serde(default)]
50    pub inputs: Value,
51    /// Package-level variables.
52    #[serde(default)]
53    pub vars: Option<Value>,
54    /// Opaque concurrency token, sent back by [`PackagePolicy::edit`].
55    #[serde(default)]
56    pub version: Option<String>,
57    #[serde(flatten)]
58    pub extra: Map<String, Value>,
59}
60
61impl PackagePolicy {
62    /// A replacement for this policy in the full format. It keeps the current
63    /// inputs, variables and assignments until changed, and is bound to the
64    /// policy's ID and concurrency token.
65    /// If the response has simplified inputs, fetch the full policy with
66    /// [`Fleet::get_package_policy`] before editing it. Sending an edit requires
67    /// full inputs and a nonempty concurrency token; construction does not check
68    /// them. Response-only `compiled_input` fields are removed from the inputs.
69    ///
70    /// ```
71    /// use security_client_rs::fleet::PackagePolicy;
72    /// use serde_json::{from_value, json, to_value};
73    ///
74    /// let policy: PackagePolicy = from_value(json!({
75    ///     "id": "integration-a", "name": "system-1", "namespace": "default",
76    ///     "revision": 2, "version": "opaque-token", "policy_ids": ["policy-a"],
77    ///     "package": {"name": "system", "version": "2.24.0"},
78    ///     "inputs": [{"type": "logfile", "enabled": true, "streams": [],
79    ///                 "compiled_input": {"type": "logfile"}}]
80    /// }))?;
81    /// let body = to_value(policy.edit().name("SOC system"))?;
82    /// assert_eq!(body["version"], "opaque-token");
83    /// assert_eq!(body["policy_ids"], json!(["policy-a"]));
84    /// assert_eq!(body["inputs"][0]["enabled"], true);
85    /// assert!(body["inputs"][0].get("compiled_input").is_none());
86    /// assert!(body.get("id").is_none()); // The edit binds the URL's ID instead.
87    /// # Ok::<(), serde_json::Error>(())
88    /// ```
89    pub fn edit(&self) -> PackagePolicyEdit {
90        PackagePolicyEdit {
91            id: self.id.clone(),
92            name: self.name.clone(),
93            namespace: self.namespace.clone(),
94            description: self.description.clone(),
95            enabled: self.enabled,
96            package: self.package.clone(),
97            policy_ids: self.policy_ids.clone(),
98            inputs: editable_inputs(self.inputs.clone()),
99            vars: self.vars.clone(),
100            version: self.version.clone(),
101            full_format: self.inputs.is_array(),
102        }
103    }
104}
105
106fn editable_inputs(mut inputs: Value) -> Value {
107    if let Some(inputs) = inputs.as_array_mut() {
108        for input in inputs {
109            if let Some(input) = input.as_object_mut() {
110                input.remove("compiled_input");
111            }
112        }
113    }
114    inputs
115}
116
117impl fmt::Debug for PackagePolicy {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        f.debug_struct("PackagePolicy")
120            .field("id", &self.id)
121            .field("name", &self.name)
122            .field("namespace", &self.namespace)
123            .field("enabled", &self.enabled)
124            .field("revision", &self.revision)
125            .field("package", &self.package)
126            .field("policy_ids", &self.policy_ids)
127            .finish_non_exhaustive()
128    }
129}
130
131/// Changes to a retrieved package policy, created by [`PackagePolicy::edit`]
132/// and sent with [`Fleet::update_package_policy`] in the full format.
133///
134/// A policy changed since it was read fails with HTTP 409; read it again and
135/// reapply the change. `Debug` output omits inputs and variables.
136#[derive(Clone, Serialize)]
137pub struct PackagePolicyEdit {
138    #[serde(skip)]
139    id: String,
140    name: String,
141    namespace: String,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    description: Option<String>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    enabled: Option<bool>,
146    package: PackageRef,
147    policy_ids: Vec<String>,
148    #[serde(skip_serializing_if = "Value::is_null")]
149    inputs: Value,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    vars: Option<Value>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    version: Option<String>,
154    #[serde(skip)]
155    full_format: bool,
156}
157
158impl PackagePolicyEdit {
159    pub fn name(mut self, name: impl Into<String>) -> Self {
160        self.name = name.into();
161        self
162    }
163
164    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
165        self.namespace = namespace.into();
166        self
167    }
168
169    pub fn description(mut self, description: impl Into<String>) -> Self {
170        self.description = Some(description.into());
171        self
172    }
173
174    pub fn enabled(mut self, enabled: bool) -> Self {
175        self.enabled = Some(enabled);
176        self
177    }
178
179    /// Replaces the agent policies the package policy is assigned to.
180    pub fn policy_ids<I: IntoIterator<Item = S>, S: Into<String>>(mut self, ids: I) -> Self {
181        self.policy_ids = ids.into_iter().map(Into::into).collect();
182        self
183    }
184
185    /// Replaces the inputs with a full-format array, typically a modified copy
186    /// of [`PackagePolicy::inputs`].
187    pub fn inputs(mut self, inputs: Value) -> Self {
188        self.inputs = editable_inputs(inputs);
189        self
190    }
191
192    /// Replaces the package-level variables in the full format.
193    pub fn vars(mut self, vars: Value) -> Self {
194        self.vars = Some(vars);
195        self
196    }
197}
198
199impl fmt::Debug for PackagePolicyEdit {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        f.debug_struct("PackagePolicyEdit")
202            .field("id", &self.id)
203            .field("name", &self.name)
204            .field("namespace", &self.namespace)
205            .field("policy_ids", &self.policy_ids)
206            .finish_non_exhaustive()
207    }
208}
209
210/// A replacement for [`Fleet::update_package_policy`].
211#[derive(Clone, Copy, Debug)]
212#[non_exhaustive]
213pub enum PackagePolicyUpdate<'p> {
214    /// A retrieved policy with changes, in the full format. Created from a
215    /// [`PackagePolicyEdit`].
216    Edit(&'p PackagePolicyEdit),
217    /// A new simplified definition for policy `id`, from
218    /// [`NewPackagePolicy::replacing`]. Inputs it does not configure revert to
219    /// package defaults, and it carries no concurrency token.
220    Replace {
221        id: &'p str,
222        policy: &'p NewPackagePolicy,
223    },
224}
225
226impl<'p> From<&'p PackagePolicyEdit> for PackagePolicyUpdate<'p> {
227    fn from(edit: &'p PackagePolicyEdit) -> Self {
228        Self::Edit(edit)
229    }
230}
231
232/// Lists variable names without values, which can be credentials.
233struct VarNames<'v>(&'v BTreeMap<String, Value>);
234
235impl fmt::Debug for VarNames<'_> {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        f.debug_set().entries(self.0.keys()).finish()
238    }
239}
240
241/// One input in Fleet's simplified package-policy format. Input, stream and
242/// variable names depend on the package. `Debug` output omits variable values.
243#[derive(Clone, Default, Serialize)]
244pub struct PolicyInput {
245    #[serde(skip_serializing_if = "Option::is_none")]
246    enabled: Option<bool>,
247    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
248    vars: BTreeMap<String, Value>,
249    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
250    streams: BTreeMap<String, PolicyStream>,
251}
252
253impl PolicyInput {
254    pub fn new() -> Self {
255        Self::default()
256    }
257
258    pub fn enabled(mut self, enabled: bool) -> Self {
259        self.enabled = Some(enabled);
260        self
261    }
262
263    pub fn var(mut self, name: impl Into<String>, value: Value) -> Self {
264        self.vars.insert(name.into(), value);
265        self
266    }
267
268    pub fn stream(mut self, name: impl Into<String>, stream: PolicyStream) -> Self {
269        self.streams.insert(name.into(), stream);
270        self
271    }
272}
273
274impl fmt::Debug for PolicyInput {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        f.debug_struct("PolicyInput")
277            .field("enabled", &self.enabled)
278            .field("vars", &VarNames(&self.vars))
279            .field("streams", &self.streams)
280            .finish()
281    }
282}
283
284/// A stream within a [`PolicyInput`]. `Debug` output omits variable values.
285#[derive(Clone, Default, Serialize)]
286pub struct PolicyStream {
287    #[serde(skip_serializing_if = "Option::is_none")]
288    enabled: Option<bool>,
289    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
290    vars: BTreeMap<String, Value>,
291}
292
293impl PolicyStream {
294    pub fn new() -> Self {
295        Self::default()
296    }
297
298    pub fn enabled(mut self, enabled: bool) -> Self {
299        self.enabled = Some(enabled);
300        self
301    }
302
303    pub fn var(mut self, name: impl Into<String>, value: Value) -> Self {
304        self.vars.insert(name.into(), value);
305        self
306    }
307}
308
309impl fmt::Debug for PolicyStream {
310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311        f.debug_struct("PolicyStream")
312            .field("enabled", &self.enabled)
313            .field("vars", &VarNames(&self.vars))
314            .finish()
315    }
316}
317
318/// A package policy in the simplified format, for [`Fleet::create_package_policy`]
319/// and, through [`replacing`](Self::replacing), [`Fleet::update_package_policy`].
320/// Unset inputs use package defaults. `Debug` output omits variable values.
321#[derive(Clone, Debug, Serialize)]
322pub struct NewPackagePolicy {
323    name: String,
324    namespace: String,
325    policy_ids: Vec<String>,
326    package: PackageRef,
327    inputs: BTreeMap<String, PolicyInput>,
328    #[serde(skip_serializing_if = "Option::is_none")]
329    description: Option<String>,
330}
331
332impl NewPackagePolicy {
333    pub fn new(name: impl Into<String>, namespace: impl Into<String>, package: PackageRef) -> Self {
334        Self {
335            name: name.into(),
336            namespace: namespace.into(),
337            policy_ids: Vec::new(),
338            package,
339            inputs: BTreeMap::new(),
340            description: None,
341        }
342    }
343
344    /// Assigns the package policy to an agent policy. Repeat for several policies.
345    pub fn policy_id(mut self, id: impl Into<String>) -> Self {
346        self.policy_ids.push(id.into());
347        self
348    }
349
350    /// Configures an input by name, such as `system-logfile`.
351    pub fn input(mut self, name: impl Into<String>, input: PolicyInput) -> Self {
352        self.inputs.insert(name.into(), input);
353        self
354    }
355
356    pub fn description(mut self, description: impl Into<String>) -> Self {
357        self.description = Some(description.into());
358        self
359    }
360
361    /// Replaces the whole of package policy `id` with this definition. Existing
362    /// configuration this definition does not set reverts to package defaults;
363    /// to change a retrieved policy instead, use [`PackagePolicy::edit`].
364    pub fn replacing<'p>(&'p self, id: &'p str) -> PackagePolicyUpdate<'p> {
365        PackagePolicyUpdate::Replace { id, policy: self }
366    }
367}
368
369impl<'a> Fleet<'a> {
370    pub fn find_package_policies(&self) -> FindPackagePolicies<'a> {
371        FindPackagePolicies(self.0.request(
372            Method::GET,
373            Scope::Space,
374            &["api", "fleet", "package_policies"],
375        ))
376    }
377
378    pub fn get_package_policy(&self, id: &str) -> GetPackagePolicy<'a> {
379        GetPackagePolicy(self.0.request(
380            Method::GET,
381            Scope::Space,
382            &["api", "fleet", "package_policies", id],
383        ))
384    }
385
386    /// Creates a package policy from a [`NewPackagePolicy`] or equivalent JSON
387    /// in the simplified format. The response is also simplified; fetch it with
388    /// [`get_package_policy`](Self::get_package_policy) before editing it.
389    pub fn create_package_policy<B: Serialize + ?Sized>(
390        &self,
391        policy: &B,
392    ) -> CreatePackagePolicy<'a> {
393        CreatePackagePolicy(
394            self.0
395                .request(
396                    Method::POST,
397                    Scope::Space,
398                    &["api", "fleet", "package_policies"],
399                )
400                .param("format", "simplified")
401                .json(policy),
402        )
403    }
404
405    /// Replaces a package policy, either with a [`PackagePolicyEdit`] of a
406    /// retrieved policy, sent in the full format with its concurrency token, or
407    /// with a simplified definition from [`NewPackagePolicy::replacing`].
408    /// Edits with simplified inputs or a missing/empty concurrency token fail
409    /// locally with [`crate::Error::InvalidRequest`].
410    pub fn update_package_policy<'p>(
411        &self,
412        update: impl Into<PackagePolicyUpdate<'p>>,
413    ) -> UpdatePackagePolicy<'a> {
414        let update = update.into();
415        let id = match update {
416            PackagePolicyUpdate::Edit(edit) => edit.id.as_str(),
417            PackagePolicyUpdate::Replace { id, .. } => id,
418        };
419        let request = self.0.request(
420            Method::PUT,
421            Scope::Space,
422            &["api", "fleet", "package_policies", id],
423        );
424        UpdatePackagePolicy(match update {
425            PackagePolicyUpdate::Edit(edit) => {
426                let request = if edit.full_format && edit.inputs.is_array() {
427                    request
428                } else {
429                    request.invalid(
430                        "package-policy edits require full inputs; fetch the policy with get_package_policy before editing it".into(),
431                    )
432                };
433                request
434                    .nonempty("version", edit.version.as_deref().unwrap_or_default())
435                    .json(edit)
436            }
437            PackagePolicyUpdate::Replace { policy, .. } => {
438                request.param("format", "simplified").json(policy)
439            }
440        })
441    }
442
443    pub fn delete_package_policy(&self, id: &str) -> DeletePackagePolicy<'a> {
444        DeletePackagePolicy(self.0.request(
445            Method::DELETE,
446            Scope::Space,
447            &["api", "fleet", "package_policies", id],
448        ))
449    }
450}
451
452endpoint! {
453    /// `GET /api/fleet/package_policies`
454    FindPackagePolicies => FleetPage<PackagePolicy>
455}
456
457endpoint! {
458    /// `GET /api/fleet/package_policies/{packagePolicyId}`
459    GetPackagePolicy => Item<PackagePolicy>
460}
461
462endpoint! {
463    /// `POST /api/fleet/package_policies`
464    CreatePackagePolicy => Item<PackagePolicy>
465}
466
467endpoint! {
468    /// `PUT /api/fleet/package_policies/{packagePolicyId}`
469    UpdatePackagePolicy => Item<PackagePolicy>
470}
471
472endpoint! {
473    /// `DELETE /api/fleet/package_policies/{packagePolicyId}`
474    DeletePackagePolicy => Value
475}
476
477impl DeletePackagePolicy<'_> {
478    /// Deletes even when the package policy is managed.
479    pub fn force(self, force: bool) -> Self {
480        Self(self.0.param("force", force))
481    }
482}
483
484page_setters!(FindPackagePolicies);
485sort_setters!(FindPackagePolicies);
486paginated!(FindPackagePolicies => FleetPage<PackagePolicy>);