use std::collections::BTreeSet;
use safe_chains::pathctx::anchor::{FolderLevel, GIT_HOOKS, NAMED_FILES};
use safe_chains::pathctx::{self, PathCtx, folder};
use safe_chains::targets::UNKNOWN_WORKDIR;
use safe_chains::verdict::{SafetyLevel, Verdict};
const ROOT: &str = "/work/projects/app";
const LEVELS: [FolderLevel; 3] = [FolderLevel::Reads, FolderLevel::Developer, FolderLevel::Workspace];
fn approved(level: FolderLevel, command: &str) -> bool {
let _ctx = pathctx::enter(PathCtx { cwd: Some(UNKNOWN_WORKDIR.to_string()), root: Some(ROOT.to_string()), session_id: None });
let _folder = folder::enter(level);
match safe_chains::command_verdict(command) {
Verdict::Allowed(l) => level > FolderLevel::Reads || l <= SafetyLevel::SafeRead,
Verdict::Denied => false,
}
}
const SPELLINGS: &[&str] = &[
"echo x > {t}", "echo x >> {t}", "tee {t}", "tee -a {t}", "cp a.txt {t}", "mv a.txt {t}", "install -m 644 a.txt {t}",
"ln -s a.txt {t}", "sed -i s/a/b/ {t}", "sort -o {t} a.txt", "curl -s -o {t} https://example.com/x", "touch {t}", "rm -f {t}",
];
const DOTFILES: &[&str] = &[
".zshrc", ".bashrc", ".bash_profile", ".profile", ".zprofile", ".gitconfig", ".envrc", ".npmrc", ".netrc", ".config/fish/config.fish",
".config/autostart/x.desktop", ".config/systemd/user/x.service", ".local/bin/ls", ".ssh/authorized_keys", ".aws/credentials",
".git/config", ".github/workflows/ci.yml", ".vscode/tasks.json",
];
const CLIMBING: &[&str] = &["../x", "../../x", "a/../../x", "./../x", "sub/../../x"];
fn region_targets() -> BTreeSet<String> {
let src = include_str!("../regions/default.toml");
let file: toml::Value = toml::from_str(src).expect("regions/default.toml parses");
let mut out = BTreeSet::new();
for node in file.get("region").and_then(toml::Value::as_array).expect("[[region]] nodes") {
let Some(path) = node.get("path").and_then(toml::Value::as_str) else { continue };
if let Some(home) = path.strip_prefix("~/") {
let home = home.trim_end_matches('/');
out.insert(home.to_string());
out.insert(format!("{home}/x"));
} else if !path.starts_with('/') {
out.insert(path.to_string());
out.insert(format!("{path}/x"));
out.insert(format!("src/{path}/x"));
}
}
out
}
fn names_anywhere() -> BTreeSet<String> {
let mut targets = BTreeSet::new();
targets.extend(NAMED_FILES.iter().map(|n| (*n).to_string()));
targets.extend(NAMED_FILES.iter().map(|n| format!("keys/{n}")));
targets.extend(GIT_HOOKS.iter().map(|h| format!("hooks/{h}")));
targets.extend(GIT_HOOKS.iter().map(|h| format!(".git/hooks/{h}")));
targets.extend(DOTFILES.iter().map(|d| (*d).to_string()));
targets
}
fn hazards() -> BTreeSet<String> {
let spell = |t: &str| SPELLINGS.iter().map(move |s| s.replace("{t}", &format!("'{t}'"))).collect::<Vec<_>>();
let mut out = BTreeSet::new();
for t in region_targets()
.iter()
.chain(names_anywhere().iter())
.map(String::as_str)
.chain(CLIMBING.iter().copied())
{
out.extend(spell(t));
}
for t in names_anywhere().iter().map(String::as_str).chain(["../../x", "../../../x"]) {
out.extend(spell(t).into_iter().map(|c| format!("cd sub && {c}")));
}
out
}
#[test]
fn no_level_approves_a_sensitive_or_climbing_write() {
let hazards = hazards();
let distinct_targets: BTreeSet<&str> = hazards.iter().filter_map(|c| c.rsplit('\'').nth(1)).collect();
assert!(distinct_targets.len() >= 50, "only {} distinct hazard targets; the bar is 50", distinct_targets.len());
for level in LEVELS {
let misses: Vec<&String> = hazards.iter().filter(|c| approved(level, c)).collect();
assert!(
misses.is_empty(),
"{} approved {} of {} hazards:\n{}",
level.name(),
misses.len(),
hazards.len(),
misses.iter().take(40).map(|s| s.as_str()).collect::<Vec<_>>().join("\n")
);
}
}
const NEAR_MISSES: &[&str] = &[
"echo x > config.json", "tee notes/ssh.md", "mkdir -p build", "mkdir -p out/gen", "echo x > src/hooks.ts", "tee src/hooks/useThing.ts",
"cp a.txt docs/credentials.md", "echo x > library.md", "touch bin.rs", "echo x >> README.md", "sed -i s/a/b/ src/main.rs",
"cp a.txt b.txt", "cp a.txt out/b.txt", "touch notes.md", "tee -a log.txt", "echo x > ./out.txt", "cd sub && echo x > out.txt",
"echo x > keys.txt", "echo x > environment.ts", "echo x > rc.txt", "echo x > hooks/README.md", "echo x > Library.md",
"echo x > a/../b.txt",
];
#[test]
fn developer_approves_the_near_misses() {
assert!(NEAR_MISSES.len() >= 20);
let refused: Vec<&&str> = NEAR_MISSES.iter().filter(|c| !approved(FolderLevel::Developer, c)).collect();
assert!(refused.is_empty(), "developer refused near misses: {refused:?}");
let at_reads: Vec<&&str> = NEAR_MISSES.iter().filter(|c| approved(FolderLevel::Reads, c)).collect();
assert!(at_reads.is_empty(), "reads approved writes: {at_reads:?}");
}
const GENERIC: &[&str] = &["config", "config.json", "config.toml", "hosts", "settings.yml", "x.plist", "data.txt", "ls", "AGENTS.md"];
const SENSITIVE_FOLDERS: &[&str] = &[
"~/.ssh", "~/.aws", "~/.kube", "~/.docker", "~/.gnupg", "~/.config/git", "~/Library/LaunchAgents", "~/bin", "~/.local/bin", "~/.codex",
"~/.claude", ".git",
];
const ACCEPTED_RISK: [(FolderLevel, usize); 3] = [(FolderLevel::Reads, 0), (FolderLevel::Developer, 108), (FolderLevel::Workspace, 108)];
#[test]
fn the_accepted_risk_is_the_published_number() {
for (level, published) in ACCEPTED_RISK {
let names = GENERIC.iter().filter(|n| approved(level, &format!("echo x >> {n}"))).count();
assert_eq!(names * SENSITIVE_FOLDERS.len(), published, "{}: update the published count in the design doc", level.name());
}
}
#[test]
fn nothing_brings_a_name_into_the_folder() {
let mut hazards = Vec::new();
for name in names_anywhere()
.iter()
.filter_map(|t| t.rsplit('/').next().map(str::to_string))
.collect::<BTreeSet<_>>()
{
for into in [".", "./", "sub/.."] {
for verb in ["cp", "cp -r", "mv", "install -m 644", "ln -s", "rsync -a"] {
hazards.push(format!("{verb} '/tmp/x/{name}' {into}"));
}
}
}
for contents in ["rsync -a /tmp/home/ .", "cp -r /tmp/home/. .", "cp -R /tmp/home/ ./", "rsync -a /tmp/home/. sub/.."] {
hazards.push(contents.to_string());
}
assert!(hazards.len() >= 50, "only {} hazards", hazards.len());
for level in LEVELS {
let misses: Vec<&String> = hazards.iter().filter(|c| approved(level, c)).collect();
assert!(misses.is_empty(), "{} approved:\n{}", level.name(), misses.iter().map(|s| s.as_str()).collect::<Vec<_>>().join("\n"));
}
for tool in ["gofmt -w .", "shfmt -w .", "rubocop -a ."] {
assert!(approved(FolderLevel::Developer, tool), "`{tool}` declares that it rewrites its own files, so it may write the folder");
}
}
fn cli(args: &[&str]) -> (String, i32) {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_safe-chains"))
.args(args)
.env("HOME", "/nonexistent/unknown-folder-cli-home")
.env("SAFE_CHAINS_NO_LOCAL", "1")
.output()
.expect("run safe-chains");
(String::from_utf8_lossy(&out.stdout).into_owned(), out.status.code().unwrap_or(-1))
}
#[test]
fn the_cli_checks_and_explains_with_the_folder_unknown() {
let root = ["--root", ROOT];
let (_, code) = cli(&[&root[..], &["--unknown-folder", "developer", "cargo fmt"]].concat());
assert_eq!(code, 0, "developer approves cargo fmt");
let (_, code) = cli(&[&root[..], &["--unknown-folder", "reads", "cargo fmt"]].concat());
assert_eq!(code, 1, "reads leaves it to the prompt");
let (out, code) = cli(&[
&root[..],
&["--unknown-folder", "developer", "--explain", "echo x > notes.md && echo x >> .zshrc && rm -rf build && mise trust"],
]
.concat());
assert_eq!(code, 1);
for expected in [
"folder unknown: writes are judged at `developer`",
"`notes.md` is relative-plain",
"`.zshrc` is relative-sensitive",
"`build` is relative-plain: `developer` leaves deleting",
"`mise trust` writes in its folder",
] {
assert!(out.contains(expected), "missing `{expected}` in:\n{out}");
}
assert!(!out.contains("outside the working directory"), "the known-folder advice does not apply:\n{out}");
assert!(!out.contains("`x` is"), "an echoed word is not a path:\n{out}");
let (out, code) = cli(&[&root[..], &["--unknown-folder", "reads", "--explain", "cargo fmt"]].concat());
assert_eq!(code, 1, "reads does not approve a write:\n{out}");
assert!(!out.contains("auto-approves"), "the header agrees with the verdict:\n{out}");
assert!(out.contains("`reads` approves no write"), "{out}");
let (out, code) = cli(&["--level", "reader", "--explain", "cargo fmt"]);
assert_eq!(code, 1);
assert!(!out.contains("auto-approves"), "--explain applies --level's ceiling too:\n{out}");
}
#[test]
fn the_review_findings_stay_refused() {
let findings = [
"ls -A | xargs -I{} sh -c 'echo x >> {}'",
"echo .zshrc | xargs -I{} sh -c 'echo x >> {}'",
"echo hooks/pre-commit | xargs -I% cp /tmp/evil %",
"ls -a | xargs sed -i s/a/b/",
"ls -A | while read f; do echo x >> \"$f\"; done",
"ls -A | xargs rm -rf",
"echo .zshrc | xargs rm",
"unxz -f authorized_keys.xz",
"bunzip2 authorized_keys.bz2",
"zstd -d authorized_keys.zst",
"rsync -a /tmp/evil/ hooks/",
"ditto /tmp/evil hooks",
"xz a",
"echo x > LaunchAgents/a.plist",
"cp /tmp/x.plist LaunchAgents/",
"echo x > git/config",
"echo x > fish/conf.d/x.fish",
"echo x > systemd/user/x.service",
"echo x > autostart/x.desktop",
"echo x > nvim/init.lua",
"echo x > direnv/direnvrc",
"echo x > gh/hosts.yml",
"echo x > share/applications/x.desktop",
"echo x > hooks.json",
"echo x > rules/default.rules",
"echo x > settings.local.json",
"GIT_INDEX_FILE=.zshrc git add .",
"CARGO_TARGET_DIR=../.. cargo build",
"mise use -p .mise.toml node@20",
"mise set --file .zshrc FOO=1",
"echo x > {.,}envrc",
"echo x >> {.zshrc,a}",
"sed -i'.profil*' s/x/x/ e",
"sed -i'.git/hooks/*' s/x/x/ pre-commit",
"perl -i'.git/hooks/*' -pe 1 pre-commit",
"ls | while read f; do sh -c \"echo x >> $f\"; done",
"f(){ echo x >> \"$1\"; }; ls -A | while read l; do f \"$l\"; done",
"cp a .//",
"cp a ./.",
"echo x > launchagents/x.plist",
"echo x > Library//LaunchAgents/x",
];
for level in LEVELS {
let misses: Vec<&&str> = findings.iter().filter(|c| approved(level, c)).collect();
assert!(misses.is_empty(), "{} approved: {misses:?}", level.name());
}
}
#[test]
fn a_relative_read_is_approved_unless_it_could_be_a_secret() {
let src = include_str!("../regions/default.toml");
let file: toml::Value = toml::from_str(src).expect("regions/default.toml parses");
let mut secrets: BTreeSet<String> =
["id_rsa", "id_ed25519", ".ssh/id_rsa", "credentials", "Keychains/login.keychain-db", "../x", "../../etc/x"]
.iter()
.map(|s| (*s).to_string())
.collect();
for node in file.get("region").and_then(toml::Value::as_array).expect("[[region]] nodes") {
let (Some(path), Some(role)) = (node.get("path").and_then(toml::Value::as_str), node.get("role").and_then(toml::Value::as_str))
else {
continue;
};
if role != "credential-store" {
continue;
}
if let Some(home) = path.strip_prefix("~/") {
secrets.insert(if home.ends_with('/') { format!("{home}x") } else { home.to_string() });
} else if !path.starts_with('/') {
secrets.insert(format!("{path}/x"));
}
}
let ordinary =
["grep -r foo src", "ls src", "cat README.md", "grep -n x tests/*.rs", "cat .gitignore", "grep -r foo .", "head -5 src/main.rs"];
for level in LEVELS {
for command in ordinary {
assert!(approved(level, command), "`{command}` is refused at {}", level.name());
}
for secret in &secrets {
for spelling in ["cat '{s}'", "grep -r x '{s}'", "head '{s}'"] {
let command = spelling.replace("{s}", secret);
assert!(!approved(level, &command), "`{command}` is approved at {}", level.name());
}
}
}
assert!(secrets.len() >= 20, "only {} secrets", secrets.len());
}