bctx-weave 0.1.17

bctx-weave — FilterMesh lens pipeline, CLI interception, domain compression
Documentation
use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;

#[allow(dead_code)]
static CF_RESOURCE_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^\d{4}-\d{2}-\d{2}T[^\s]+ [A-Z_]+ [A-Z_]+ [^\n]+\n?").unwrap());
static PROGRESS_BAR_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^\s*[-=]{3,}[^\n]*\n?").unwrap());

// ── aws cloudformation ────────────────────────────────────────────────────────

pub fn compress_cloudformation(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // Keep only FAILED/ROLLBACK/COMPLETE status lines, strip IN_PROGRESS noise
    let out: Vec<&str> = cleaned
        .lines()
        .filter(|l| !l.contains("_IN_PROGRESS") || l.contains("FAILED") || l.contains("ROLLBACK"))
        .collect();
    compactor::collapse_blanks(&out.join("\n"))
}

// ── aws s3 cp / sync ──────────────────────────────────────────────────────────

pub fn compress_s3(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = PROGRESS_BAR_RE.replace_all(&cleaned, "");
    // Keep "upload:" / "download:" / "copy:" / "delete:" lines + errors
    let out: Vec<&str> = s
        .lines()
        .filter(|l| {
            l.trim_start().starts_with("upload:")
                || l.trim_start().starts_with("download:")
                || l.trim_start().starts_with("copy:")
                || l.trim_start().starts_with("delete:")
                || l.contains("error")
                || l.contains("Error")
                || l.contains("fatal")
        })
        .collect();
    if out.is_empty() {
        s.into_owned()
    } else {
        out.join("\n")
    }
}

// ── aws ec2 / generic json ────────────────────────────────────────────────────

pub fn compress_generic_aws(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // Detect large JSON responses and truncate
    let lines: Vec<&str> = cleaned.lines().collect();
    if lines.len() > 40 {
        return format!(
            "{}\n... [{} more lines — use --query to filter] ...",
            lines[..40].join("\n"),
            lines.len() - 40
        );
    }
    cleaned
}

// ── aws logs (CloudWatch) ─────────────────────────────────────────────────────

pub fn compress_logs(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    super::super::sys::log_dedup(&cleaned)
}

// ── aws lambda ────────────────────────────────────────────────────────────────

pub fn compress_lambda(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    // Strip verbose fields rarely useful in task context
    let noisy: &[&str] = &[
        "\"FunctionArn\"",
        "\"Role\"",
        "\"CodeSha256\"",
        "\"RevisionId\"",
        "\"LastModified\"",
        "\"CodeSize\"",
        "\"Layers\"",
        "\"VpcConfig\"",
        "\"TracingConfig\"",
    ];
    let out: Vec<&str> = cleaned
        .lines()
        .filter(|l| noisy.iter().all(|n| !l.contains(n)))
        .collect();
    compactor::collapse_blanks(&out.join("\n"))
}

// ── aws ecs ───────────────────────────────────────────────────────────────────

pub fn compress_ecs(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
    if lines.len() <= 30 {
        return lines.join("\n");
    }
    format!(
        "{}\n... [{} more lines] ...",
        lines[..30].join("\n"),
        lines.len() - 30
    )
}

// ── aws iam ───────────────────────────────────────────────────────────────────

pub fn compress_iam(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);

    // Detect assume-role output: contains SecretAccessKey — redact credentials
    if cleaned.contains("\"SecretAccessKey\"") || cleaned.contains("\"SessionToken\"") {
        return compress_iam_assume_role(&cleaned);
    }

    // Detect list-* output: arrays of policy/role/user objects
    if cleaned.contains("\"Arn\"")
        && (cleaned.contains("\"PolicyName\"")
            || cleaned.contains("\"RoleName\"")
            || cleaned.contains("\"UserName\"")
            || cleaned.contains("\"GroupName\""))
    {
        return compress_iam_list(&cleaned);
    }

    // Policy document / role detail: strip Statement internals
    let noisy: &[&str] = &[
        "\"PolicyDocument\":",
        "\"AssumeRolePolicyDocument\":",
        "\"Statement\":",
        "\"Action\":",
        "\"Effect\":",
        "\"Resource\":",
        "\"Condition\":",
        "\"Principal\":",
        "\"Path\":",
        "\"PolicyId\":",
        "\"AttachmentCount\":",
        "\"PermissionsBoundaryUsageCount\":",
        "\"IsAttachable\":",
        "\"DefaultVersionId\":",
    ];
    let out: Vec<&str> = cleaned
        .lines()
        .filter(|l| noisy.iter().all(|n| !l.trim().starts_with(n)))
        .collect();
    let lines = out.as_slice();
    if lines.len() <= 40 {
        return lines.join("\n");
    }
    format!(
        "{}\n… [{} more lines — use --query to filter] …",
        lines[..40].join("\n"),
        lines.len() - 40
    )
}

fn compress_iam_list(raw: &str) -> String {
    // Extract name+arn per entry; count and summarise
    use once_cell::sync::Lazy;
    use regex::Regex;
    static NAME_RE: Lazy<Regex> = Lazy::new(|| {
        Regex::new(r#""(PolicyName|RoleName|UserName|GroupName)"\s*:\s*"([^"]+)""#).unwrap()
    });
    static ARN_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#""Arn"\s*:\s*"([^"]+)""#).unwrap());

    let names: Vec<&str> = NAME_RE
        .captures_iter(raw)
        .filter_map(|c| c.get(2).map(|m| m.as_str()))
        .collect();
    let arns: Vec<&str> = ARN_RE
        .captures_iter(raw)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .collect();

    if names.is_empty() {
        return compactor::collapse_blanks(raw);
    }

    let count = names.len();
    let lines: Vec<String> = names
        .iter()
        .zip(arns.iter().chain(std::iter::repeat(&"")))
        .map(|(name, arn)| {
            if arn.is_empty() {
                (*name).to_string()
            } else {
                format!("{name}  {arn}")
            }
        })
        .take(20)
        .collect();

    let mut result = lines.join("\n");
    if count > 20 {
        result.push_str(&format!("\n{} more (use --query to filter)", count - 20));
    } else {
        result.push_str(&format!("\n({count} total)"));
    }
    result
}

fn compress_iam_assume_role(raw: &str) -> String {
    use once_cell::sync::Lazy;
    use regex::Regex;
    static KEY_RE: Lazy<Regex> =
        Lazy::new(|| Regex::new(r#""AccessKeyId"\s*:\s*"([^"]{8})[^"]*""#).unwrap());
    static EXP_RE: Lazy<Regex> =
        Lazy::new(|| Regex::new(r#""Expiration"\s*:\s*"([^"]+)""#).unwrap());
    static ARN_RE: Lazy<Regex> =
        Lazy::new(|| Regex::new(r#""AssumedRoleId"\s*:\s*"([^"]+)""#).unwrap());

    let key = KEY_RE
        .captures(raw)
        .map(|c| format!("{}", &c[1]))
        .unwrap_or_else(|| "REDACTED".to_string());
    let expiry = EXP_RE
        .captures(raw)
        .map(|c| c[1].to_string())
        .unwrap_or_else(|| "unknown".to_string());
    let role = ARN_RE
        .captures(raw)
        .map(|c| c[1].to_string())
        .unwrap_or_else(|| "role".to_string());

    format!(
        "sts:AssumeRole — AccessKeyId: {key}  Expiration: {expiry}  Role: {role}\n(credentials redacted)"
    )
}

// ── aws sts (caller identity / assume-role) ───────────────────────────────────

pub fn compress_sts(raw: &str) -> String {
    if raw.contains("\"SecretAccessKey\"") {
        return compress_iam_assume_role(raw);
    }
    compactor::normalise(raw)
}

// ── top-level dispatcher ──────────────────────────────────────────────────────

pub fn compress_aws(subcmd: &str, raw: &str) -> String {
    let sub = subcmd.trim();
    if sub.starts_with("cloudformation") {
        return compress_cloudformation(raw);
    }
    if sub.starts_with("s3") {
        return compress_s3(raw);
    }
    if sub.starts_with("logs") {
        return compress_logs(raw);
    }
    if sub.starts_with("lambda") {
        return compress_lambda(raw);
    }
    if sub.starts_with("ecs") {
        return compress_ecs(raw);
    }
    if sub.starts_with("iam") {
        return compress_iam(raw);
    }
    if sub.starts_with("sts") {
        return compress_sts(raw);
    }
    compress_generic_aws(raw)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cloudformation_strips_in_progress() {
        let raw = "MyBucket AWS::S3::Bucket CREATE_IN_PROGRESS\nMyBucket AWS::S3::Bucket CREATE_COMPLETE\n";
        let out = compress_cloudformation(raw);
        assert!(!out.contains("CREATE_IN_PROGRESS"), "{out}");
        assert!(out.contains("CREATE_COMPLETE"));
    }

    #[test]
    fn s3_keeps_upload_lines() {
        let raw =
            "upload: ./foo.txt to s3://bucket/foo.txt\nCompleted 1.0 MiB/1.0 MiB (500 KiB/s)\n";
        let out = compress_s3(raw);
        assert!(out.contains("upload:"));
    }

    #[test]
    fn generic_truncates_large_json() {
        let raw = (0..50)
            .map(|i| format!("  \"key{i}\": \"value{i}\","))
            .collect::<Vec<_>>()
            .join("\n");
        let out = compress_generic_aws(&raw);
        assert!(out.contains("more lines"), "{out}");
    }

    #[test]
    fn lambda_strips_noisy_fields() {
        let raw = "{\n  \"FunctionName\": \"my-fn\",\n  \"FunctionArn\": \"arn:aws:lambda:us-east-1:123:function:my-fn\",\n  \"Runtime\": \"nodejs18.x\",\n  \"Role\": \"arn:aws:iam::123:role/role\",\n  \"CodeSha256\": \"abc123\"\n}\n";
        let out = compress_lambda(raw);
        assert!(!out.contains("FunctionArn"), "{out}");
        assert!(!out.contains("CodeSha256"), "{out}");
        assert!(out.contains("FunctionName"), "{out}");
        assert!(out.contains("Runtime"), "{out}");
    }

    #[test]
    fn logs_deduplicates_repeated_entries() {
        let raw = "[ERROR] timeout\n[ERROR] timeout\n[ERROR] timeout\n[INFO] recovered\n";
        let out = compress_logs(raw);
        assert!(
            out.contains("repeated") || out.contains("[ERROR] timeout"),
            "{out}"
        );
        assert!(out.contains("[INFO] recovered"), "{out}");
    }

    #[test]
    fn iam_list_policies_extracts_names() {
        let raw = r#"{"Policies":[{"PolicyName":"AdminPolicy","Arn":"arn:aws:iam::123456789:policy/AdminPolicy","AttachmentCount":3,"IsAttachable":true},{"PolicyName":"ReadOnly","Arn":"arn:aws:iam::123456789:policy/ReadOnly","AttachmentCount":1,"IsAttachable":true}]}"#;
        let out = compress_iam(raw);
        assert!(out.contains("AdminPolicy"), "{out}");
        assert!(out.contains("ReadOnly"), "{out}");
        assert!(!out.contains("AttachmentCount"), "{out}");
    }

    #[test]
    fn iam_assume_role_redacts_credentials() {
        let raw = r#"{"Credentials":{"AccessKeyId":"ASIAIOSFODNN7EXAMPLE","SecretAccessKey":"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY","SessionToken":"AQoXnyc4lcK4w//abcdef","Expiration":"2026-06-01T12:00:00Z"},"AssumedRoleId":"AROAIOSFODNN7EXAMPLE:session"}"#;
        let out = compress_iam(raw);
        assert!(!out.contains("SecretAccessKey"), "{out}");
        assert!(!out.contains("SessionToken"), "{out}");
        assert!(
            out.contains("ASIAIOSF") || out.contains("REDACTED"),
            "{out}"
        );
        assert!(out.contains("Expiration") || out.contains("2026"), "{out}");
    }

    #[test]
    fn iam_policy_document_strips_statement_fields() {
        let raw = r#"{"Policy":{"PolicyName":"MyPolicy","PolicyId":"ANPAI3VAJF5ZCREXAMPLE","Arn":"arn:aws:iam::123:policy/MyPolicy","Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":"*"}]}}"#;
        let out = compress_iam(raw);
        assert!(out.contains("MyPolicy"), "{out}");
        assert!(!out.contains("\"Effect\""), "{out}");
        assert!(!out.contains("\"Action\""), "{out}");
    }

    #[test]
    fn sts_redacts_assume_role_credentials() {
        let raw = r#"{"Credentials":{"AccessKeyId":"TESTIOSFODNN7EXAMPLE","SecretAccessKey":"secret","SessionToken":"token","Expiration":"2026-06-01T00:00:00Z"}}"#;
        let out = compress_sts(raw);
        assert!(!out.contains("SecretAccessKey"), "{out}");
        assert!(!out.contains("secret"), "{out}");
    }
}