Hand over the M3a plan: 22 tasks, their files, and the check record
Task files, the files they copy in (byte-identical to the reference on m3a-ref), each area's check record, and a README with the per-task table of what each check exposed. The handoff note is done with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
//! Table tests for `policy::decide`: which arguments each tool's grants cover. Do not edit.
|
||||
//! `policy_matching.rs` covers how a winner, a label and a reason are picked, `policy_redecide.rs`
|
||||
//! covers approvals, and `policy_property.rs` checks all of it against an oracle.
|
||||
|
||||
#[path = "support/build.rs"]
|
||||
mod build;
|
||||
|
||||
use brokerd::policy::decide;
|
||||
use build::{allowed, fetch, grant, now, private, read, reason, request, set, shell, write};
|
||||
use proto::{DenyReason, Mode};
|
||||
|
||||
#[test]
|
||||
fn no_grants_means_no_grant() {
|
||||
let none = set(vec![]);
|
||||
for req in [
|
||||
read("/etc/hosts"),
|
||||
write("/tmp/x"),
|
||||
shell(None),
|
||||
fetch("https://example.com/"),
|
||||
] {
|
||||
assert_eq!(
|
||||
reason(decide(req, &none, private(), now())),
|
||||
DenyReason::NoGrant
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tool_that_is_not_one_of_the_four_is_no_grant_and_its_arguments_are_not_parsed() {
|
||||
let grants = set(vec![grant("s", "shell", Mode::Auto)]);
|
||||
for tool in ["echo", "clock", "call_tool", "", "Shell"] {
|
||||
for arguments in ["{}", "not json", r#"{"command":"ls"}"#] {
|
||||
let outcome = decide(request(tool, arguments), &grants, private(), now());
|
||||
assert_eq!(
|
||||
reason(outcome),
|
||||
DenyReason::NoGrant,
|
||||
"{tool:?} {arguments:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Invalid arguments are refused before matching, so the answer is the same with a grant that
|
||||
/// would cover them, with a `deny` grant, and with no grant at all.
|
||||
#[test]
|
||||
fn invalid_arguments_are_refused_before_matching() {
|
||||
let covering = set(vec![
|
||||
grant("r", "read_file", Mode::Auto).paths(&["/home/kyle/notes"]),
|
||||
grant("no", "read_file", Mode::Deny).paths(&["/home/kyle"]),
|
||||
]);
|
||||
let none = set(vec![]);
|
||||
for grants in [&covering, &none] {
|
||||
for req in [
|
||||
// The rows of the "Paths" table that are about form.
|
||||
read("/home/kyle/notes/../.ssh/id"),
|
||||
read("notes/a.md"),
|
||||
read("/home/kyle//notes/./a.md"),
|
||||
request("read_file", "{}"),
|
||||
request(
|
||||
"read_file",
|
||||
r#"{"path":"/home/kyle/notes/a.md","mode":"r"}"#,
|
||||
),
|
||||
request("read_file", "not json"),
|
||||
write("/home/kyle/notes/"),
|
||||
shell(Some("relative")),
|
||||
fetch("http://example.com/"),
|
||||
fetch("https://127.0.0.1/"),
|
||||
fetch("https://user@example.com/"),
|
||||
] {
|
||||
let text = req.arguments.clone();
|
||||
assert_eq!(
|
||||
reason(decide(req, grants, private(), now())),
|
||||
DenyReason::InvalidArguments,
|
||||
"{text}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The "Paths" table, the rows about containment.
|
||||
#[test]
|
||||
fn read_file_is_covered_inside_a_granted_path() {
|
||||
let grants = set(vec![
|
||||
grant("notes", "read_file", Mode::Auto).paths(&["/home/kyle/notes"]),
|
||||
]);
|
||||
for path in [
|
||||
"/home/kyle/notes/a.md",
|
||||
"/home/kyle/notes",
|
||||
"/home/kyle/notes/x/y/z",
|
||||
] {
|
||||
let d = allowed(decide(read(path), &grants, private(), now()));
|
||||
assert_eq!(d.grant(), "notes");
|
||||
assert_eq!(d.matched_path(), Some("/home/kyle/notes"));
|
||||
}
|
||||
for path in [
|
||||
"/home/kyle/notes2/a.md",
|
||||
"/home/kyle",
|
||||
"/",
|
||||
"/etc/passwd",
|
||||
"/home/kyle/note",
|
||||
] {
|
||||
assert_eq!(
|
||||
reason(decide(read(path), &grants, private(), now())),
|
||||
DenyReason::NoGrant,
|
||||
"{path}"
|
||||
);
|
||||
}
|
||||
// A grant is for one tool.
|
||||
let outcome = decide(write("/home/kyle/notes/a.md"), &grants, private(), now());
|
||||
assert_eq!(reason(outcome), DenyReason::NoGrant);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_file_is_covered_inside_a_granted_path_but_not_at_the_path_itself() {
|
||||
let grants = set(vec![
|
||||
grant("scratch", "write_file", Mode::Auto)
|
||||
.paths(&["/home/kyle/scratch", "/home/kyle/scratch/out"]),
|
||||
]);
|
||||
let d = allowed(decide(
|
||||
write("/home/kyle/scratch/a.txt"),
|
||||
&grants,
|
||||
private(),
|
||||
now(),
|
||||
));
|
||||
assert_eq!(d.matched_path(), Some("/home/kyle/scratch"));
|
||||
// The longest path that holds the argument is the matched one.
|
||||
let d = allowed(decide(
|
||||
write("/home/kyle/scratch/out/b.txt"),
|
||||
&grants,
|
||||
private(),
|
||||
now(),
|
||||
));
|
||||
assert_eq!(d.matched_path(), Some("/home/kyle/scratch/out"));
|
||||
// A granted path itself cannot be written, but it can lie inside another granted path.
|
||||
let d = allowed(decide(
|
||||
write("/home/kyle/scratch/out"),
|
||||
&grants,
|
||||
private(),
|
||||
now(),
|
||||
));
|
||||
assert_eq!(d.matched_path(), Some("/home/kyle/scratch"));
|
||||
let outcome = decide(write("/home/kyle/scratch"), &grants, private(), now());
|
||||
assert_eq!(reason(outcome), DenyReason::NoGrant);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_is_covered_by_no_paths_and_no_cwd_or_by_a_cwd_inside_a_path() {
|
||||
let bare = set(vec![grant("bare", "shell", Mode::Auto)]);
|
||||
let d = allowed(decide(shell(None), &bare, private(), now()));
|
||||
assert_eq!((d.matched_path(), d.paths().len()), (None, 0));
|
||||
assert_eq!(
|
||||
reason(decide(shell(Some("/home/kyle")), &bare, private(), now())),
|
||||
DenyReason::NoGrant
|
||||
);
|
||||
|
||||
let scoped = set(vec![
|
||||
grant("scoped", "shell", Mode::Auto).paths(&["/home/kyle/a", "/srv/b"]),
|
||||
]);
|
||||
let d = allowed(decide(shell(Some("/srv/b/sub")), &scoped, private(), now()));
|
||||
assert_eq!(d.matched_path(), Some("/srv/b"));
|
||||
// The runner mounts every path of the grant, so the decision carries them all.
|
||||
assert_eq!(d.paths(), ["/home/kyle/a", "/srv/b"]);
|
||||
assert_eq!(
|
||||
reason(decide(shell(None), &scoped, private(), now())),
|
||||
DenyReason::NoGrant
|
||||
);
|
||||
assert_eq!(
|
||||
reason(decide(shell(Some("/srv")), &scoped, private(), now())),
|
||||
DenyReason::NoGrant
|
||||
);
|
||||
}
|
||||
|
||||
/// The "Hosts" table, row by row.
|
||||
#[test]
|
||||
fn http_fetch_is_covered_when_the_host_matches() {
|
||||
let exact = set(vec![
|
||||
grant("exact", "http_fetch", Mode::Auto).hosts(&["example.com"]),
|
||||
]);
|
||||
let wild = set(vec![
|
||||
grant("wild", "http_fetch", Mode::Auto).hosts(&["*.example.com"]),
|
||||
]);
|
||||
let d = allowed(decide(
|
||||
fetch("https://example.com/a?b=c"),
|
||||
&exact,
|
||||
private(),
|
||||
now(),
|
||||
));
|
||||
assert_eq!(d.hosts(), ["example.com"]);
|
||||
assert_eq!(d.matched_path(), None);
|
||||
assert_eq!(
|
||||
reason(decide(
|
||||
fetch("https://www.example.com/"),
|
||||
&exact,
|
||||
private(),
|
||||
now()
|
||||
)),
|
||||
DenyReason::NoGrant
|
||||
);
|
||||
for url in ["https://www.example.com/", "https://a.b.example.com:443/x"] {
|
||||
allowed(decide(fetch(url), &wild, private(), now()));
|
||||
}
|
||||
for url in [
|
||||
"https://example.com/",
|
||||
"https://badexample.com/",
|
||||
"https://example.com.evil.org/",
|
||||
] {
|
||||
assert_eq!(
|
||||
reason(decide(fetch(url), &wild, private(), now())),
|
||||
DenyReason::NoGrant,
|
||||
"{url}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user