1use serde::{Deserialize, Serialize};
6
7use crate::pattern::GlobPattern;
8use crate::policy::PolicyError;
9
10const PROTECTED_WRITES: [&str; 16] = [
15 "**/.github/workflows/**",
16 "**/.gitlab-ci.yml",
17 "**/.circleci/**",
18 "**/.husky/**",
19 "**/.githooks/**",
20 "**/.vscode/**",
21 "**/package.json",
22 "**/Makefile",
23 "**/build.rs",
24 "**/CLAUDE.md",
25 "**/AGENTS.md",
26 "**/.claude/**",
27 "**/.codex/**",
28 "**/.cursor/**",
29 "**/.cursorrules",
30 "**/.mcp.json",
31];
32
33#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct TaintSettings {
37 #[serde(default)]
41 pub protected_writes: Vec<String>,
42}
43
44pub(crate) fn protected_writes(settings: Option<&TaintSettings>) -> impl Iterator<Item = &str> {
46 PROTECTED_WRITES.into_iter().chain(
47 settings
48 .into_iter()
49 .flat_map(|s| s.protected_writes.iter().map(String::as_str)),
50 )
51}
52
53pub(crate) fn check(settings: Option<&TaintSettings>) -> Result<(), PolicyError> {
56 let problem = |problem: String| PolicyError::Rule {
57 rule: "taint.protected_writes".to_owned(),
58 problem,
59 };
60 for p in settings.iter().flat_map(|s| &s.protected_writes) {
61 if p.trim_start().starts_with('!') {
62 return Err(problem(format!(
63 "`{p}` is an exclusion; the list can only add paths"
64 )));
65 }
66 GlobPattern::compile(p, false).map_err(|e| problem(e.to_string()))?;
67 }
68 Ok(())
69}
70
71#[cfg(test)]
72mod tests {
73 use crate::policy::{Policy, PolicyError};
74
75 #[test]
76 fn protected_writes_must_be_plain_globs() {
77 let ok = "version: 1\ntaint:\n protected_writes: ['**/deploy/**', '${project}/run.sh']\n";
78 assert_eq!(
79 Policy::parse(ok)
80 .unwrap()
81 .taint
82 .unwrap()
83 .protected_writes
84 .len(),
85 2
86 );
87 assert!(Policy::parse("version: 1\ntaint: {}\n").is_ok());
88 for bad in ["'**/[x'", "'!**/package.json'", "''"] {
89 let yaml = format!("version: 1\ntaint:\n protected_writes: [{bad}]\n");
90 assert!(
91 matches!(Policy::parse(&yaml), Err(PolicyError::Rule { rule, .. }) if rule == "taint.protected_writes"),
92 "{bad} must be rejected"
93 );
94 }
95 assert!(Policy::parse("version: 1\ntaint: { bogus: [] }\n").is_err());
96 }
97}