koala-core 1.0.4

Shared types, invariant evaluator, and primitives for the koala framework.
Documentation
use crate::invariant::rules::util::{rel, walk_repo};
use crate::invariant::{Category, Context, Invariant, Outcome};
use regex::Regex;
use std::fs;
use std::sync::OnceLock;

const SCAN_EXTS: &[&str] = &["rs", "toml", "md", "yml", "yaml", "json", "sh", "txt"];
// Skip files where the literal pattern appears as documentation (this rule's own source / hook / CLAUDE refs).
const SKIP_FILES: &[&str] = &[
    "crates/koala-core/src/invariant/rules/no_aws_key_pattern.rs",
    ".githooks/pre-commit",
    "crates/koala-cli/templates/.githooks/pre-commit",
];

fn pattern() -> &'static Regex {
    static R: OnceLock<Regex> = OnceLock::new();
    R.get_or_init(|| Regex::new(r"AKIA[0-9A-Z]{16}").expect("static pattern compiles"))
}

pub struct NoAwsKeyPattern;

impl Invariant for NoAwsKeyPattern {
    fn id(&self) -> &'static str {
        "security.no-aws-key-pattern"
    }
    fn category(&self) -> Category {
        Category::Security
    }
    fn intent(&self) -> &'static str {
        "No AWS access-key pattern (AKIA + 16 alnum) in repository (defense in depth alongside the pre-commit hook)."
    }
    fn adr(&self) -> Option<&'static str> {
        Some("ADR-0013")
    }

    fn evaluate(&self, ctx: &Context) -> Outcome {
        let re = pattern();
        let mut hits = Vec::new();
        for entry in walk_repo(ctx.root()).filter(|e| e.file_type().is_file()) {
            let path = entry.path();
            let ext_matches = path
                .extension()
                .and_then(|s| s.to_str())
                .is_some_and(|ext| SCAN_EXTS.contains(&ext));
            if !ext_matches {
                continue;
            }
            let r = rel(path, ctx.root());
            if SKIP_FILES.iter().any(|s| r == *s) {
                continue;
            }
            let Ok(content) = fs::read_to_string(path) else {
                continue;
            };
            for (i, line) in content.lines().enumerate() {
                if re.is_match(line) {
                    hits.push(format!("{}:{}", r, i + 1));
                }
            }
        }
        if hits.is_empty() {
            Outcome::pass()
        } else {
            Outcome::fail_repro(
                format!("AWS key pattern found:\n  {}", hits.join("\n  ")),
                "rg -nP 'AKIA[0-9A-Z]{16}'",
            )
        }
    }
}