#![forbid(unsafe_code)]
mod cli;
mod cmd;
mod context;
mod guard;
mod render;
mod resolve;
use clap::Parser;
use cli::{Cli, Command, ConfigAction, GlobalArgs, RulesAction, StateAction};
use context::Context;
use elasticctl_api::rules::RuleFilter;
use elasticctl_core::{Config, Error, ErrorKind};
use serde_json::{Value, json};
#[tokio::main]
async fn main() {
let args = Cli::parse();
let result = match &args.command {
Command::Config { action } => match action {
ConfigAction::List => cmd::config_cmd::list(&args.global),
ConfigAction::Show => cmd::config_cmd::show(&args.global),
ConfigAction::Init { name, from_env } => {
cmd::config_cmd::init(&args.global, name.as_deref(), *from_env)
}
ConfigAction::Test => match Context::build(&args.global) {
Ok(ctx) => cmd::config_cmd::test(&ctx).await,
Err(e) => Err(e),
},
},
Command::Doctor => cmd::doctor::run(&args.global).await,
Command::Info => match Context::build(&args.global) {
Ok(ctx) => cmd::info::run(&ctx).await,
Err(e) => Err(e),
},
Command::Rules { action } => match action {
RulesAction::List {
enabled,
disabled,
rule_type,
severity,
tag,
filter,
} => {
if *enabled && *disabled {
Err(Error::new(
ErrorKind::Error,
"--enabled and --disabled are mutually exclusive",
))
} else {
let f = RuleFilter {
enabled: if *enabled {
Some(true)
} else if *disabled {
Some(false)
} else {
None
},
rule_type: rule_type.clone(),
severity: severity.clone(),
tag: tag.clone(),
name: None,
query: filter.clone(),
};
match Context::build(&args.global) {
Ok(ctx) => cmd::rules::list(&ctx, &f).await,
Err(e) => Err(e),
}
}
}
RulesAction::Get { selector } => match Context::build(&args.global) {
Ok(ctx) => cmd::rules::get(&ctx, selector).await,
Err(e) => Err(e),
},
RulesAction::Validate { path } => cmd::rules::validate(path),
RulesAction::Enable { selectors } if selectors.is_empty() => Err(Error::new(
ErrorKind::Error,
"Name at least one rule to enable",
)),
RulesAction::Enable { selectors } => match Context::build(&args.global) {
Ok(ctx) => cmd::rules::set_enabled(&ctx, selectors, true).await,
Err(e) => Err(e),
},
RulesAction::Disable { selectors } if selectors.is_empty() => Err(Error::new(
ErrorKind::Error,
"Name at least one rule to disable",
)),
RulesAction::Disable { selectors } => match Context::build(&args.global) {
Ok(ctx) => cmd::rules::set_enabled(&ctx, selectors, false).await,
Err(e) => Err(e),
},
RulesAction::Delete { selectors } if selectors.is_empty() => Err(Error::new(
ErrorKind::Error,
"Name at least one rule to delete",
)),
RulesAction::Delete { selectors } => match Context::build(&args.global) {
Ok(ctx) => cmd::rules::delete(&ctx, selectors).await,
Err(e) => Err(e),
},
RulesAction::Export {
selectors,
tag,
format_file,
} => match parse_file_format(format_file) {
Ok(format) => match Context::build(&args.global) {
Ok(ctx) => {
cmd::rules::export(
&ctx,
selectors,
tag.as_deref(),
args.global.out.as_deref(),
format,
)
.await
}
Err(e) => Err(e),
},
Err(e) => Err(e),
},
RulesAction::Import {
path,
overwrite,
skip_existing,
} => match Context::build(&args.global) {
Ok(ctx) => cmd::rules::import(&ctx, path, *overwrite, *skip_existing).await,
Err(e) => Err(e),
},
RulesAction::Preview {
source,
invocations,
sample,
} => match Context::build(&args.global) {
Ok(ctx) => cmd::rules::preview(&ctx, source, *invocations, *sample).await,
Err(e) => Err(e),
},
},
Command::State { action } => match action {
StateAction::Pull {
dir,
format_file,
selectors,
tag,
} => match parse_file_format(format_file) {
Ok(format) => match Context::build(&args.global) {
Ok(ctx) => cmd::state::pull(&ctx, dir, format, selectors, tag.as_deref()).await,
Err(e) => Err(e),
},
Err(e) => Err(e),
},
StateAction::Diff {
dir,
selectors,
tag,
} => match Context::build(&args.global) {
Ok(ctx) => cmd::state::diff(&ctx, dir, selectors, tag.as_deref()).await,
Err(e) => Err(e),
},
StateAction::Push {
dir,
report,
selectors,
tag,
} => match Context::build(&args.global) {
Ok(ctx) => {
cmd::state::push(&ctx, dir, report.as_deref(), selectors, tag.as_deref()).await
}
Err(e) => Err(e),
},
},
Command::Completion { shell } => cmd::meta::completion(*shell).map(|_| Value::Null),
Command::Commands => cmd::meta::command_tree(),
};
if result.is_ok()
&& !matches!(
&args.command,
Command::Doctor | Command::Completion { .. } | Command::Commands
)
{
emit_permission_warning(&args.global);
}
match result {
Ok(value) => {
if matches!(&args.command, Command::Completion { .. }) {
use std::io::Write;
std::io::stdout().flush().ok();
std::process::exit(0);
}
let export_to_stdout = matches!(
&args.command,
Command::Rules {
action: RulesAction::Export { .. }
}
) && args.global.out.is_none();
if export_to_stdout && let Some(text) = value.get("text").and_then(Value::as_str) {
use std::io::Write;
print!("{text}");
std::io::stdout().flush().ok();
std::process::exit(render::exit_code_for_value(&value));
}
let out_already_written = matches!(
&args.command,
Command::Rules {
action: RulesAction::Export { .. }
}
) && args.global.out.is_some();
let render_global = if out_already_written {
GlobalArgs {
out: None,
..args.global.clone()
}
} else {
args.global.clone()
};
match render::emit(&value, &render_global) {
Ok(()) => {
let code = render::exit_code_for_value(&value);
if code != 0 {
std::process::exit(code);
}
}
Err(e) => {
eprintln!("{}", e.to_envelope());
std::process::exit(render::exit_code_for(&e));
}
}
}
Err(err) => {
eprintln!("{}", err.to_envelope());
std::process::exit(render::exit_code_for(&err));
}
}
}
fn parse_file_format(s: &str) -> Result<elasticctl_api::codec::Format, Error> {
use elasticctl_api::codec::Format;
match s.to_ascii_lowercase().as_str() {
"yaml" | "yml" => Ok(Format::Yaml),
"ndjson" | "json" => Ok(Format::Ndjson),
other => Err(Error::new(
ErrorKind::Error,
format!("unknown format-file '{other}'; expected ndjson or yaml"),
)),
}
}
fn emit_permission_warning(global: &GlobalArgs) {
let path = context::config_path(global);
if let Some(message) = Config::permission_warning(&path) {
eprintln!(
"{}",
json!({"warning": {"kind": "insecure_config_permissions", "message": message}})
);
}
}