elasticctl 0.10.0

Operate Elastic Security as code with a safety-first CLI for security engineers.
use crate::context::Context;
use crate::render;
use crate::report_file;
use elasticctl_api::codec::Format as FileFormat;
use elasticctl_api::rules::RuleSource;
use elasticctl_api::state;
use elasticctl_core::{Error, ErrorKind, Result};
use serde_json::Value;
use std::path::Path;

mod value_lists;

fn to_value<T: serde::Serialize>(v: &T) -> Result<Value> {
    serde_json::to_value(v)
        .map_err(|e| Error::new(ErrorKind::Error, format!("encoding report: {e}")))
}

pub async fn pull(
    ctx: &Context,
    dir: &Path,
    format: FileFormat,
    selectors: &[String],
    tag: Option<&str>,
    search: Option<&str>,
    source: RuleSource,
) -> Result<Value> {
    ctx.require_credential()?;
    let t = ctx.transport().await?;
    to_value(&state::pull(t, dir, format, selectors, tag, search, source).await?)
}

pub async fn diff(
    ctx: &Context,
    dir: &Path,
    selectors: &[String],
    tag: Option<&str>,
    search: Option<&str>,
    source: RuleSource,
) -> Result<Value> {
    ctx.require_credential()?;
    let t = ctx.transport().await?;
    to_value(&state::diff(t, dir, selectors, tag, search, source).await?)
}

pub async fn push(
    ctx: &Context,
    dir: &Path,
    report_path: Option<&Path>,
    selectors: &[String],
    tag: Option<&str>,
    search: Option<&str>,
    source: RuleSource,
) -> Result<Value> {
    ctx.require_credential()?;
    let t = ctx.transport().await?;
    let identity = state::StackIdentity {
        profile: ctx.resolved.name.clone(),
        host: ctx.resolved.profile.host(),
        space: ctx.resolved.profile.space.clone(),
    };
    let plan = state::plan_push(t, dir, selectors, tag, search, source, &identity).await?;

    let prepared_report = report_path
        .map(|path| report_file::prepare_report(path, &plan.report))
        .transpose()?;

    let plan = if value_lists::check(ctx, &plan)? {
        state::apply_push(t, plan).await?
    } else {
        plan
    };

    if let Some(prepared) = prepared_report {
        let path = prepared.requested_path().to_path_buf();
        prepared.publish(&plan.report).map_err(|error| {
            report_file::report_publication_error(plan.report.applied, &path, error)
        })?;
    }

    let mut summary = to_value(&plan.summary)?;
    let failure = plan
        .value_list_outcomes
        .iter()
        .find_map(|list| list.error.as_ref());
    if value_lists::changed(&plan) || failure.is_some() {
        summary["value_lists"] = to_value(&plan.value_list_outcomes)?;
    }
    if let Some(failure) = failure {
        render::emit(&summary, &ctx.global)?;
        let kind = match failure.kind.as_str() {
            "auth" => ErrorKind::Auth,
            "permission" => ErrorKind::Permission,
            "not_found" => ErrorKind::NotFound,
            "conflict" => ErrorKind::Conflict,
            "unsupported" => ErrorKind::Unsupported,
            "http" => ErrorKind::Http,
            "connection" => ErrorKind::Connection,
            "timeout" => ErrorKind::Timeout,
            _ => ErrorKind::Error,
        };
        return Err(Error {
            kind,
            http_status: failure.status,
            message: failure.message.clone(),
        });
    }
    Ok(summary)
}