use super::{Classification, Handler, HandlerContext, has_flag, positional_args};
pub static KUBECTL_HANDLER: KubectlHandler = KubectlHandler;
pub struct KubectlHandler;
const KUBECTL_SAFE: &[&str] = &[
"get",
"describe",
"explain",
"logs",
"top",
"cluster-info",
"version",
"api-resources",
"api-versions",
"config",
"auth",
"wait",
"diff",
"plugin",
"completion",
"kustomize",
];
impl Handler for KubectlHandler {
fn commands(&self) -> &[&str] {
&["kubectl", "k"]
}
fn classify(&self, ctx: &HandlerContext) -> Classification {
let sub = ctx.args.first().map_or("", String::as_str);
let desc = format!("kubectl {sub}");
if has_flag(ctx.args, &["--help", "-h", "--version"]) {
return Classification::Allow("kubectl help/version".into());
}
if sub == "exec" {
return classify_kubectl_exec(ctx);
}
if KUBECTL_SAFE.contains(&sub) {
Classification::Allow(desc)
} else {
Classification::Ask(desc)
}
}
}
fn classify_kubectl_exec(ctx: &HandlerContext) -> Classification {
if let Some(sep) = ctx.args.iter().position(|a| a == "--") {
let inner = ctx.args[sep + 1..]
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.join(" ");
if !inner.is_empty() {
return Classification::RecurseRemote(inner);
}
}
Classification::Ask("kubectl exec".into())
}
pub static AWS_HANDLER: AwsHandler = AwsHandler;
pub struct AwsHandler;
const AWS_SAFE_PREFIXES: &[&str] = &[
"describe-",
"list-",
"get-",
"show-",
"head-",
"lookup-",
"filter-",
"validate-",
"estimate-",
"simulate-",
"generate-",
"download-",
"detect-",
"test-",
"check-if-",
"admin-get-",
"admin-list-",
];
const AWS_SAFE_ACTIONS: &[&str] = &[
"ls",
"wait",
"help",
"query",
"scan",
"tail",
"receive-message",
"batch-get-item",
"transact-get-items",
];
impl Handler for AwsHandler {
fn commands(&self) -> &[&str] {
&["aws"]
}
fn classify(&self, ctx: &HandlerContext) -> Classification {
if has_flag(ctx.args, &["--help", "--version"]) {
return Classification::Allow("aws help/version".into());
}
let positionals = positional_args(ctx.args);
let service = positionals.first().copied().unwrap_or_default();
let action = positionals.get(1).copied().unwrap_or_default();
if service == "configure" {
return if matches!(action, "list" | "list-profiles" | "get" | "") {
Classification::Allow(format!("aws configure {action}"))
} else {
Classification::Ask(format!("aws configure {action}"))
};
}
if service == "sts" {
let sts_safe = [
"get-caller-identity",
"get-session-token",
"get-access-key-info",
"decode-authorization-message",
];
if sts_safe.contains(&action) {
return Classification::Allow(format!("aws sts {action}"));
}
}
if AWS_SAFE_ACTIONS.contains(&action) {
return Classification::Allow(format!("aws {service} {action}"));
}
if AWS_SAFE_PREFIXES.iter().any(|p| action.starts_with(p)) {
return Classification::Allow(format!("aws {service} {action}"));
}
Classification::Ask(format!("aws {service} {action}"))
}
}
pub static GCLOUD_HANDLER: GcloudHandler = GcloudHandler;
pub struct GcloudHandler;
const GCLOUD_SAFE_KEYWORDS: &[&str] = &[
"describe",
"list",
"get",
"show",
"info",
"status",
"version",
"get-credentials",
"list-tags",
"read",
"configurations",
];
impl Handler for GcloudHandler {
fn commands(&self) -> &[&str] {
&["gcloud", "gsutil"]
}
fn classify(&self, ctx: &HandlerContext) -> Classification {
if has_flag(ctx.args, &["--help", "-h", "--version"]) {
return Classification::Allow(format!("{} help/version", ctx.command_name));
}
if ctx.command_name == "gsutil" {
let sub = ctx.args.first().map_or("", String::as_str);
return match sub {
"ls" | "cat" | "stat" | "du" | "hash" | "version" | "help" => {
Classification::Allow(format!("gsutil {sub}"))
}
_ => Classification::Ask(format!("gsutil {sub}")),
};
}
let args: Vec<&str> = ctx
.args
.iter()
.map(String::as_str)
.skip_while(|a| matches!(*a, "alpha" | "beta"))
.collect();
let action = args.last().copied().unwrap_or_default();
if GCLOUD_SAFE_KEYWORDS.contains(&action) {
Classification::Allow(format!("gcloud ... {action}"))
} else {
Classification::Ask(format!("gcloud {}", ctx.args.join(" ")))
}
}
}
pub static AZ_HANDLER: AzHandler = AzHandler;
pub struct AzHandler;
const AZ_SAFE_KEYWORDS: &[&str] = &[
"show",
"list",
"get",
"exists",
"query",
"logs",
"check-health",
"download",
"tail",
];
impl Handler for AzHandler {
fn commands(&self) -> &[&str] {
&["az"]
}
fn classify(&self, ctx: &HandlerContext) -> Classification {
if has_flag(ctx.args, &["--help", "-h", "--version"]) {
return Classification::Allow("az help/version".into());
}
let positionals = positional_args(ctx.args);
let action = positionals.last().copied().unwrap_or_default();
if AZ_SAFE_KEYWORDS.contains(&action)
|| action.starts_with("list-")
|| action.starts_with("show-")
|| action.starts_with("get-")
{
Classification::Allow(format!("az ... {action}"))
} else {
Classification::Ask(format!("az {}", ctx.args.join(" ")))
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::path::Path;
use super::*;
fn ctx<'a>(args: &'a [String], cmd: &'a str) -> HandlerContext<'a> {
HandlerContext {
command_name: cmd,
args,
working_directory: Path::new("/tmp"),
remote: false,
receives_piped_input: false,
cd_allowed_dirs: &[],
}
}
#[test]
fn kubectl_get_allows() {
let args: Vec<String> = vec!["get".into(), "pods".into()];
let result = KUBECTL_HANDLER.classify(&ctx(&args, "kubectl"));
assert!(matches!(result, Classification::Allow(_)));
}
#[test]
fn kubectl_exec_recurses_remote() {
let args: Vec<String> = vec![
"exec".into(),
"mypod".into(),
"--".into(),
"cat".into(),
"/etc/hosts".into(),
];
let result = KUBECTL_HANDLER.classify(&ctx(&args, "kubectl"));
assert!(matches!(result, Classification::RecurseRemote(cmd) if cmd == "cat /etc/hosts"));
}
#[test]
fn kubectl_apply_asks() {
let args: Vec<String> = vec!["apply".into(), "-f".into(), "deploy.yaml".into()];
let result = KUBECTL_HANDLER.classify(&ctx(&args, "kubectl"));
assert!(matches!(result, Classification::Ask(_)));
}
#[test]
fn aws_describe_allows() {
let args: Vec<String> = vec!["ec2".into(), "describe-instances".into()];
let result = AWS_HANDLER.classify(&ctx(&args, "aws"));
assert!(matches!(result, Classification::Allow(_)));
}
#[test]
fn aws_create_asks() {
let args: Vec<String> = vec!["ec2".into(), "create-instance".into()];
let result = AWS_HANDLER.classify(&ctx(&args, "aws"));
assert!(matches!(result, Classification::Ask(_)));
}
}