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"];
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}'",
)
}
}
}