//! AWS CLI output compression.
//!
//! Replaces verbose `--output table`/`text` with JSON, then compresses.
//! Specialized filters for high-frequency commands (STS, S3, EC2, ECS, RDS, CloudFormation).
use crate::core::tee::force_tee_hint;
use crate::core::tracking;
use crate::core::truncate::{CAP_INVENTORY, CAP_LIST};
use crate::core::utils::{
exit_code_from_output, exit_code_from_status, human_bytes, join_with_overflow,
resolved_command, shorten_arn, truncate_iso_date,
};
use crate::json_cmd;
use anyhow::{Context, Result};
use lazy_static::lazy_static;
use regex::Regex;
use serde_json::Value;
const MAX_ITEMS: usize = CAP_LIST;
const JSON_COMPRESS_DEPTH: usize = 4;
/// Result of a filter function: filtered text + whether items were truncated.
/// When `truncated` is true, the shared runner force-tees the full raw output
/// so the LLM has a recovery path to access all data.
struct FilterResult {
text: String,
truncated: bool,
}
impl FilterResult {
fn new(text: String) -> Self {
Self {
text,
truncated: false,
}
}
fn truncated(text: String) -> Self {
Self {
text,
truncated: true,
}
}
}
/// Run an AWS CLI command with token-optimized output
pub fn run(subcommand: &str, args: &[String], verbose: u8) -> Result<i32> {
// Build the full sub-path: e.g. "sts" + ["get-caller-identity"] -> "sts get-caller-identity"
let full_sub = if args.is_empty() {
subcommand.to_string()
} else {
format!("{} {}", subcommand, args.join(" "))
};
// Route to specialized handlers
match subcommand {
"sts" if !args.is_empty() && args[0] == "get-caller-identity" => run_aws_filtered(
&["sts", "get-caller-identity"],
&args[1..],
verbose,
filter_sts_identity,
),
"s3" if !args.is_empty() && args[0] == "ls" => run_s3_ls(&args[1..], verbose),
"ec2" if !args.is_empty() && args[0] == "describe-instances" => run_aws_filtered(
&["ec2", "describe-instances"],
&args[1..],
verbose,
filter_ec2_instances,
),
"ecs" if !args.is_empty() && args[0] == "list-services" => run_aws_filtered(
&["ecs", "list-services"],
&args[1..],
verbose,
filter_ecs_list_services,
),
"ecs" if !args.is_empty() && args[0] == "describe-services" => run_aws_filtered(
&["ecs", "describe-services"],
&args[1..],
verbose,
filter_ecs_describe_services,
),
"rds" if !args.is_empty() && args[0] == "describe-db-instances" => run_aws_filtered(
&["rds", "describe-db-instances"],
&args[1..],
verbose,
filter_rds_instances,
),
"cloudformation" if !args.is_empty() && args[0] == "list-stacks" => run_aws_filtered(
&["cloudformation", "list-stacks"],
&args[1..],
verbose,
filter_cfn_list_stacks,
),
"cloudformation" if !args.is_empty() && args[0] == "describe-stacks" => run_aws_filtered(
&["cloudformation", "describe-stacks"],
&args[1..],
verbose,
filter_cfn_describe_stacks,
),
"cloudformation" if !args.is_empty() && args[0] == "describe-stack-events" => {
run_aws_filtered(
&["cloudformation", "describe-stack-events"],
&args[1..],
verbose,
filter_cfn_events,
)
}
"logs"
if !args.is_empty()
&& (args[0] == "get-log-events" || args[0] == "filter-log-events") =>
{
run_aws_filtered(&["logs", &args[0]], &args[1..], verbose, filter_logs_events)
}
"lambda" if !args.is_empty() && args[0] == "list-functions" => run_aws_filtered(
&["lambda", "list-functions"],
&args[1..],
verbose,
filter_lambda_list,
),
"lambda" if !args.is_empty() && args[0] == "get-function" => run_aws_filtered(
&["lambda", "get-function"],
&args[1..],
verbose,
filter_lambda_get,
),
"iam" if !args.is_empty() && args[0] == "list-roles" => run_aws_filtered(
&["iam", "list-roles"],
&args[1..],
verbose,
filter_iam_roles,
),
"iam" if !args.is_empty() && args[0] == "list-users" => run_aws_filtered(
&["iam", "list-users"],
&args[1..],
verbose,
filter_iam_users,
),
"dynamodb" if !args.is_empty() && (args[0] == "scan" || args[0] == "query") => {
run_aws_filtered(
&["dynamodb", &args[0]],
&args[1..],
verbose,
filter_dynamodb_items,
)
}
"ecs" if !args.is_empty() && args[0] == "describe-tasks" => run_aws_filtered(
&["ecs", "describe-tasks"],
&args[1..],
verbose,
filter_ecs_tasks,
),
"ec2" if !args.is_empty() && args[0] == "describe-security-groups" => run_aws_filtered(
&["ec2", "describe-security-groups"],
&args[1..],
verbose,
filter_security_groups,
),
"s3api" if !args.is_empty() && args[0] == "list-objects-v2" => run_aws_filtered(
&["s3api", "list-objects-v2"],
&args[1..],
verbose,
filter_s3_objects,
),
"eks" if !args.is_empty() && args[0] == "describe-cluster" => run_aws_filtered(
&["eks", "describe-cluster"],
&args[1..],
verbose,
filter_eks_cluster,
),
"sqs" if !args.is_empty() && args[0] == "receive-message" => run_aws_filtered(
&["sqs", "receive-message"],
&args[1..],
verbose,
filter_sqs_messages,
),
"dynamodb" if !args.is_empty() && args[0] == "get-item" => run_aws_filtered(
&["dynamodb", "get-item"],
&args[1..],
verbose,
filter_dynamodb_get_item,
),
"logs" if !args.is_empty() && args[0] == "get-query-results" => run_aws_filtered(
&["logs", "get-query-results"],
&args[1..],
verbose,
filter_logs_query_results,
),
"s3" if !args.is_empty() && (args[0] == "sync" || args[0] == "cp") => {
run_s3_transfer(&args[0], &args[1..], verbose)
}
"secretsmanager" if !args.is_empty() && args[0] == "get-secret-value" => run_aws_filtered(
&["secretsmanager", "get-secret-value"],
&args[1..],
verbose,
filter_secrets_get,
),
_ => run_generic(subcommand, args, verbose, &full_sub),
}
}
/// Returns true for operations that return structured JSON (describe-*, list-*, get-*).
/// Mutating/transfer operations (s3 cp, s3 sync, s3 mb, etc.) emit plain text progress
/// and do not accept --output json, so we must not inject it for them.
fn is_structured_operation(args: &[String]) -> bool {
let op = args.first().map(|s| s.as_str()).unwrap_or("");
// Exclude s3 sync/cp (they're text operations)
if op == "sync" || op == "cp" {
return false;
}
op.starts_with("describe-")
|| op.starts_with("list-")
|| op.starts_with("get-")
|| op == "scan"
|| op == "query"
|| op == "receive-message"
}
/// Generic strategy: force --output json for structured ops, compress via json_cmd compact (values preserved)
fn run_generic(subcommand: &str, args: &[String], verbose: u8, full_sub: &str) -> Result<i32> {
let timer = tracking::TimedExecution::start();
let mut cmd = resolved_command("aws");
cmd.arg(subcommand);
let mut has_output_flag = false;
for arg in args {
if arg == "--output" {
has_output_flag = true;
}
cmd.arg(arg);
}
// Only inject --output json for structured read operations.
// Mutating/transfer operations (s3 cp, s3 sync, s3 mb, cloudformation deploy…)
// emit plain-text progress and reject --output json.
if !has_output_flag && is_structured_operation(args) {
cmd.args(["--output", "json"]);
}
if verbose > 0 {
eprintln!("Running: aws {}", full_sub);
}
let output = cmd.output().context("Failed to run aws CLI")?;
let raw = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if !output.status.success() {
timer.track(
&format!("aws {}", full_sub),
&format!("rtk aws {}", full_sub),
&stderr,
&stderr,
);
eprintln!("{}", stderr.trim());
return Ok(crate::core::utils::exit_code_from_output(&output, "aws"));
}
let filtered = match json_cmd::filter_json_compact(&raw, JSON_COMPRESS_DEPTH) {
Ok(compact) => {
println!("{}", compact);
compact
}
Err(_) => {
// Fallback: print raw (maybe not JSON)
print!("{}", raw);
raw.clone()
}
};
timer.track(
&format!("aws {}", full_sub),
&format!("rtk aws {}", full_sub),
&raw,
&filtered,
);
Ok(0)
}
fn run_aws_json(
sub_args: &[&str],
extra_args: &[String],
verbose: u8,
) -> Result<(String, String, std::process::ExitStatus)> {
let mut cmd = resolved_command("aws");
for arg in sub_args {
cmd.arg(arg);
}
// Replace --output table/text with --output json
let mut skip_next = false;
for arg in extra_args {
if skip_next {
skip_next = false;
continue;
}
if arg == "--output" {
skip_next = true;
continue;
}
if arg.starts_with("--output=") {
continue;
}
cmd.arg(arg);
}
cmd.args(["--output", "json"]);
let cmd_desc = format!("aws {}", sub_args.join(" "));
if verbose > 0 {
eprintln!("Running: {}", cmd_desc);
}
let output = cmd
.output()
.context(format!("Failed to run {}", cmd_desc))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if !output.status.success() {
eprintln!("{}", stderr.trim());
}
Ok((stdout, stderr, output.status))
}
/// Shared runner for AWS commands that return JSON.
/// Follows the six-phase contract: timer → execute → filter (fallback) → tee → track → exit code.
fn run_aws_filtered(
sub_args: &[&str],
extra_args: &[String],
verbose: u8,
filter_fn: fn(&str) -> Option<FilterResult>,
) -> Result<i32> {
let cmd_label = format!("aws {}", sub_args.join(" "));
let rtk_label = format!("rtk {}", cmd_label);
let slug = cmd_label.replace(' ', "_");
let timer = tracking::TimedExecution::start();
let (stdout, stderr, status) = run_aws_json(sub_args, extra_args, verbose)?;
// Combine stdout+stderr for accurate tracking (per contract)
let raw = if stderr.is_empty() {
stdout.clone()
} else {
format!("{}\n{}", stdout, stderr)
};
if !status.success() {
let exit_code = exit_code_from_status(&status, "aws");
if let Some(hint) = crate::core::tee::tee_and_hint(&raw, &slug, exit_code) {
eprintln!("{}\n{}", stderr.trim(), hint);
} else {
eprintln!("{}", stderr.trim());
}
timer.track(&cmd_label, &rtk_label, &raw, &stderr);
return Ok(exit_code);
}
let result = filter_fn(&stdout).unwrap_or_else(|| {
eprintln!("rtk: filter warning: aws filter returned None, passing through raw output");
FilterResult::new(stdout.clone())
});
if result.truncated {
if let Some(hint) = crate::core::tee::force_tee_hint(&raw, &slug) {
println!("{}\n{}", result.text, hint);
} else {
println!("{}", result.text);
}
} else if let Some(hint) = crate::core::tee::tee_and_hint(&raw, &slug, 0) {
println!("{}\n{}", result.text, hint);
} else {
println!("{}", result.text);
}
timer.track(&cmd_label, &rtk_label, &raw, &result.text);
Ok(0)
}
fn run_s3_ls(extra_args: &[String], verbose: u8) -> Result<i32> {
let timer = tracking::TimedExecution::start();
let mut cmd = resolved_command("aws");
cmd.args(["s3", "ls"]);
for arg in extra_args {
cmd.arg(arg);
}
if verbose > 0 {
eprintln!("Running: aws s3 ls {}", extra_args.join(" "));
}
let output = cmd.output().context("Failed to run aws s3 ls")?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let raw = if stderr.is_empty() {
stdout.clone()
} else {
format!("{}\n{}", stdout, stderr)
};
if !output.status.success() {
let exit_code = exit_code_from_output(&output, "aws");
if let Some(hint) = crate::core::tee::tee_and_hint(&raw, "aws_s3_ls", exit_code) {
eprintln!("{}\n{}", stderr.trim(), hint);
} else {
eprintln!("{}", stderr.trim());
}
timer.track("aws s3 ls", "rtk aws s3 ls", &raw, &stderr);
return Ok(exit_code);
}
let result = filter_s3_ls(&stdout);
if result.truncated {
if let Some(hint) = crate::core::tee::force_tee_hint(&raw, "aws_s3_ls") {
println!("{}\n{}", result.text, hint);
} else {
println!("{}", result.text);
}
} else {
println!("{}", result.text);
}
timer.track("aws s3 ls", "rtk aws s3 ls", &raw, &result.text);
Ok(0)
}
/// Run s3 sync/cp (text output, not JSON)
fn run_s3_transfer(operation: &str, extra_args: &[String], verbose: u8) -> Result<i32> {
let timer = tracking::TimedExecution::start();
let cmd_label = format!("aws s3 {}", operation);
let rtk_label = format!("rtk aws s3 {}", operation);
let slug = format!("aws_s3_{}", operation);
let mut cmd = resolved_command("aws");
cmd.args(["s3", operation]);
for arg in extra_args {
cmd.arg(arg);
}
if verbose > 0 {
eprintln!("Running: {} {}", cmd_label, extra_args.join(" "));
}
let output = cmd
.output()
.context(format!("Failed to run {}", cmd_label))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let raw = if stderr.is_empty() {
stdout.clone()
} else {
format!("{}\n{}", stdout, stderr)
};
if !output.status.success() {
let exit_code = exit_code_from_output(&output, "aws");
if let Some(hint) = crate::core::tee::tee_and_hint(&raw, &slug, exit_code) {
eprintln!("{}\n{}", stderr.trim(), hint);
} else {
eprintln!("{}", stderr.trim());
}
timer.track(&cmd_label, &rtk_label, &raw, &stderr);
return Ok(exit_code);
}
let result = filter_s3_transfer(&stdout);
if result.truncated {
if let Some(hint) = force_tee_hint(&raw, &slug) {
println!("{}\n{}", result.text, hint);
} else {
println!("{}", result.text);
}
} else {
println!("{}", result.text);
}
timer.track(&cmd_label, &rtk_label, &raw, &result.text);
Ok(0)
}
// --- Filter functions (all use serde_json::Value for resilience) ---
// Each returns Option<FilterResult>: Some = filtered, None = fallback to raw.
// FilterResult.truncated = true means items were cut; shared runner will tee full output.
fn filter_sts_identity(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let account = v["Account"].as_str().unwrap_or("?");
let arn = v["Arn"].as_str().unwrap_or("?");
Some(FilterResult::new(format!("AWS: {} {}", account, arn)))
}
fn filter_s3_ls(output: &str) -> FilterResult {
let lines: Vec<&str> = output.lines().collect();
let total = lines.len();
let limit = MAX_ITEMS + 10;
if total > limit {
let text = format!(
"{}\n… +{} more items",
lines[..limit].join("\n"),
total - limit
);
FilterResult::truncated(text)
} else {
FilterResult::new(lines.join("\n"))
}
}
fn filter_ec2_instances(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let reservations = v["Reservations"].as_array()?;
let mut instances: Vec<String> = Vec::new();
for res in reservations {
if let Some(insts) = res["Instances"].as_array() {
for inst in insts {
let id = inst["InstanceId"].as_str().unwrap_or("?");
let state = inst["State"]["Name"].as_str().unwrap_or("?");
let itype = inst["InstanceType"].as_str().unwrap_or("?");
let private_ip = inst["PrivateIpAddress"].as_str().unwrap_or("-");
let public_ip = inst["PublicIpAddress"].as_str().unwrap_or("-");
let subnet = inst["SubnetId"].as_str().unwrap_or("-");
let vpc = inst["VpcId"].as_str().unwrap_or("-");
let name = inst["Tags"]
.as_array()
.and_then(|tags| tags.iter().find(|t| t["Key"].as_str() == Some("Name")))
.and_then(|t| t["Value"].as_str())
.unwrap_or("-");
let sgs: Vec<&str> = inst["SecurityGroups"]
.as_array()
.map(|arr| arr.iter().filter_map(|sg| sg["GroupId"].as_str()).collect())
.unwrap_or_default();
let sg_str = if sgs.is_empty() {
"-".to_string()
} else {
sgs.join(",")
};
instances.push(format!(
"{} {} {} {} pub:{} vpc:{} subnet:{} sg:[{}] ({})",
id, state, itype, private_ip, public_ip, vpc, subnet, sg_str, name
));
}
}
}
let total = instances.len();
let truncated = total > MAX_ITEMS;
let mut result = format!("EC2: {} instances\n", total);
for inst in instances.iter().take(MAX_ITEMS) {
result.push_str(&format!(" {}\n", inst));
}
if truncated {
result.push_str(&format!(" … +{} more\n", total - MAX_ITEMS));
}
let text = result.trim_end().to_string();
Some(if truncated {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_ecs_list_services(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let arns = v["serviceArns"].as_array()?;
let mut result = Vec::new();
let total = arns.len();
for arn in arns.iter().take(MAX_ITEMS) {
let arn_str = arn.as_str().unwrap_or("?");
result.push(shorten_arn(arn_str).to_string());
}
let text = join_with_overflow(&result, total, MAX_ITEMS, "services");
Some(if total > MAX_ITEMS {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_ecs_describe_services(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let services = v["services"].as_array()?;
let mut result = Vec::new();
let total = services.len();
for svc in services.iter().take(MAX_ITEMS) {
let name = svc["serviceName"].as_str().unwrap_or("?");
let status = svc["status"].as_str().unwrap_or("?");
let running = svc["runningCount"].as_i64().unwrap_or(0);
let desired = svc["desiredCount"].as_i64().unwrap_or(0);
let launch = svc["launchType"].as_str().unwrap_or("?");
result.push(format!(
"{} {} {}/{} ({})",
name, status, running, desired, launch
));
}
let text = join_with_overflow(&result, total, MAX_ITEMS, "services");
Some(if total > MAX_ITEMS {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_rds_instances(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let dbs = v["DBInstances"].as_array()?;
let mut result = Vec::new();
let total = dbs.len();
for db in dbs.iter().take(MAX_ITEMS) {
let name = db["DBInstanceIdentifier"].as_str().unwrap_or("?");
let engine = db["Engine"].as_str().unwrap_or("?");
let version = db["EngineVersion"].as_str().unwrap_or("?");
let class = db["DBInstanceClass"].as_str().unwrap_or("?");
let status = db["DBInstanceStatus"].as_str().unwrap_or("?");
let endpoint = db["Endpoint"]["Address"].as_str().unwrap_or("-");
let port = db["Endpoint"]["Port"].as_i64().unwrap_or(0);
result.push(format!(
"{} {} {} {} {} {}:{}",
name, engine, version, class, status, endpoint, port
));
}
let text = join_with_overflow(&result, total, MAX_ITEMS, "instances");
Some(if total > MAX_ITEMS {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_cfn_list_stacks(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let stacks = v["StackSummaries"].as_array()?;
let mut result = Vec::new();
let total = stacks.len();
for stack in stacks.iter().take(MAX_ITEMS) {
let name = stack["StackName"].as_str().unwrap_or("?");
let status = stack["StackStatus"].as_str().unwrap_or("?");
let date = stack["LastUpdatedTime"]
.as_str()
.or_else(|| stack["CreationTime"].as_str())
.unwrap_or("?");
result.push(format!("{} {} {}", name, status, truncate_iso_date(date)));
}
let text = join_with_overflow(&result, total, MAX_ITEMS, "stacks");
Some(if total > MAX_ITEMS {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_cfn_describe_stacks(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let stacks = v["Stacks"].as_array()?;
let mut result = Vec::new();
let total = stacks.len();
for stack in stacks.iter().take(MAX_ITEMS) {
let name = stack["StackName"].as_str().unwrap_or("?");
let status = stack["StackStatus"].as_str().unwrap_or("?");
let date = stack["LastUpdatedTime"]
.as_str()
.or_else(|| stack["CreationTime"].as_str())
.unwrap_or("?");
result.push(format!("{} {} {}", name, status, truncate_iso_date(date)));
if let Some(outputs) = stack["Outputs"].as_array() {
for out in outputs {
let key = out["OutputKey"].as_str().unwrap_or("?");
let val = out["OutputValue"].as_str().unwrap_or("?");
result.push(format!(" {}={}", key, val));
}
}
}
let text = join_with_overflow(&result, total, MAX_ITEMS, "stacks");
Some(if total > MAX_ITEMS {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
// --- P0 filters: CloudWatch Logs, CloudFormation Events, Lambda ---
const MAX_LOG_EVENTS: usize = CAP_INVENTORY;
/// Convert days since Unix epoch to (year, month, day). Civil calendar, UTC.
fn days_to_ymd(days: i64) -> (i64, i64, i64) {
// Algorithm from http://howardhinnant.github.io/date_algorithms.html
let z = days + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}
fn filter_logs_events(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let events = v["events"].as_array()?;
let total = events.len();
let truncated = total > MAX_LOG_EVENTS;
let mut lines = Vec::new();
for event in events.iter().take(MAX_LOG_EVENTS) {
// Convert epoch ms to YYYY-MM-DD HH:MM:SS UTC
let time_str = match event["timestamp"].as_i64() {
Some(ts) if ts > 0 => {
let epoch_secs = ts / 1000;
// Days since Unix epoch
let days = epoch_secs / 86400;
let time_of_day = epoch_secs % 86400;
let h = time_of_day / 3600;
let m = (time_of_day % 3600) / 60;
let s = time_of_day % 60;
// Convert days to Y-M-D (simplified: good through 2099)
let (y, mo, d) = days_to_ymd(days);
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}", y, mo, d, h, m, s)
}
_ => "??:??:??".to_string(),
};
let msg = event["message"].as_str().unwrap_or("").trim_end();
// If the message is JSON, compact it to one line
let compact_msg = if msg.starts_with('{') {
serde_json::from_str::<Value>(msg)
.ok()
.and_then(|v| serde_json::to_string(&v).ok())
.unwrap_or_else(|| msg.to_string())
} else {
msg.to_string()
};
lines.push(format!("{} {}", time_str, compact_msg));
}
if truncated {
lines.push(format!("… +{} more events", total - MAX_LOG_EVENTS));
}
let text = lines.join("\n");
Some(if truncated {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_cfn_events(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let events = v["StackEvents"].as_array()?;
let mut failed = Vec::new();
let mut failed_count = 0usize;
let mut success_count = 0usize;
for event in events {
let status = event["ResourceStatus"].as_str().unwrap_or("?");
let logical_id = event["LogicalResourceId"].as_str().unwrap_or("?");
let resource_type_raw = event["ResourceType"].as_str().unwrap_or("?");
let resource_type = resource_type_raw
.strip_prefix("AWS::")
.unwrap_or(resource_type_raw);
let ts = event["Timestamp"]
.as_str()
.map(truncate_iso_date)
.unwrap_or("?");
if status.contains("FAILED") || status.contains("ROLLBACK") {
failed_count += 1;
if failed.len() < MAX_ITEMS {
let reason = event["ResourceStatusReason"].as_str().unwrap_or("");
let mut line = format!("{} {} {} {}", ts, logical_id, resource_type, status);
if !reason.is_empty() {
line.push_str(&format!(" REASON: {}", reason));
}
failed.push(line);
}
} else {
success_count += 1;
}
}
let total_events = events.len();
let mut lines = Vec::new();
lines.push(format!(
"CloudFormation: {} events ({} failed, {} successful)",
total_events, failed_count, success_count
));
if !failed.is_empty() {
lines.push("--- FAILURES ---".to_string());
for f in &failed {
lines.push(format!(" {}", f));
}
}
if success_count > 0 {
lines.push(format!("+ {} successful resources", success_count));
}
// Truncate if huge number of events
let truncated = total_events > MAX_ITEMS * 5; // >100 events
let text = lines.join("\n");
Some(if truncated {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_lambda_list(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let functions = v["Functions"].as_array()?;
let total = functions.len();
let truncated = total > MAX_ITEMS;
let mut result = Vec::new();
for func in functions.iter().take(MAX_ITEMS) {
let name = func["FunctionName"].as_str().unwrap_or("?");
let runtime = func["Runtime"].as_str().unwrap_or("?");
let memory = func["MemorySize"].as_i64().unwrap_or(0);
let timeout = func["Timeout"].as_i64().unwrap_or(0);
let state = func["State"].as_str().unwrap_or("active");
// SECURITY: Environment is intentionally NOT read (may contain secrets)
result.push(format!(
"{} {} {}MB {}s {}",
name, runtime, memory, timeout, state
));
}
let text = join_with_overflow(&result, total, MAX_ITEMS, "functions");
Some(if truncated {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_lambda_get(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let config = &v["Configuration"];
let name = config["FunctionName"].as_str().unwrap_or("?");
let runtime = config["Runtime"].as_str().unwrap_or("?");
let handler = config["Handler"].as_str().unwrap_or("?");
let memory = config["MemorySize"].as_i64().unwrap_or(0);
let timeout = config["Timeout"].as_i64().unwrap_or(0);
let state = config["State"].as_str().unwrap_or("active");
let last_modified = config["LastModified"]
.as_str()
.map(truncate_iso_date)
.unwrap_or("?");
// SECURITY: Environment and Code.Location intentionally NOT read
let mut text = format!(
"{} {} {} {}MB {}s {} {}",
name, runtime, handler, memory, timeout, state, last_modified
);
// Show layer names if present
// Layer ARNs use colons: arn:aws:lambda:region:acct:layer:name:version
if let Some(layers) = config["Layers"].as_array() {
if !layers.is_empty() {
let layer_names: Vec<String> = layers
.iter()
.filter_map(|l| {
let arn = l["Arn"].as_str()?;
let parts: Vec<&str> = arn.rsplitn(3, ':').collect();
if parts.len() >= 2 {
Some(format!("{}:{}", parts[1], parts[0]))
} else {
Some(arn.to_string())
}
})
.collect();
text.push_str(&format!("\n layers: {}", layer_names.join(", ")));
}
}
Some(FilterResult::new(text))
}
// --- P1 filters: IAM, DynamoDB, ECS tasks ---
/// Extract principal services/accounts from AssumeRolePolicyDocument.
/// Returns compact list like ["lambda.amazonaws.com", "ecs-tasks.amazonaws.com"]
/// instead of the full 200+ token JSON policy document.
fn extract_assume_principals(role: &Value) -> Vec<String> {
let mut principals = Vec::new();
// AssumeRolePolicyDocument can be a JSON string or an object
let doc = if let Some(s) = role["AssumeRolePolicyDocument"].as_str() {
serde_json::from_str::<Value>(s).ok()
} else if role["AssumeRolePolicyDocument"].is_object() {
Some(role["AssumeRolePolicyDocument"].clone())
} else {
None
};
if let Some(doc) = doc {
let statements = doc["Statement"].as_array();
if let Some(stmts) = statements {
for stmt in stmts {
let principal = &stmt["Principal"];
// Principal can be "*", {"Service": "..."}, {"AWS": "..."}, etc.
if let Some(s) = principal.as_str() {
principals.push(s.to_string());
} else if let Some(svc) = principal["Service"].as_str() {
principals.push(svc.to_string());
} else if let Some(svcs) = principal["Service"].as_array() {
for s in svcs {
if let Some(s) = s.as_str() {
principals.push(s.to_string());
}
}
} else if let Some(aws) = principal["AWS"].as_str() {
principals.push(shorten_arn(aws).to_string());
} else if let Some(awss) = principal["AWS"].as_array() {
for a in awss {
if let Some(a) = a.as_str() {
principals.push(shorten_arn(a).to_string());
}
}
}
}
}
}
principals.dedup();
principals
}
fn filter_iam_roles(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let roles = v["Roles"].as_array()?;
let total = roles.len();
let truncated = total > MAX_ITEMS;
let mut result = Vec::new();
for role in roles.iter().take(MAX_ITEMS) {
let name = role["RoleName"].as_str().unwrap_or("?");
let date = role["CreateDate"]
.as_str()
.map(truncate_iso_date)
.unwrap_or("?");
let desc = role["Description"].as_str().unwrap_or("");
// Extract principals from AssumeRolePolicyDocument (compact, not full JSON)
let principals = extract_assume_principals(role);
let principal_str = if principals.is_empty() {
String::new()
} else {
format!(" assume:[{}]", principals.join(","))
};
if desc.is_empty() {
result.push(format!("{} {}{}", name, date, principal_str));
} else {
result.push(format!("{} {} [{}]{}", name, date, desc, principal_str));
}
}
let text = join_with_overflow(&result, total, MAX_ITEMS, "roles");
Some(if truncated {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_iam_users(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let users = v["Users"].as_array()?;
let total = users.len();
let truncated = total > MAX_ITEMS;
let mut result = Vec::new();
for user in users.iter().take(MAX_ITEMS) {
let name = user["UserName"].as_str().unwrap_or("?");
let date = user["CreateDate"]
.as_str()
.map(truncate_iso_date)
.unwrap_or("?");
result.push(format!("{} created:{}", name, date));
}
let text = join_with_overflow(&result, total, MAX_ITEMS, "users");
Some(if truncated {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
/// Recursively unwrap DynamoDB typed values to plain JSON.
/// `{"S": "foo"}` -> `"foo"`, `{"N": "42"}` -> `42`, `{"M": {...}}` -> unwrapped object, etc.
fn unwrap_dynamodb_value(val: &Value, depth: usize) -> Value {
if depth > 10 {
return val.clone();
}
if let Some(obj) = val.as_object() {
if obj.len() == 1 {
if let Some((key, inner)) = obj.iter().next() {
match key.as_str() {
"S" | "B" => return inner.clone(),
"N" => {
if let Some(s) = inner.as_str() {
// Try i64 first, then f64
if let Ok(n) = s.parse::<i64>() {
return Value::Number(n.into());
}
if let Ok(f) = s.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(f) {
return Value::Number(n);
}
}
return Value::String(s.to_string());
}
return inner.clone();
}
"BOOL" => return inner.clone(),
"NULL" => return Value::Null,
"L" => {
if let Some(arr) = inner.as_array() {
return Value::Array(
arr.iter()
.map(|v| unwrap_dynamodb_value(v, depth + 1))
.collect(),
);
}
}
"M" => {
if let Some(map) = inner.as_object() {
let unwrapped: serde_json::Map<String, Value> = map
.iter()
.map(|(k, v)| (k.clone(), unwrap_dynamodb_value(v, depth + 1)))
.collect();
return Value::Object(unwrapped);
}
}
"SS" => return inner.clone(),
"NS" => {
// Parse NS set: try i64 first, then f64
if let Some(arr) = inner.as_array() {
let nums: Vec<Value> = arr
.iter()
.filter_map(|v| {
let s = v.as_str()?;
if let Ok(n) = s.parse::<i64>() {
Some(Value::Number(n.into()))
} else if let Ok(f) = s.parse::<f64>() {
serde_json::Number::from_f64(f).map(Value::Number)
} else {
Some(Value::String(s.to_string()))
}
})
.collect();
return Value::Array(nums);
}
return inner.clone();
}
"BS" => return inner.clone(),
_ => {}
}
}
}
// Not a DynamoDB type wrapper — unwrap each field as a potential item
let unwrapped: serde_json::Map<String, Value> = obj
.iter()
.map(|(k, v)| (k.clone(), unwrap_dynamodb_value(v, depth + 1)))
.collect();
return Value::Object(unwrapped);
}
val.clone()
}
fn filter_dynamodb_items(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let items = v["Items"].as_array()?;
let count = v["Count"].as_i64().unwrap_or(items.len() as i64);
let scanned = v["ScannedCount"].as_i64().unwrap_or(count);
let total = items.len();
let truncated = total > MAX_ITEMS;
let mut lines = Vec::new();
lines.push(format!("Count: {}/{}", count, scanned));
// Show ConsumedCapacity if present
if let Some(capacity) = v["ConsumedCapacity"].as_object() {
if let Some(units) = capacity["CapacityUnits"].as_f64() {
lines.push(format!("Capacity: {} RCU", units));
}
}
// Show pagination status if LastEvaluatedKey exists
if v["LastEvaluatedKey"].is_object() {
lines.push("(paginated — more results available)".to_string());
}
for item in items.iter().take(MAX_ITEMS) {
let unwrapped = unwrap_dynamodb_value(item, 0);
let compact = serde_json::to_string(&unwrapped).unwrap_or_else(|_| "?".to_string());
lines.push(compact);
}
if truncated {
lines.push(format!("… +{} more items", total - MAX_ITEMS));
}
let text = lines.join("\n");
Some(if truncated {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_ecs_tasks(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let tasks = v["tasks"].as_array()?;
let total = tasks.len();
let truncated = total > MAX_ITEMS;
let mut result = Vec::new();
for task in tasks.iter().take(MAX_ITEMS) {
let task_arn = task["taskArn"].as_str().unwrap_or("?");
let task_id = shorten_arn(task_arn);
let status = task["lastStatus"].as_str().unwrap_or("?");
let containers: Vec<String> = task["containers"]
.as_array()
.map(|cs| {
cs.iter()
.map(|c| {
let name = c["name"].as_str().unwrap_or("?");
let cstatus = c["lastStatus"].as_str().unwrap_or("?");
let exit = c["exitCode"].as_i64();
match exit {
Some(code) => format!("{}:{}(exit:{})", name, cstatus, code),
None => format!("{}:{}", name, cstatus),
}
})
.collect()
})
.unwrap_or_default();
let stopped_reason = task["stoppedReason"].as_str().unwrap_or("");
let reason_str = if stopped_reason.is_empty() {
String::new()
} else {
format!(" reason:{}", stopped_reason)
};
result.push(format!(
"{} {} containers:[{}]{}",
task_id,
status,
containers.join(", "),
reason_str
));
}
let text = join_with_overflow(&result, total, MAX_ITEMS, "tasks");
Some(if truncated {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
// --- P2 filters: Security Groups, S3 objects, EKS, SQS ---
fn format_sg_rule(perm: &Value) -> String {
let protocol = perm["IpProtocol"].as_str().unwrap_or("?");
let proto = if protocol == "-1" { "all" } else { protocol };
let from_port = perm["FromPort"].as_i64();
let to_port = perm["ToPort"].as_i64();
let port = match (from_port, to_port) {
(Some(f), Some(t)) if f == t => format!("{}", f),
(Some(f), Some(t)) => format!("{}-{}", f, t),
_ => "*".to_string(),
};
let mut sources = Vec::new();
if let Some(ranges) = perm["IpRanges"].as_array() {
for r in ranges {
if let Some(cidr) = r["CidrIp"].as_str() {
sources.push(cidr.to_string());
}
}
}
if let Some(ranges) = perm["Ipv6Ranges"].as_array() {
for r in ranges {
if let Some(cidr) = r["CidrIpv6"].as_str() {
sources.push(cidr.to_string());
}
}
}
if let Some(groups) = perm["UserIdGroupPairs"].as_array() {
for g in groups {
let gid = g["GroupId"].as_str().unwrap_or("?");
sources.push(gid.to_string());
}
}
let src = if sources.is_empty() {
"?".to_string()
} else {
sources.join(",")
};
if proto == "all" {
format!("all<-{}", src)
} else {
format!("{}/{}<-{}", proto, port, src)
}
}
fn filter_security_groups(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let groups = v["SecurityGroups"].as_array()?;
let total = groups.len();
let truncated = total > MAX_ITEMS;
let mut result = Vec::new();
for sg in groups.iter().take(MAX_ITEMS) {
let name = sg["GroupName"].as_str().unwrap_or("?");
let id = sg["GroupId"].as_str().unwrap_or("?");
let ingress: Vec<String> = sg["IpPermissions"]
.as_array()
.map(|perms| perms.iter().map(format_sg_rule).collect())
.unwrap_or_default();
let egress: Vec<String> = sg["IpPermissionsEgress"]
.as_array()
.map(|perms| perms.iter().map(format_sg_rule).collect())
.unwrap_or_default();
let ingress_str = if ingress.is_empty() {
"none".to_string()
} else {
ingress.join(", ")
};
let egress_str = if egress.is_empty() {
"none".to_string()
} else {
egress.join(", ")
};
result.push(format!(
"{} ({}) ingress: {} | egress: {}",
name, id, ingress_str, egress_str
));
}
let text = join_with_overflow(&result, total, MAX_ITEMS, "groups");
Some(if truncated {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_s3_objects(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let empty_vec = vec![];
let contents = v["Contents"].as_array().unwrap_or(&empty_vec);
let total = contents.len();
let truncated = total > MAX_ITEMS;
let mut result = Vec::new();
for obj in contents.iter().take(MAX_ITEMS) {
let key = obj["Key"].as_str().unwrap_or("?");
let size = obj["Size"].as_u64().unwrap_or(0);
let modified = obj["LastModified"]
.as_str()
.map(truncate_iso_date)
.unwrap_or("?");
result.push(format!("{} {} {}", key, human_bytes(size), modified));
}
let text = join_with_overflow(&result, total, MAX_ITEMS, "objects");
Some(if truncated {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_eks_cluster(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let cluster = &v["cluster"];
let name = cluster["name"].as_str().unwrap_or("?");
let status = cluster["status"].as_str().unwrap_or("?");
let version = cluster["version"].as_str().unwrap_or("?");
let endpoint = cluster["endpoint"].as_str().unwrap_or("?");
// certificateAuthority intentionally NOT read (base64 cert, 1000+ chars)
let text = format!("{} {} k8s/{} {}", name, status, version, endpoint);
Some(FilterResult::new(text))
}
lazy_static! {
static ref S3_TRANSFER_RE: Regex = Regex::new(r"^(upload|download|delete|copy|move):").unwrap();
}
fn filter_sqs_messages(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let empty_vec = vec![];
let messages = v["Messages"].as_array().unwrap_or(&empty_vec);
let total = messages.len();
let truncated = total > MAX_ITEMS;
let mut result = Vec::new();
for msg in messages.iter().take(MAX_ITEMS) {
let id = msg["MessageId"].as_str().unwrap_or("?");
let id_short = &id[..id.len().min(8)]; // UUIDs are ASCII-safe
let body = msg["Body"].as_str().unwrap_or("?");
let body_truncated = crate::core::utils::truncate(body, 200);
// ReceiptHandle intentionally NOT read (200+ chars of opaque garbage)
result.push(format!("{} {}", id_short, body_truncated));
}
let text = join_with_overflow(&result, total, MAX_ITEMS, "messages");
Some(if truncated {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
})
}
fn filter_dynamodb_get_item(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let mut lines = Vec::new();
// Extract and unwrap the Item
if let Some(item) = v["Item"].as_object() {
let unwrapped = unwrap_dynamodb_value(&Value::Object(item.clone()), 0);
let compact = serde_json::to_string(&unwrapped).unwrap_or_else(|_| "?".to_string());
lines.push(compact);
}
// Show ConsumedCapacity if present
if let Some(capacity) = v["ConsumedCapacity"].as_object() {
if let Some(units) = capacity["CapacityUnits"].as_f64() {
lines.push(format!("Capacity: {} RCU", units));
}
}
if lines.is_empty() {
return None;
}
Some(FilterResult::new(lines.join("\n")))
}
fn filter_logs_query_results(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let mut lines = Vec::new();
// Show status
if let Some(status) = v["status"].as_str() {
lines.push(format!("Status: {}", status));
}
// Extract results array (array of arrays of {field, value} objects)
if let Some(results) = v["results"].as_array() {
let total = results.len();
let truncated = total > MAX_ITEMS;
for row in results.iter().take(MAX_ITEMS) {
if let Some(fields) = row.as_array() {
let field_pairs: Vec<String> = fields
.iter()
.filter_map(|field| {
let field_name = field["field"].as_str()?;
// Skip internal @ptr field
if field_name == "@ptr" {
return None;
}
let field_value = match field["value"].as_str() {
Some(s) => s.to_string(),
None => field["value"].to_string(), // numbers, booleans
};
Some(format!("{}={}", field_name, field_value))
})
.collect();
lines.push(field_pairs.join(" "));
}
}
if truncated {
lines.push(format!("… +{} more rows", total - MAX_ITEMS));
}
let text = lines.join("\n");
return Some(if truncated {
FilterResult::truncated(text)
} else {
FilterResult::new(text)
});
}
None
}
fn filter_s3_transfer(output: &str) -> FilterResult {
let lines: Vec<&str> = output.lines().collect();
let total = lines.len();
// Pass through short output unchanged
if total < 10 {
return FilterResult::new(output.to_string());
}
// Count operations
let mut uploaded = 0;
let mut downloaded = 0;
let mut deleted = 0;
let mut copied = 0;
let mut moved = 0;
let mut errors = Vec::new();
for line in &lines {
if let Some(captures) = S3_TRANSFER_RE.captures(line) {
match captures.get(1).map(|m| m.as_str()) {
Some("upload") => uploaded += 1,
Some("download") => downloaded += 1,
Some("delete") => deleted += 1,
Some("copy") => copied += 1,
Some("move") => moved += 1,
_ => {}
}
} else if line.contains("error") || line.contains("failed") {
errors.push(line.to_string());
}
}
let mut summary_parts = Vec::new();
if uploaded > 0 {
summary_parts.push(format!("{} uploaded", uploaded));
}
if downloaded > 0 {
summary_parts.push(format!("{} downloaded", downloaded));
}
if deleted > 0 {
summary_parts.push(format!("{} deleted", deleted));
}
if copied > 0 {
summary_parts.push(format!("{} copied", copied));
}
if moved > 0 {
summary_parts.push(format!("{} moved", moved));
}
let mut result_lines = Vec::new();
if !summary_parts.is_empty() {
result_lines.push(format!(
"S3 transfer: {}, {} errors",
summary_parts.join(", "),
errors.len()
));
}
// Include error lines verbatim
for error in errors.iter().take(10) {
result_lines.push(error.clone());
}
if result_lines.is_empty() {
return FilterResult::new(output.to_string());
}
FilterResult::new(result_lines.join("\n"))
}
fn filter_secrets_get(json_str: &str) -> Option<FilterResult> {
let v: Value = serde_json::from_str(json_str).ok()?;
let mut lines = Vec::new();
// Extract Name
if let Some(name) = v["Name"].as_str() {
lines.push(format!("Name: {}", name));
}
// Extract SecretString
if let Some(secret_str) = v["SecretString"].as_str() {
// Try to parse as JSON and compact it
if let Ok(secret_json) = serde_json::from_str::<Value>(secret_str) {
let compact =
serde_json::to_string(&secret_json).unwrap_or_else(|_| secret_str.to_string());
lines.push(format!("Secret: {}", compact));
} else {
lines.push(format!("Secret: {}", secret_str));
}
}
if lines.is_empty() {
return None;
}
Some(FilterResult::new(lines.join("\n")))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::utils::count_tokens;
#[test]
fn test_snapshot_sts_identity() {
let json = r#"{
"UserId": "AIDAEXAMPLEUSERID1234",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/dev-user"
}"#;
let result = filter_sts_identity(json).unwrap();
assert_eq!(
result.text,
"AWS: 123456789012 arn:aws:iam::123456789012:user/dev-user"
);
assert!(!result.truncated);
}
#[test]
fn test_snapshot_ec2_instances() {
let json = r#"{"Reservations":[{"Instances":[{"InstanceId":"i-0a1b2c3d4e5f00001","InstanceType":"t3.micro","PrivateIpAddress":"10.0.1.10","PublicIpAddress":"54.1.2.3","VpcId":"vpc-123","SubnetId":"subnet-a","State":{"Code":16,"Name":"running"},"Tags":[{"Key":"Name","Value":"web-server-1"}],"BlockDeviceMappings":[],"SecurityGroups":[{"GroupId":"sg-001"}]},{"InstanceId":"i-0a1b2c3d4e5f00002","InstanceType":"t3.large","PrivateIpAddress":"10.0.2.20","VpcId":"vpc-123","SubnetId":"subnet-b","State":{"Code":80,"Name":"stopped"},"Tags":[{"Key":"Name","Value":"worker-1"}],"BlockDeviceMappings":[],"SecurityGroups":[{"GroupId":"sg-002"}]}]}]}"#;
let result = filter_ec2_instances(json).unwrap();
assert!(result.text.contains("EC2: 2 instances"));
assert!(result.text.contains("i-0a1b2c3d4e5f00001 running t3.micro 10.0.1.10 pub:54.1.2.3 vpc:vpc-123 subnet:subnet-a sg:[sg-001] (web-server-1)"));
assert!(result
.text
.contains("i-0a1b2c3d4e5f00002 stopped t3.large 10.0.2.20"));
assert!(!result.truncated);
}
#[test]
fn test_filter_sts_identity() {
let json = r#"{
"UserId": "AIDAEXAMPLE",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/dev"
}"#;
let result = filter_sts_identity(json).unwrap();
assert_eq!(
result.text,
"AWS: 123456789012 arn:aws:iam::123456789012:user/dev"
);
}
#[test]
fn test_filter_sts_identity_missing_fields() {
let json = r#"{}"#;
let result = filter_sts_identity(json).unwrap();
assert_eq!(result.text, "AWS: ? ?");
}
#[test]
fn test_filter_sts_identity_invalid_json() {
let result = filter_sts_identity("not json");
assert!(result.is_none());
}
#[test]
fn test_filter_s3_ls_basic() {
let output = "2024-01-01 bucket1\n2024-01-02 bucket2\n2024-01-03 bucket3\n";
let result = filter_s3_ls(output);
assert!(result.text.contains("bucket1"));
assert!(result.text.contains("bucket3"));
assert!(!result.truncated);
}
#[test]
fn test_filter_s3_ls_overflow() {
let mut lines = Vec::new();
for i in 1..=50 {
lines.push(format!("2024-01-01 bucket{}", i));
}
let input = lines.join("\n");
let result = filter_s3_ls(&input);
assert!(result.text.contains("… +20 more items"));
assert!(result.truncated);
}
#[test]
fn test_filter_ec2_instances() {
let json = r#"{
"Reservations": [{
"Instances": [{
"InstanceId": "i-abc123",
"State": {"Name": "running"},
"InstanceType": "t3.micro",
"PrivateIpAddress": "10.0.1.5",
"PublicIpAddress": "54.1.2.3",
"VpcId": "vpc-001",
"SubnetId": "subnet-001",
"SecurityGroups": [{"GroupId": "sg-001", "GroupName": "web"}],
"Tags": [{"Key": "Name", "Value": "web-server"}]
}, {
"InstanceId": "i-def456",
"State": {"Name": "stopped"},
"InstanceType": "t3.large",
"PrivateIpAddress": "10.0.1.6",
"VpcId": "vpc-001",
"SubnetId": "subnet-002",
"SecurityGroups": [{"GroupId": "sg-002", "GroupName": "worker"}],
"Tags": [{"Key": "Name", "Value": "worker"}]
}]
}]
}"#;
let result = filter_ec2_instances(json).unwrap();
assert!(result.text.contains("EC2: 2 instances"));
assert!(result.text.contains("i-abc123 running t3.micro 10.0.1.5 pub:54.1.2.3 vpc:vpc-001 subnet:subnet-001 sg:[sg-001] (web-server)"));
assert!(result.text.contains("i-def456 stopped t3.large 10.0.1.6"));
assert!(result.text.contains("sg:[sg-002]"));
}
#[test]
fn test_filter_ec2_no_name_tag() {
let json = r#"{
"Reservations": [{
"Instances": [{
"InstanceId": "i-abc123",
"State": {"Name": "running"},
"InstanceType": "t3.micro",
"PrivateIpAddress": "10.0.1.5",
"Tags": []
}]
}]
}"#;
let result = filter_ec2_instances(json).unwrap();
assert!(result.text.contains("(-)"));
}
#[test]
fn test_filter_ec2_invalid_json() {
assert!(filter_ec2_instances("not json").is_none());
}
#[test]
fn test_filter_ecs_list_services() {
let json = r#"{
"serviceArns": [
"arn:aws:ecs:us-east-1:123:service/cluster/api-service",
"arn:aws:ecs:us-east-1:123:service/cluster/worker-service"
]
}"#;
let result = filter_ecs_list_services(json).unwrap();
assert!(result.text.contains("api-service"));
assert!(result.text.contains("worker-service"));
assert!(!result.text.contains("arn:aws"));
}
#[test]
fn test_filter_ecs_describe_services() {
let json = r#"{
"services": [{
"serviceName": "api",
"status": "ACTIVE",
"runningCount": 3,
"desiredCount": 3,
"launchType": "FARGATE"
}]
}"#;
let result = filter_ecs_describe_services(json).unwrap();
assert_eq!(result.text, "api ACTIVE 3/3 (FARGATE)");
}
#[test]
fn test_filter_rds_instances() {
let json = r#"{
"DBInstances": [{
"DBInstanceIdentifier": "mydb",
"Engine": "postgres",
"EngineVersion": "15.4",
"DBInstanceClass": "db.t3.micro",
"DBInstanceStatus": "available",
"Endpoint": {"Address": "mydb.cluster-abc.us-east-1.rds.amazonaws.com", "Port": 5432}
}]
}"#;
let result = filter_rds_instances(json).unwrap();
assert_eq!(result.text, "mydb postgres 15.4 db.t3.micro available mydb.cluster-abc.us-east-1.rds.amazonaws.com:5432");
}
#[test]
fn test_filter_cfn_list_stacks() {
let json = r#"{
"StackSummaries": [{
"StackName": "my-stack",
"StackStatus": "CREATE_COMPLETE",
"CreationTime": "2024-01-15T10:30:00Z"
}, {
"StackName": "other-stack",
"StackStatus": "UPDATE_COMPLETE",
"LastUpdatedTime": "2024-02-20T14:00:00Z",
"CreationTime": "2024-01-01T00:00:00Z"
}]
}"#;
let result = filter_cfn_list_stacks(json).unwrap();
assert!(result.text.contains("my-stack CREATE_COMPLETE 2024-01-15"));
assert!(result
.text
.contains("other-stack UPDATE_COMPLETE 2024-02-20"));
}
#[test]
fn test_filter_cfn_describe_stacks_with_outputs() {
let json = r#"{
"Stacks": [{
"StackName": "my-stack",
"StackStatus": "CREATE_COMPLETE",
"CreationTime": "2024-01-15T10:30:00Z",
"Outputs": [
{"OutputKey": "ApiUrl", "OutputValue": "https://api.example.com"},
{"OutputKey": "BucketName", "OutputValue": "my-bucket"}
]
}]
}"#;
let result = filter_cfn_describe_stacks(json).unwrap();
assert!(result.text.contains("my-stack CREATE_COMPLETE 2024-01-15"));
assert!(result.text.contains("ApiUrl=https://api.example.com"));
assert!(result.text.contains("BucketName=my-bucket"));
}
#[test]
fn test_filter_cfn_describe_stacks_no_outputs() {
let json = r#"{
"Stacks": [{
"StackName": "my-stack",
"StackStatus": "CREATE_COMPLETE",
"CreationTime": "2024-01-15T10:30:00Z"
}]
}"#;
let result = filter_cfn_describe_stacks(json).unwrap();
assert!(result.text.contains("my-stack CREATE_COMPLETE 2024-01-15"));
assert!(!result.text.contains("="));
}
#[test]
fn test_ec2_token_savings() {
let json = r#"{
"Reservations": [{
"ReservationId": "r-001",
"OwnerId": "123456789012",
"Groups": [],
"Instances": [{
"InstanceId": "i-0a1b2c3d4e5f00001",
"ImageId": "ami-0abcdef1234567890",
"InstanceType": "t3.micro",
"KeyName": "my-key-pair",
"LaunchTime": "2024-01-15T10:30:00+00:00",
"Placement": { "AvailabilityZone": "us-east-1a", "GroupName": "", "Tenancy": "default" },
"PrivateDnsName": "ip-10-0-1-10.ec2.internal",
"PrivateIpAddress": "10.0.1.10",
"PublicDnsName": "ec2-54-0-0-10.compute-1.amazonaws.com",
"PublicIpAddress": "54.0.0.10",
"State": { "Code": 16, "Name": "running" },
"SubnetId": "subnet-0abc123def456001",
"VpcId": "vpc-0abc123def456001",
"Architecture": "x86_64",
"BlockDeviceMappings": [{ "DeviceName": "/dev/xvda", "Ebs": { "AttachTime": "2024-01-15T10:30:05+00:00", "DeleteOnTermination": true, "Status": "attached", "VolumeId": "vol-001" } }],
"EbsOptimized": false,
"EnaSupport": true,
"Hypervisor": "xen",
"NetworkInterfaces": [{ "NetworkInterfaceId": "eni-001", "PrivateIpAddress": "10.0.1.10", "Status": "in-use" }],
"RootDeviceName": "/dev/xvda",
"RootDeviceType": "ebs",
"SecurityGroups": [{ "GroupId": "sg-001", "GroupName": "web-server-sg" }],
"SourceDestCheck": true,
"Tags": [{ "Key": "Name", "Value": "web-server-1" }, { "Key": "Environment", "Value": "production" }, { "Key": "Team", "Value": "backend" }],
"VirtualizationType": "hvm",
"CpuOptions": { "CoreCount": 1, "ThreadsPerCore": 2 },
"MetadataOptions": { "State": "applied", "HttpTokens": "required", "HttpEndpoint": "enabled" }
}]
}]
}"#;
let result = filter_ec2_instances(json).unwrap();
let input_tokens = count_tokens(json);
let output_tokens = count_tokens(&result.text);
let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0);
assert!(
savings >= 60.0,
"EC2 filter: expected >=60% savings, got {:.1}%",
savings
);
}
#[test]
fn test_sts_token_savings() {
let json = r#"{
"UserId": "AIDAEXAMPLEUSERID1234",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/dev-user"
}"#;
let result = filter_sts_identity(json).unwrap();
let input_tokens = count_tokens(json);
let output_tokens = count_tokens(&result.text);
let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0);
assert!(
savings >= 60.0,
"STS identity filter: expected >=60% savings, got {:.1}%",
savings
);
}
#[test]
fn test_rds_overflow() {
let mut dbs = Vec::new();
for i in 1..=25 {
dbs.push(format!(
r#"{{"DBInstanceIdentifier": "db-{}", "Engine": "postgres", "EngineVersion": "15.4", "DBInstanceClass": "db.t3.micro", "DBInstanceStatus": "available"}}"#,
i
));
}
let json = format!(r#"{{"DBInstances": [{}]}}"#, dbs.join(","));
let result = filter_rds_instances(&json).unwrap();
assert!(result.text.contains("… +5 more instances"));
assert!(result.truncated);
}
// === P0 filter tests ===
#[test]
fn test_filter_logs_events() {
let json = r#"{
"events": [
{"timestamp": 1705312200000, "message": "INFO: Starting service\n", "ingestionTime": 1705312201000},
{"timestamp": 1705312260000, "message": "ERROR: Connection refused\n", "ingestionTime": 1705312261000},
{"timestamp": 1705312320000, "message": "{\"level\":\"warn\",\"msg\":\"retrying\"}\n", "ingestionTime": 1705312321000}
],
"nextForwardToken": "f/1234567890abcdef1234567890abcdef",
"nextBackwardToken": "b/1234567890abcdef1234567890abcdef"
}"#;
let result = filter_logs_events(json).unwrap();
assert!(result.text.contains("INFO: Starting service"));
assert!(result.text.contains("ERROR: Connection refused"));
// JSON log message should be compacted to single line
assert!(result.text.contains("retrying"));
// Pagination tokens should NOT appear
assert!(!result.text.contains("nextForwardToken"));
assert!(!result.text.contains("f/1234567890"));
assert!(!result.truncated);
}
#[test]
fn test_filter_logs_events_truncation() {
let mut events = Vec::new();
for i in 0..60 {
events.push(format!(
r#"{{"timestamp": {}, "message": "line {}", "ingestionTime": {}}}"#,
1705312200000i64 + i * 1000,
i,
1705312200000i64 + i * 1000 + 100
));
}
let json = format!(r#"{{"events": [{}]}}"#, events.join(","));
let result = filter_logs_events(&json).unwrap();
assert!(result.text.contains("… +10 more events"));
assert!(result.truncated);
}
#[test]
fn test_filter_logs_events_token_savings() {
let mut events = Vec::new();
for i in 0..20 {
events.push(format!(
r#"{{"timestamp": {}, "message": "2024-01-15T10:30:{:02}Z INFO [com.example.service.Handler] Processing request id={} user=admin@example.com action=GET /api/v1/items?limit=100&offset=0 duration={}ms", "ingestionTime": {}}}"#,
1705312200000i64 + i * 1000,
i,
1000 + i,
50 + i * 10,
1705312200000i64 + i * 1000 + 100
));
}
let json = format!(
r#"{{"events": [{}], "nextForwardToken": "f/abcdef1234567890abcdef1234567890abcdef1234567890", "nextBackwardToken": "b/abcdef1234567890abcdef1234567890abcdef1234567890"}}"#,
events.join(",")
);
let result = filter_logs_events(&json).unwrap();
let input_tokens = count_tokens(&json);
let output_tokens = count_tokens(&result.text);
let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0);
// Logs savings come from stripping ingestionTime, pagination tokens, and JSON keys.
// With realistic fixtures the savings are modest per-event but the pagination
// tokens alone save ~20 tokens each.
assert!(
savings >= 15.0,
"Logs filter: expected >=15% savings, got {:.1}%",
savings
);
}
#[test]
fn test_filter_logs_events_invalid_json() {
assert!(filter_logs_events("not json").is_none());
}
#[test]
fn test_filter_cfn_events() {
let json = r#"{
"StackEvents": [
{
"Timestamp": "2024-01-15T10:30:00Z",
"LogicalResourceId": "MyBucket",
"ResourceType": "AWS::S3::Bucket",
"ResourceStatus": "CREATE_FAILED",
"ResourceStatusReason": "Bucket already exists",
"ResourceProperties": "{\"BucketName\":\"my-bucket\",\"VersioningConfiguration\":{\"Status\":\"Enabled\"},\"Tags\":[{\"Key\":\"Env\",\"Value\":\"prod\"}]}"
},
{
"Timestamp": "2024-01-15T10:29:00Z",
"LogicalResourceId": "MyVpc",
"ResourceType": "AWS::EC2::VPC",
"ResourceStatus": "CREATE_COMPLETE",
"ResourceProperties": "{\"CidrBlock\":\"10.0.0.0/16\"}"
},
{
"Timestamp": "2024-01-15T10:28:00Z",
"LogicalResourceId": "MyStack",
"ResourceType": "AWS::CloudFormation::Stack",
"ResourceStatus": "ROLLBACK_IN_PROGRESS",
"ResourceStatusReason": "The following resource(s) failed to create: [MyBucket]"
}
]
}"#;
let result = filter_cfn_events(json).unwrap();
assert!(result.text.contains("3 events"));
assert!(result.text.contains("2 failed"));
assert!(result.text.contains("1 successful"));
assert!(result.text.contains("FAILURES"));
assert!(result.text.contains("MyBucket"));
assert!(result.text.contains("Bucket already exists"));
// ResourceProperties should NOT appear
assert!(!result.text.contains("BucketName"));
assert!(!result.text.contains("CidrBlock"));
// AWS:: prefix stripped from resource type
assert!(result.text.contains("S3::Bucket"));
assert!(!result.text.contains("AWS::S3"));
}
#[test]
fn test_filter_cfn_events_token_savings() {
let json = r#"{
"StackEvents": [
{"Timestamp": "2024-01-15T10:30:00Z", "LogicalResourceId": "Res1", "ResourceType": "AWS::Lambda::Function", "ResourceStatus": "CREATE_FAILED", "ResourceStatusReason": "Error", "ResourceProperties": "{\"FunctionName\":\"my-fn\",\"Runtime\":\"python3.12\",\"Handler\":\"index.handler\",\"MemorySize\":512,\"Timeout\":30,\"Role\":\"arn:aws:iam::123:role/my-role\",\"Environment\":{\"Variables\":{\"TABLE\":\"my-table\"}}}"},
{"Timestamp": "2024-01-15T10:29:00Z", "LogicalResourceId": "Res2", "ResourceType": "AWS::EC2::VPC", "ResourceStatus": "CREATE_COMPLETE", "ResourceProperties": "{\"CidrBlock\":\"10.0.0.0/16\",\"EnableDnsSupport\":true,\"EnableDnsHostnames\":true}"},
{"Timestamp": "2024-01-15T10:28:00Z", "LogicalResourceId": "Res3", "ResourceType": "AWS::S3::Bucket", "ResourceStatus": "CREATE_COMPLETE", "ResourceProperties": "{\"BucketName\":\"my-bucket\",\"VersioningConfiguration\":{\"Status\":\"Enabled\"}}"}
]
}"#;
let result = filter_cfn_events(json).unwrap();
let input_tokens = count_tokens(json);
let output_tokens = count_tokens(&result.text);
let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0);
// Real CF deployments have 30+ events with huge ResourceProperties
// (stringified JSON). Small fixture shows ~46% but real-world is 90%+.
assert!(
savings >= 40.0,
"CFN events filter: expected >=40% savings, got {:.1}%",
savings
);
}
#[test]
fn test_filter_lambda_list() {
let json = r#"{
"Functions": [
{"FunctionName": "my-api", "Runtime": "python3.12", "MemorySize": 512, "Timeout": 30, "State": "Active", "Environment": {"Variables": {"SECRET_KEY": "s3cr3t", "DB_PASSWORD": "hunter2"}}},
{"FunctionName": "my-worker", "Runtime": "nodejs20.x", "MemorySize": 256, "Timeout": 60, "State": "Active"}
]
}"#;
let result = filter_lambda_list(json).unwrap();
assert!(result.text.contains("my-api python3.12 512MB 30s Active"));
assert!(result
.text
.contains("my-worker nodejs20.x 256MB 60s Active"));
// SECURITY: secrets must NOT appear
assert!(!result.text.contains("SECRET_KEY"));
assert!(!result.text.contains("s3cr3t"));
assert!(!result.text.contains("DB_PASSWORD"));
assert!(!result.text.contains("hunter2"));
assert!(!result.truncated);
}
#[test]
fn test_filter_lambda_list_token_savings() {
let json = r#"{
"Functions": [
{"FunctionName": "fn-1", "FunctionArn": "arn:aws:lambda:us-east-1:123:function:fn-1", "Runtime": "python3.12", "Role": "arn:aws:iam::123:role/role-1", "Handler": "index.handler", "CodeSize": 5242880, "Description": "A function", "Timeout": 30, "MemorySize": 512, "LastModified": "2024-01-15T10:30:00.000+0000", "CodeSha256": "abc123def456", "Version": "$LATEST", "TracingConfig": {"Mode": "Active"}, "RevisionId": "rev-123", "State": "Active", "LastUpdateStatus": "Successful", "PackageType": "Zip", "Architectures": ["x86_64"], "EphemeralStorage": {"Size": 512}, "Environment": {"Variables": {"TABLE_NAME": "my-table", "API_KEY": "secret-api-key-12345"}}}
]
}"#;
let result = filter_lambda_list(json).unwrap();
let input_tokens = count_tokens(json);
let output_tokens = count_tokens(&result.text);
let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0);
assert!(
savings >= 60.0,
"Lambda list filter: expected >=60% savings, got {:.1}%",
savings
);
}
#[test]
fn test_filter_lambda_get() {
let json = r#"{
"Configuration": {
"FunctionName": "my-api",
"Runtime": "python3.12",
"Handler": "app.handler",
"MemorySize": 512,
"Timeout": 30,
"State": "Active",
"LastModified": "2024-01-15T10:30:00.000+0000",
"Environment": {"Variables": {"SECRET": "hunter2"}},
"Layers": [
{"Arn": "arn:aws:lambda:us-east-1:123:layer:my-layer:5"},
{"Arn": "arn:aws:lambda:us-east-1:123:layer:common-utils:3"}
]
},
"Code": {"Location": "https://awslambda-us-east-1-tasks.s3.amazonaws.com/snapshots/123/my-func?versionId=abc&X-Amz-Security-Token=very-long-token"},
"Tags": {"Team": "backend"}
}"#;
let result = filter_lambda_get(json).unwrap();
assert!(result
.text
.contains("my-api python3.12 app.handler 512MB 30s Active 2024-01-15"));
assert!(result.text.contains("layers: my-layer:5, common-utils:3"));
// SECURITY
assert!(!result.text.contains("SECRET"));
assert!(!result.text.contains("hunter2"));
assert!(!result.text.contains("awslambda"));
assert!(!result.text.contains("X-Amz-Security-Token"));
}
#[test]
fn test_filter_lambda_get_no_layers() {
let json = r#"{
"Configuration": {
"FunctionName": "simple-fn",
"Runtime": "nodejs20.x",
"Handler": "index.handler",
"MemorySize": 128,
"Timeout": 10,
"State": "Active",
"LastModified": "2024-02-20T14:00:00.000+0000"
},
"Code": {"Location": "https://example.com/code"}
}"#;
let result = filter_lambda_get(json).unwrap();
assert!(result.text.contains("simple-fn"));
assert!(!result.text.contains("layers"));
}
#[test]
fn test_filter_lambda_list_invalid_json() {
assert!(filter_lambda_list("not json").is_none());
}
#[test]
fn test_filter_cfn_events_invalid_json() {
assert!(filter_cfn_events("not json").is_none());
}
// === P1 filter tests ===
#[test]
fn test_filter_iam_roles() {
let json = r#"{
"Roles": [
{"RoleName": "admin-role", "CreateDate": "2024-01-15T10:30:00Z", "Description": "Admin access", "AssumeRolePolicyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"},
{"RoleName": "lambda-exec", "CreateDate": "2024-02-20T14:00:00Z", "AssumeRolePolicyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"}
]
}"#;
let result = filter_iam_roles(json).unwrap();
assert!(result
.text
.contains("admin-role 2024-01-15 [Admin access] assume:[lambda.amazonaws.com]"));
assert!(result
.text
.contains("lambda-exec 2024-02-20 assume:[lambda.amazonaws.com]"));
// Full policy JSON should NOT appear, only extracted principals
assert!(!result.text.contains("Statement"));
assert!(!result.text.contains("Version"));
}
#[test]
fn test_filter_iam_roles_token_savings() {
let json = r#"{
"Roles": [
{"RoleName": "role-1", "RoleId": "AROA1234567890", "Arn": "arn:aws:iam::123:role/role-1", "Path": "/", "CreateDate": "2024-01-15T10:30:00Z", "MaxSessionDuration": 3600, "Description": "Test role", "AssumeRolePolicyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}", "Tags": [{"Key": "Team", "Value": "backend"}]}
]
}"#;
let result = filter_iam_roles(json).unwrap();
let input_tokens = count_tokens(json);
let output_tokens = count_tokens(&result.text);
let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0);
assert!(
savings >= 60.0,
"IAM roles filter: expected >=60% savings, got {:.1}%",
savings
);
}
#[test]
fn test_filter_iam_users() {
let json = r#"{
"Users": [
{"UserName": "alice", "UserId": "AIDA1234", "Arn": "arn:aws:iam::123:user/alice", "Path": "/", "CreateDate": "2024-01-15T10:30:00Z"},
{"UserName": "bob", "UserId": "AIDA5678", "Arn": "arn:aws:iam::123:user/bob", "Path": "/", "CreateDate": "2024-02-20T14:00:00Z"}
]
}"#;
let result = filter_iam_users(json).unwrap();
assert!(result.text.contains("alice created:2024-01-15"));
assert!(result.text.contains("bob created:2024-02-20"));
assert!(!result.text.contains("AIDA"));
assert!(!result.text.contains("arn:aws"));
}
#[test]
fn test_filter_dynamodb_items() {
let json = r#"{
"Items": [
{"id": {"S": "user-1"}, "name": {"S": "Alice"}, "age": {"N": "30"}, "active": {"BOOL": true}},
{"id": {"S": "user-2"}, "name": {"S": "Bob"}, "scores": {"L": [{"N": "100"}, {"N": "95"}]}, "meta": {"M": {"role": {"S": "admin"}}}}
],
"Count": 2,
"ScannedCount": 100
}"#;
let result = filter_dynamodb_items(json).unwrap();
assert!(result.text.contains("Count: 2/100"));
// Type wrappers should be unwrapped
assert!(result.text.contains("\"Alice\""));
assert!(result.text.contains("\"Bob\""));
assert!(!result.text.contains(r#""S""#));
assert!(!result.text.contains(r#""N""#));
assert!(!result.text.contains(r#""BOOL""#));
// Nested types should be unwrapped too
assert!(result.text.contains("\"admin\""));
}
#[test]
fn test_filter_dynamodb_token_savings() {
let json = r#"{
"Items": [
{"pk": {"S": "USER#1"}, "sk": {"S": "PROFILE"}, "name": {"S": "Alice"}, "email": {"S": "alice@example.com"}, "age": {"N": "30"}, "active": {"BOOL": true}, "tags": {"SS": ["admin", "user"]}, "meta": {"M": {"role": {"S": "admin"}, "team": {"S": "backend"}}}, "scores": {"L": [{"N": "100"}, {"N": "95"}, {"N": "88"}]}},
{"pk": {"S": "USER#2"}, "sk": {"S": "PROFILE"}, "name": {"S": "Bob"}, "email": {"S": "bob@example.com"}, "age": {"N": "25"}, "active": {"BOOL": false}, "tags": {"SS": ["user"]}, "meta": {"M": {"role": {"S": "viewer"}, "team": {"S": "frontend"}}}, "scores": {"L": [{"N": "80"}, {"N": "75"}]}}
],
"Count": 2,
"ScannedCount": 2
}"#;
let result = filter_dynamodb_items(json).unwrap();
let input_tokens = count_tokens(json);
let output_tokens = count_tokens(&result.text);
let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0);
assert!(
savings >= 30.0,
"DynamoDB filter: expected >=30% savings, got {:.1}%",
savings
);
}
#[test]
fn test_filter_dynamodb_null_type() {
let json = r#"{
"Items": [{"id": {"S": "1"}, "deleted_at": {"NULL": true}}],
"Count": 1,
"ScannedCount": 1
}"#;
let result = filter_dynamodb_items(json).unwrap();
assert!(result.text.contains("null"));
assert!(!result.text.contains("NULL"));
}
#[test]
fn test_filter_ecs_tasks() {
let json = r#"{
"tasks": [
{
"taskArn": "arn:aws:ecs:us-east-1:123:task/my-cluster/abc123def456",
"lastStatus": "RUNNING",
"desiredStatus": "RUNNING",
"containers": [
{"name": "web", "lastStatus": "RUNNING"},
{"name": "sidecar", "lastStatus": "RUNNING"}
],
"attachments": [{"id": "eni-123", "type": "ElasticNetworkInterface", "status": "ATTACHED", "details": []}],
"overrides": {"containerOverrides": []}
},
{
"taskArn": "arn:aws:ecs:us-east-1:123:task/my-cluster/def789ghi012",
"lastStatus": "STOPPED",
"stoppedReason": "Essential container in task exited",
"containers": [
{"name": "worker", "lastStatus": "STOPPED", "exitCode": 1}
],
"attachments": [],
"overrides": {}
}
]
}"#;
let result = filter_ecs_tasks(json).unwrap();
assert!(result
.text
.contains("abc123def456 RUNNING containers:[web:RUNNING, sidecar:RUNNING]"));
assert!(result
.text
.contains("def789ghi012 STOPPED containers:[worker:STOPPED(exit:1)]"));
assert!(result
.text
.contains("reason:Essential container in task exited"));
// Attachments and overrides should NOT appear
assert!(!result.text.contains("ElasticNetworkInterface"));
assert!(!result.text.contains("containerOverrides"));
}
#[test]
fn test_filter_iam_roles_invalid_json() {
assert!(filter_iam_roles("not json").is_none());
}
#[test]
fn test_filter_dynamodb_invalid_json() {
assert!(filter_dynamodb_items("not json").is_none());
}
#[test]
fn test_filter_ecs_tasks_invalid_json() {
assert!(filter_ecs_tasks("not json").is_none());
}
// === P2 filter tests ===
#[test]
fn test_filter_security_groups() {
let json = r#"{
"SecurityGroups": [{
"GroupName": "web-sg",
"GroupId": "sg-001",
"IpPermissions": [
{"IpProtocol": "tcp", "FromPort": 443, "ToPort": 443, "IpRanges": [{"CidrIp": "0.0.0.0/0"}], "Ipv6Ranges": [], "UserIdGroupPairs": []},
{"IpProtocol": "tcp", "FromPort": 22, "ToPort": 22, "IpRanges": [{"CidrIp": "10.0.0.0/8"}], "Ipv6Ranges": [], "UserIdGroupPairs": []}
],
"IpPermissionsEgress": [
{"IpProtocol": "-1", "IpRanges": [{"CidrIp": "0.0.0.0/0"}], "Ipv6Ranges": [], "UserIdGroupPairs": []}
]
}]
}"#;
let result = filter_security_groups(json).unwrap();
assert!(result.text.contains("web-sg (sg-001)"));
assert!(result.text.contains("tcp/443<-0.0.0.0/0"));
assert!(result.text.contains("tcp/22<-10.0.0.0/8"));
assert!(result.text.contains("all<-0.0.0.0/0"));
}
#[test]
fn test_filter_security_groups_token_savings() {
let json = r#"{
"SecurityGroups": [{
"GroupName": "web-sg", "GroupId": "sg-001", "Description": "Web server security group", "VpcId": "vpc-001", "OwnerId": "123456789012",
"IpPermissions": [
{"IpProtocol": "tcp", "FromPort": 443, "ToPort": 443, "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTPS from anywhere"}], "Ipv6Ranges": [{"CidrIpv6": "::/0", "Description": "HTTPS IPv6"}], "PrefixListIds": [], "UserIdGroupPairs": []},
{"IpProtocol": "tcp", "FromPort": 80, "ToPort": 80, "IpRanges": [{"CidrIp": "0.0.0.0/0", "Description": "HTTP from anywhere"}], "Ipv6Ranges": [], "PrefixListIds": [], "UserIdGroupPairs": []}
],
"IpPermissionsEgress": [{"IpProtocol": "-1", "IpRanges": [{"CidrIp": "0.0.0.0/0"}], "Ipv6Ranges": [{"CidrIpv6": "::/0"}], "PrefixListIds": [], "UserIdGroupPairs": []}],
"Tags": [{"Key": "Name", "Value": "web-sg"}, {"Key": "Environment", "Value": "production"}]
}]
}"#;
let result = filter_security_groups(json).unwrap();
let input_tokens = count_tokens(json);
let output_tokens = count_tokens(&result.text);
let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0);
assert!(
savings >= 60.0,
"SG filter: expected >=60% savings, got {:.1}%",
savings
);
}
#[test]
fn test_filter_s3_objects() {
let json = r#"{
"Contents": [
{"Key": "data/users.csv", "Size": 5242880, "LastModified": "2024-01-15T10:30:00Z", "ETag": "\"abc123\"", "StorageClass": "STANDARD"},
{"Key": "logs/app.log", "Size": 1024, "LastModified": "2024-02-20T14:00:00Z", "ETag": "\"def456\"", "StorageClass": "STANDARD"}
]
}"#;
let result = filter_s3_objects(json).unwrap();
assert!(result.text.contains("data/users.csv 5.0 MB 2024-01-15"));
assert!(result.text.contains("logs/app.log 1.0 KB 2024-02-20"));
// ETag and StorageClass should NOT appear
assert!(!result.text.contains("abc123"));
assert!(!result.text.contains("STANDARD"));
}
#[test]
fn test_filter_eks_cluster() {
let json = r#"{
"cluster": {
"name": "my-cluster",
"status": "ACTIVE",
"version": "1.28",
"endpoint": "https://ABC123.gr7.us-east-1.eks.amazonaws.com",
"certificateAuthority": {"data": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY21...VERY_LONG_BASE64_CERT_DATA"},
"logging": {"clusterLogging": [{"types": ["api","audit","authenticator","controllerManager","scheduler"], "enabled": true}]},
"platformVersion": "eks.5"
}
}"#;
let result = filter_eks_cluster(json).unwrap();
assert!(result
.text
.contains("my-cluster ACTIVE k8s/1.28 https://ABC123.gr7.us-east-1.eks.amazonaws.com"));
// certificateAuthority should NOT appear
assert!(!result.text.contains("LS0tLS1CRUdJTi"));
assert!(!result.text.contains("VERY_LONG"));
}
#[test]
fn test_filter_sqs_messages() {
let json = r#"{
"Messages": [
{
"MessageId": "12345678-abcd-efgh-ijkl-1234567890ab",
"ReceiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...VERY_LONG_RECEIPT_HANDLE_200_CHARS_OF_OPAQUE_GARBAGE_THAT_NOBODY_NEEDS",
"MD5OfBody": "abc123",
"Body": "{\"orderId\": 42, \"status\": \"pending\"}"
}
]
}"#;
let result = filter_sqs_messages(json).unwrap();
assert!(result.text.contains("12345678"));
assert!(result.text.contains("orderId"));
// ReceiptHandle should NOT appear
assert!(!result.text.contains("AQEBwJnK"));
assert!(!result.text.contains("OPAQUE_GARBAGE"));
assert!(!result.text.contains("MD5OfBody"));
}
#[test]
fn test_filter_security_groups_invalid_json() {
assert!(filter_security_groups("not json").is_none());
}
#[test]
fn test_filter_s3_objects_invalid_json() {
assert!(filter_s3_objects("not json").is_none());
}
#[test]
fn test_filter_eks_cluster_invalid_json() {
assert!(filter_eks_cluster("not json").is_none());
}
#[test]
fn test_filter_sqs_messages_invalid_json() {
assert!(filter_sqs_messages("not json").is_none());
}
#[test]
fn test_filter_dynamodb_get_item() {
let json = r#"{
"Item": {
"id": {"N": "123"},
"name": {"S": "test-item"},
"price": {"N": "19.99"},
"tags": {"L": [{"S": "new"}, {"S": "sale"}]},
"metadata": {"M": {"key": {"S": "value"}}}
},
"ConsumedCapacity": {
"CapacityUnits": 1.0
}
}"#;
let result = filter_dynamodb_get_item(json).unwrap();
assert!(result.text.contains(r#""id":123"#));
assert!(result.text.contains(r#""name":"test-item""#));
assert!(result.text.contains("Capacity: 1 RCU"));
}
#[test]
fn test_filter_dynamodb_get_item_no_item() {
let json = r#"{}"#;
assert!(filter_dynamodb_get_item(json).is_none());
}
#[test]
fn test_filter_dynamodb_get_item_invalid_json() {
assert!(filter_dynamodb_get_item("not json").is_none());
}
#[test]
fn test_filter_logs_query_results() {
let json = r#"{
"status": "Complete",
"results": [
[
{"field": "@timestamp", "value": "2024-01-01 12:00:00"},
{"field": "@message", "value": "Error occurred"},
{"field": "@ptr", "value": "internal-pointer"}
],
[
{"field": "@timestamp", "value": "2024-01-01 12:01:00"},
{"field": "@message", "value": "Another error"}
]
]
}"#;
let result = filter_logs_query_results(json).unwrap();
assert!(result.text.contains("Status: Complete"));
assert!(result.text.contains("@timestamp=2024-01-01 12:00:00"));
assert!(result.text.contains("@message=Error occurred"));
assert!(!result.text.contains("@ptr")); // Should be filtered out
}
#[test]
fn test_filter_logs_query_results_empty() {
let json = r#"{"status": "Complete", "results": []}"#;
let result = filter_logs_query_results(json).unwrap();
assert_eq!(result.text, "Status: Complete");
}
#[test]
fn test_filter_logs_query_results_invalid_json() {
assert!(filter_logs_query_results("not json").is_none());
}
#[test]
fn test_filter_s3_transfer_short_output() {
let output = "upload: file1.txt to s3://bucket/file1.txt\n";
let result = filter_s3_transfer(output);
// Short output passes through unchanged
assert_eq!(result.text, output);
}
#[test]
fn test_filter_s3_transfer_with_operations() {
let output = "\
upload: file1.txt to s3://bucket/file1.txt
upload: file2.txt to s3://bucket/file2.txt
download: s3://bucket/file3.txt to file3.txt
delete: s3://bucket/old.txt
upload: file4.txt to s3://bucket/file4.txt
upload: file5.txt to s3://bucket/file5.txt
download: s3://bucket/file6.txt to file6.txt
copy: s3://bucket/a.txt to s3://bucket/b.txt
error: failed to upload file7.txt
upload: file8.txt to s3://bucket/file8.txt
upload: file9.txt to s3://bucket/file9.txt
upload: file10.txt to s3://bucket/file10.txt
";
let result = filter_s3_transfer(output);
assert!(result.text.contains("7 uploaded"));
assert!(result.text.contains("2 downloaded"));
assert!(result.text.contains("1 deleted"));
assert!(result.text.contains("1 copied"));
assert!(result.text.contains("1 errors"));
assert!(result.text.contains("error: failed to upload file7.txt"));
}
#[test]
fn test_filter_secrets_get() {
let json = r#"{
"Name": "my-secret",
"SecretString": "{\"username\":\"admin\",\"password\":\"secret123\"}",
"ARN": "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-AbCdEf",
"VersionId": "version-uuid",
"CreatedDate": "2024-01-01T00:00:00Z"
}"#;
let result = filter_secrets_get(json).unwrap();
assert!(result.text.contains("Name: my-secret"));
assert!(result
.text
.contains(r#"{"username":"admin","password":"secret123"}"#));
assert!(!result.text.contains("ARN"));
assert!(!result.text.contains("VersionId"));
}
#[test]
fn test_filter_secrets_get_plain_text() {
let json = r#"{
"Name": "my-secret",
"SecretString": "plain-text-password"
}"#;
let result = filter_secrets_get(json).unwrap();
assert!(result.text.contains("Name: my-secret"));
assert!(result.text.contains("Secret: plain-text-password"));
}
#[test]
fn test_filter_secrets_get_invalid_json() {
assert!(filter_secrets_get("not json").is_none());
}
#[test]
fn test_dynamodb_n_type_parsing() {
// Test i64
let json = r#"{"N": "123"}"#;
let val: Value = serde_json::from_str(json).unwrap();
let result = unwrap_dynamodb_value(&val, 0);
assert_eq!(result, Value::Number(123.into()));
// Test f64
let json = r#"{"N": "123.45"}"#;
let val: Value = serde_json::from_str(json).unwrap();
let result = unwrap_dynamodb_value(&val, 0);
assert!(result.is_number());
}
#[test]
fn test_dynamodb_ns_type_parsing() {
// Test NS with integers and floats
let json = r#"{"NS": ["123", "456", "78.9"]}"#;
let val: Value = serde_json::from_str(json).unwrap();
let result = unwrap_dynamodb_value(&val, 0);
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 3);
assert_eq!(arr[0], Value::Number(123.into()));
assert_eq!(arr[1], Value::Number(456.into()));
assert!(arr[2].is_number());
}
#[test]
fn test_filter_dynamodb_items_with_capacity() {
let json = r#"{
"Items": [
{"id": {"N": "1"}, "name": {"S": "item1"}}
],
"Count": 1,
"ScannedCount": 1,
"ConsumedCapacity": {
"CapacityUnits": 2.5
}
}"#;
let result = filter_dynamodb_items(json).unwrap();
assert!(result.text.contains("Count: 1/1"));
assert!(result.text.contains("Capacity: 2.5 RCU"));
}
#[test]
fn test_filter_dynamodb_items_with_pagination() {
let json = r#"{
"Items": [
{"id": {"N": "1"}, "name": {"S": "item1"}}
],
"Count": 1,
"ScannedCount": 1,
"LastEvaluatedKey": {
"id": {"N": "1"}
}
}"#;
let result = filter_dynamodb_items(json).unwrap();
assert!(result.text.contains("Count: 1/1"));
assert!(result.text.contains("(paginated — more results available)"));
}
// === Snapshot-style tests: verify full output format ===
#[test]
fn test_snapshot_logs_events_format() {
let json = r#"{
"events": [
{"timestamp": 1705312200000, "message": "INFO: server started\n", "ingestionTime": 1705312201000},
{"timestamp": 1705312260000, "message": "ERROR: connection lost\n", "ingestionTime": 1705312261000}
],
"nextForwardToken": "f/token123"
}"#;
let result = filter_logs_events(json).unwrap();
assert_eq!(
result.text,
"2024-01-15 09:50:00 INFO: server started\n2024-01-15 09:51:00 ERROR: connection lost"
);
}
#[test]
fn test_snapshot_lambda_list_format() {
let json = r#"{"Functions": [
{"FunctionName": "api", "Runtime": "python3.12", "MemorySize": 512, "Timeout": 30, "State": "Active"}
]}"#;
let result = filter_lambda_list(json).unwrap();
assert_eq!(result.text, "api python3.12 512MB 30s Active");
}
#[test]
fn test_snapshot_dynamodb_scan_format() {
let json = r#"{"Items": [{"id": {"N": "1"}, "name": {"S": "Alice"}}], "Count": 1, "ScannedCount": 1}"#;
let result = filter_dynamodb_items(json).unwrap();
assert_eq!(result.text, "Count: 1/1\n{\"id\":1,\"name\":\"Alice\"}");
}
#[test]
fn test_snapshot_security_groups_format() {
let json = r#"{"SecurityGroups": [{
"GroupName": "web", "GroupId": "sg-1",
"IpPermissions": [{"IpProtocol": "tcp", "FromPort": 443, "ToPort": 443, "IpRanges": [{"CidrIp": "0.0.0.0/0"}], "Ipv6Ranges": [], "UserIdGroupPairs": []}],
"IpPermissionsEgress": [{"IpProtocol": "-1", "IpRanges": [{"CidrIp": "0.0.0.0/0"}], "Ipv6Ranges": [], "UserIdGroupPairs": []}]
}]}"#;
let result = filter_security_groups(json).unwrap();
assert_eq!(
result.text,
"web (sg-1) ingress: tcp/443<-0.0.0.0/0 | egress: all<-0.0.0.0/0"
);
}
#[test]
fn test_snapshot_cfn_events_format() {
let json = r#"{"StackEvents": [
{"Timestamp": "2024-01-15T10:30:00Z", "LogicalResourceId": "Bucket", "ResourceType": "AWS::S3::Bucket", "ResourceStatus": "CREATE_FAILED", "ResourceStatusReason": "Already exists"},
{"Timestamp": "2024-01-15T10:29:00Z", "LogicalResourceId": "VPC", "ResourceType": "AWS::EC2::VPC", "ResourceStatus": "CREATE_COMPLETE"}
]}"#;
let result = filter_cfn_events(json).unwrap();
assert!(result
.text
.starts_with("CloudFormation: 2 events (1 failed, 1 successful)"));
assert!(result.text.contains("--- FAILURES ---"));
assert!(result
.text
.contains("Bucket S3::Bucket CREATE_FAILED REASON: Already exists"));
}
// === Empty collection edge cases ===
#[test]
fn test_filter_lambda_list_empty() {
let json = r#"{"Functions": []}"#;
let result = filter_lambda_list(json).unwrap();
assert_eq!(result.text, "");
}
#[test]
fn test_filter_iam_roles_empty() {
let json = r#"{"Roles": []}"#;
let result = filter_iam_roles(json).unwrap();
assert_eq!(result.text, "");
}
#[test]
fn test_filter_iam_users_empty() {
let json = r#"{"Users": []}"#;
let result = filter_iam_users(json).unwrap();
assert_eq!(result.text, "");
}
#[test]
fn test_filter_dynamodb_items_empty() {
let json = r#"{"Items": [], "Count": 0, "ScannedCount": 0}"#;
let result = filter_dynamodb_items(json).unwrap();
assert_eq!(result.text, "Count: 0/0");
}
#[test]
fn test_filter_ecs_tasks_empty() {
let json = r#"{"tasks": []}"#;
let result = filter_ecs_tasks(json).unwrap();
assert_eq!(result.text, "");
}
#[test]
fn test_filter_security_groups_empty() {
let json = r#"{"SecurityGroups": []}"#;
let result = filter_security_groups(json).unwrap();
assert_eq!(result.text, "");
}
#[test]
fn test_filter_s3_objects_empty() {
let json = r#"{}"#;
let result = filter_s3_objects(json).unwrap();
assert_eq!(result.text, "");
}
#[test]
fn test_filter_sqs_messages_empty() {
let json = r#"{}"#;
let result = filter_sqs_messages(json).unwrap();
assert_eq!(result.text, "");
}
#[test]
fn test_filter_logs_events_empty() {
let json = r#"{"events": []}"#;
let result = filter_logs_events(json).unwrap();
assert_eq!(result.text, "");
}
#[test]
fn test_filter_ec2_instances_empty() {
let json = r#"{"Reservations": []}"#;
let result = filter_ec2_instances(json).unwrap();
assert_eq!(result.text, "EC2: 0 instances");
}
#[test]
fn test_filter_cfn_events_empty() {
let json = r#"{"StackEvents": []}"#;
let result = filter_cfn_events(json).unwrap();
assert_eq!(
result.text,
"CloudFormation: 0 events (0 failed, 0 successful)"
);
}
#[test]
fn test_filter_cfn_events_failure_count_exceeds_max_items() {
// Verify that failed_count reports the real count, not the capped collection size
let mut events = Vec::new();
for i in 0..30 {
events.push(format!(
r#"{{"Timestamp": "2024-01-15T10:30:00Z", "LogicalResourceId": "Res{}", "ResourceType": "AWS::Lambda::Function", "ResourceStatus": "CREATE_FAILED", "ResourceStatusReason": "Error {}", "ResourceProperties": "{{}}"}}"#,
i, i
));
}
let json = format!(r#"{{"StackEvents": [{}]}}"#, events.join(","));
let result = filter_cfn_events(&json).unwrap();
// Should report all 30 failures, not capped at MAX_ITEMS (20)
assert!(result.text.contains("30 failed"));
}
// Regression: generic AWS path (unsupported subcommand returning JSON) must
// compress responses while preserving values, not collapse them to schema
// type names. Calls the primitive used at aws_cmd.rs run_generic line 259.
#[test]
fn test_aws_unsupported_subcommand_json_preserves_values() {
let fixture = include_str!(
"../../../tests/fixtures/aws_backup_describe_global_settings.json"
);
let output = json_cmd::filter_json_compact(fixture, JSON_COMPRESS_DEPTH)
.expect("filter_json_compact must not error on valid AWS JSON");
assert!(
output.contains("\"false\""),
"values must be preserved (expected literal \"false\"), got:\n{output}"
);
assert!(
!output.contains(": string"),
"schema-type leakage detected (\": string\" found), got:\n{output}"
);
assert!(
output.contains("isMpaEnabled"),
"object keys must be preserved, got:\n{output}"
);
}
}