elasticctl-api 0.7.4

Typed detection-rule model and endpoint wrappers for Elastic Security.
Documentation
//! Agent policy artifact validation and export.

use super::{
    AgentPolicySpec, AgentPolicySummary, BTreeMap, BTreeSet, ContentFormat, Error, ErrorKind,
    ExportOutcome, Path, Result, Transport, collect, content_codec, is_platform_owned,
    live_from_item, read_live, resolve_item,
};

/// Read, decode, validate, and sort a portable artifact.
pub fn validate(path: &Path) -> Result<Vec<AgentPolicySpec>> {
    let body = std::fs::read_to_string(path).map_err(|error| {
        Error::new(
            ErrorKind::Error,
            format!("reading {}: {error}", path.display()),
        )
    })?;
    let mut specs = content_codec::decode_sequence::<AgentPolicySpec>(
        &body,
        ContentFormat::from_path(path),
        "agent policy",
    )?;
    let mut seen_ids = BTreeSet::new();
    let mut duplicate_ids = BTreeSet::new();
    let mut seen_names = BTreeSet::new();
    let mut duplicate_names = BTreeSet::new();
    for spec in &specs {
        if !seen_ids.insert(spec.id.as_str()) {
            duplicate_ids.insert(spec.id.as_str());
        }
        if !seen_names.insert(spec.name.as_str()) {
            duplicate_names.insert(spec.name.as_str());
        }
    }
    if !duplicate_ids.is_empty() {
        return Err(Error::new(
            ErrorKind::Error,
            format!(
                "duplicate agent policy ids: {}",
                duplicate_ids.into_iter().collect::<Vec<_>>().join(", ")
            ),
        ));
    }
    if !duplicate_names.is_empty() {
        return Err(Error::new(
            ErrorKind::Error,
            format!(
                "duplicate agent policy names: {}",
                duplicate_names.into_iter().collect::<Vec<_>>().join(", ")
            ),
        ));
    }
    specs.sort_by(|left, right| left.id.cmp(&right.id));
    Ok(specs)
}

/// Export selected policies, or every custom policy, as a portable artifact.
pub async fn export(
    transport: &Transport,
    selectors: &[String],
    all_custom: bool,
    format: ContentFormat,
) -> Result<ExportOutcome> {
    if selectors.is_empty() && !all_custom {
        return Err(Error::new(
            ErrorKind::Error,
            "agent-policy export needs selectors or --all-custom",
        ));
    }
    if !selectors.is_empty() && all_custom {
        return Err(Error::new(
            ErrorKind::Error,
            "--all-custom cannot be combined with selectors",
        ));
    }
    let mut resolved_items = BTreeMap::new();
    let ids: BTreeSet<String> = if all_custom {
        let mut ids = BTreeSet::new();
        for item in collect(transport).await? {
            if !is_platform_owned(&item)? {
                ids.insert(AgentPolicySummary::from_item(&item)?.id);
            }
        }
        ids
    } else {
        let mut ids = BTreeSet::new();
        for selector in selectors {
            let resolved = resolve_item(transport, selector).await?;
            ids.insert(resolved.summary.id.clone());
            resolved_items.insert(resolved.summary.id, resolved.item);
        }
        ids
    };
    let mut specs = Vec::with_capacity(ids.len());
    for id in &ids {
        let live = match resolved_items.get(id) {
            Some(item) => live_from_item(item, id, transport.space())?,
            None => read_live(transport, id).await?,
        };
        specs.push(live.spec);
    }
    specs.sort_by(|left, right| left.id.cmp(&right.id));
    let body = content_codec::encode_sequence(&specs, format)?;
    Ok(ExportOutcome {
        body,
        exported: specs.len() as u64,
        missing: Vec::new(),
    })
}