168aefd415
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>
183 lines
5.2 KiB
Rust
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
|
|
);
|
|
}
|