🏗️ 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>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
//! 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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user