Files
fermata/tests/core_policy_botsecrets.rs
g4borg 168aefd415 🏗️ fermata: redaction-first security model, unified .botsecrets config
Realign fermata around redaction (PostToolUse) as the primary security
layer, with access control (PreToolUse) as supplementary write/bash
protection. Remove botignore.toml — policy rules now live in .botsecrets
[policy] section. Add fermata.toml as an alias for .botsecrets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 01:10:07 +02:00

183 lines
5.2 KiB
Rust

//! Tests for `.botsecrets [policy]` section integration with `Policy`,
//! including `fermata.toml` alias support.
use dirigent_fermata::core::{Decision, Op, Policy};
use std::fs;
use tempfile::TempDir;
/// Helper: create a temp project dir with optional `.botignore` and `.botsecrets` files.
fn make_project(botignore: &str, botsecrets: &str) -> TempDir {
let tmp = TempDir::new().unwrap();
if !botignore.is_empty() {
fs::write(tmp.path().join(".botignore"), botignore).unwrap();
}
if !botsecrets.is_empty() {
fs::write(tmp.path().join(".botsecrets"), botsecrets).unwrap();
}
tmp
}
#[test]
fn botsecrets_policy_write_works() {
let tmp = make_project(
"",
r#"
[policy.write]
patterns = ["vendor/**"]
"#,
);
let policy = Policy::load(tmp.path()).unwrap();
let target = tmp.path().join("vendor/lib.rs");
fs::create_dir_all(target.parent().unwrap()).unwrap();
fs::write(&target, "").unwrap();
// Write to vendor/** should be denied via .botsecrets [policy.write]
assert!(matches!(
policy.check(Op::Write, &target).unwrap(),
Decision::Deny(_)
));
// Read should still be allowed (no read rules)
assert_eq!(policy.check(Op::Read, &target).unwrap(), Decision::Allow);
}
#[test]
fn botsecrets_policy_read_works() {
let tmp = make_project(
"",
r#"
[policy.read]
patterns = ["secrets/**"]
"#,
);
let policy = Policy::load(tmp.path()).unwrap();
let target = tmp.path().join("secrets/key.pem");
fs::create_dir_all(target.parent().unwrap()).unwrap();
fs::write(&target, "").unwrap();
assert!(matches!(
policy.check(Op::Read, &target).unwrap(),
Decision::Deny(_)
));
assert_eq!(policy.check(Op::Write, &target).unwrap(), Decision::Allow);
}
#[test]
fn botsecrets_without_policy_section_does_not_affect_policy() {
let tmp = make_project(
"",
r#"
[keys]
include = ["MY_CUSTOM_KEY"]
"#,
);
let policy = Policy::load(tmp.path()).unwrap();
// Everything should be allowed — no policy rules
let target = tmp.path().join("vendor/lib.rs");
fs::create_dir_all(target.parent().unwrap()).unwrap();
fs::write(&target, "").unwrap();
assert_eq!(policy.check(Op::Read, &target).unwrap(), Decision::Allow);
assert_eq!(policy.check(Op::Write, &target).unwrap(), Decision::Allow);
}
#[test]
fn botsecrets_policy_bash_deny_works() {
let tmp = make_project(
"",
r#"
[policy.bash]
deny = ["rm -rf /"]
ask = ["git push"]
"#,
);
let policy = Policy::load(tmp.path()).unwrap();
let d = policy.check_command("rm -rf /").unwrap();
assert!(matches!(d, Decision::Deny(_)));
let d = policy.check_command("git push origin main").unwrap();
assert!(matches!(d, Decision::Ask(_)));
let d = policy.check_command("cargo build").unwrap();
assert_eq!(d, Decision::Allow);
}
#[test]
fn malformed_botsecrets_falls_back_to_defaults() {
// .botsecrets is malformed — should still load with empty policy (fail-open)
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join(".botsecrets"), "this is not valid toml [[[").unwrap();
let policy = Policy::load(tmp.path()).unwrap();
let target = tmp.path().join("anything.rs");
fs::write(&target, "").unwrap();
assert_eq!(policy.check(Op::Read, &target).unwrap(), Decision::Allow);
}
#[test]
fn fermata_toml_works_as_alias() {
// No .botsecrets, but fermata.toml exists — should be loaded
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join("fermata.toml"),
r#"
[policy.write]
patterns = ["generated/**"]
"#,
)
.unwrap();
let policy = Policy::load(tmp.path()).unwrap();
let target = tmp.path().join("generated/output.rs");
fs::create_dir_all(target.parent().unwrap()).unwrap();
fs::write(&target, "").unwrap();
assert!(matches!(
policy.check(Op::Write, &target).unwrap(),
Decision::Deny(_)
));
}
#[test]
fn botsecrets_takes_priority_over_fermata_toml() {
// Both .botsecrets and fermata.toml exist.
// .botsecrets blocks writes to vendor/**, fermata.toml blocks writes to src/**
// Only .botsecrets rules should apply.
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join(".botsecrets"),
r#"
[policy.write]
patterns = ["vendor/**"]
"#,
)
.unwrap();
fs::write(
tmp.path().join("fermata.toml"),
r#"
[policy.write]
patterns = ["src/**"]
"#,
)
.unwrap();
let policy = Policy::load(tmp.path()).unwrap();
// vendor/** should be blocked (from .botsecrets)
let vendor_target = tmp.path().join("vendor/lib.rs");
fs::create_dir_all(vendor_target.parent().unwrap()).unwrap();
fs::write(&vendor_target, "").unwrap();
assert!(matches!(
policy.check(Op::Write, &vendor_target).unwrap(),
Decision::Deny(_)
));
// src/** should NOT be blocked (fermata.toml was ignored because .botsecrets exists)
let src_target = tmp.path().join("src/main.rs");
fs::create_dir_all(src_target.parent().unwrap()).unwrap();
fs::write(&src_target, "").unwrap();
assert_eq!(
policy.check(Op::Write, &src_target).unwrap(),
Decision::Allow
);
}