use serde::{Deserialize, Serialize};
use crate::pattern::GlobPattern;
use crate::policy::PolicyError;
const PROTECTED_WRITES: [&str; 84] = [
"**/.github/workflows/**",
"**/.gitlab-ci.yml",
"**/.circleci/**",
"**/azure-pipelines.yml",
"**/Jenkinsfile",
"**/.buildkite/**",
"**/.husky/**",
"**/.githooks/**",
"**/.pre-commit-config.yaml",
"**/lefthook.yml",
"**/.lefthook.yml",
"**/.vscode/**",
"**/package.json",
"**/.npmrc",
"**/.yarnrc",
"**/.yarnrc.yml",
"**/pnpm-workspace.yaml",
"**/Makefile",
"**/makefile",
"**/GNUmakefile",
"**/*.mk",
"**/justfile",
"**/Justfile",
"**/.justfile",
"**/Taskfile.yml",
"**/Taskfile.yaml",
"**/build.rs",
"**/Cargo.toml",
"**/.cargo/config",
"**/.cargo/config.toml",
"**/go.mod",
"**/go.work",
"**/CMakeLists.txt",
"**/*.cmake",
"**/CMakePresets.json",
"**/CMakeUserPresets.json",
"**/pom.xml",
"**/.mvn/**",
"**/mvnw",
"**/mvnw.cmd",
"**/*.gradle",
"**/*.gradle.kts",
"**/gradle.properties",
"**/gradlew",
"**/gradlew.bat",
"**/gradle/wrapper/**",
"**/*.csproj",
"**/*.fsproj",
"**/*.vbproj",
"**/*.props",
"**/*.targets",
"**/global.json",
"**/NuGet.config",
"**/nuget.config",
"**/Rakefile",
"**/*.rake",
"**/Gemfile",
"**/*.gemspec",
"**/.rspec",
"**/composer.json",
"**/artisan",
"**/phpunit.xml",
"**/phpunit.xml.dist",
"**/conftest.py",
"**/pytest.ini",
"**/pyproject.toml",
"**/setup.py",
"**/setup.cfg",
"**/tox.ini",
"**/noxfile.py",
"**/Dockerfile",
"**/docker-compose*.yml",
"**/docker-compose*.yaml",
"**/compose*.yml",
"**/compose*.yaml",
"**/CLAUDE.md",
"**/AGENTS.md",
"**/.claude/**",
"**/.claude.json",
"**/.codex/**",
"**/.cursor/**",
"**/.cursorrules",
"**/.mcp.json",
"**/.moat/**",
];
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TaintSettings {
#[serde(default)]
pub protected_writes: Vec<String>,
}
pub(crate) fn protected_writes(settings: Option<&TaintSettings>) -> impl Iterator<Item = &str> {
PROTECTED_WRITES.into_iter().chain(
settings
.into_iter()
.flat_map(|s| s.protected_writes.iter().map(String::as_str)),
)
}
pub(crate) fn check(settings: Option<&TaintSettings>) -> Result<(), PolicyError> {
let problem = |problem: String| PolicyError::Rule {
rule: "taint.protected_writes".to_owned(),
problem,
};
for p in settings.iter().flat_map(|s| &s.protected_writes) {
if p.trim_start().starts_with('!') {
return Err(problem(format!(
"`{p}` is an exclusion; the list can only add paths"
)));
}
GlobPattern::compile(p, false).map_err(|e| problem(e.to_string()))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::policy::{Policy, PolicyError};
#[test]
fn protected_writes_must_be_plain_globs() {
let ok = "version: 1\ntaint:\n protected_writes: ['**/deploy/**', '${project}/run.sh']\n";
assert_eq!(
Policy::parse(ok)
.unwrap()
.taint
.unwrap()
.protected_writes
.len(),
2
);
assert!(Policy::parse("version: 1\ntaint: {}\n").is_ok());
for bad in ["'**/[x'", "'!**/package.json'", "''"] {
let yaml = format!("version: 1\ntaint:\n protected_writes: [{bad}]\n");
assert!(
matches!(Policy::parse(&yaml), Err(PolicyError::Rule { rule, .. }) if rule == "taint.protected_writes"),
"{bad} must be rejected"
);
}
assert!(Policy::parse("version: 1\ntaint: { bogus: [] }\n").is_err());
}
fn sample(pattern: &str) -> String {
let path = pattern
.replacen('~', "/home/u", 1)
.replacen("**/", "/home/u/work/repo/", 1);
let path = match path.strip_suffix("/**") {
Some(dir) => format!("{dir}/x"),
None => path,
};
path.replace('*', "v1")
}
#[test]
fn protected_writes_cover_runs_later_and_the_kernel_config_files() {
let policy = Policy::parse(crate::DEFAULT_POLICY).unwrap();
let protected: Vec<_> = super::PROTECTED_WRITES
.iter()
.map(|p| crate::pattern::GlobPattern::compile(p, false).unwrap())
.collect();
let rule = |groups: &[crate::RuleGroup], id: &str| {
groups.iter().find(|g| g.id == id).unwrap().fs_write.clone()
};
let mut files = rule(&policy.ask, "runs-later");
files.extend(
rule(&policy.deny, "kernel-self")
.into_iter()
.filter(|p| !p.contains("/bin/moat") && !p.contains("apps/moat")),
);
assert!(files.len() > 30, "{files:?}");
for pattern in &files {
let path = sample(pattern);
assert!(
protected.iter().any(|g| g.is_match(&path)),
"`{pattern}` ({path}) is not in PROTECTED_WRITES"
);
}
}
}