1use serde::{Deserialize, Serialize};
6
7use crate::pattern::GlobPattern;
8use crate::policy::PolicyError;
9
10const PROTECTED_WRITES: [&str; 76] = [
19 "**/.github/workflows/**",
20 "**/.gitlab-ci.yml",
21 "**/.circleci/**",
22 "**/.husky/**",
23 "**/.githooks/**",
24 "**/.vscode/**",
25 "**/package.json",
27 "**/.npmrc",
28 "**/.yarnrc",
29 "**/.yarnrc.yml",
30 "**/pnpm-workspace.yaml",
31 "**/Makefile",
33 "**/makefile",
34 "**/GNUmakefile",
35 "**/*.mk",
36 "**/justfile",
37 "**/Justfile",
38 "**/.justfile",
39 "**/Taskfile.yml",
40 "**/Taskfile.yaml",
41 "**/build.rs",
43 "**/Cargo.toml",
44 "**/.cargo/config",
45 "**/.cargo/config.toml",
46 "**/go.mod",
48 "**/go.work",
49 "**/CMakeLists.txt",
51 "**/*.cmake",
52 "**/CMakePresets.json",
53 "**/CMakeUserPresets.json",
54 "**/pom.xml",
56 "**/.mvn/**",
57 "**/mvnw",
58 "**/mvnw.cmd",
59 "**/*.gradle",
61 "**/*.gradle.kts",
62 "**/gradle.properties",
63 "**/gradlew",
64 "**/gradlew.bat",
65 "**/gradle/wrapper/**",
66 "**/*.csproj",
68 "**/*.fsproj",
69 "**/*.vbproj",
70 "**/*.props",
71 "**/*.targets",
72 "**/global.json",
73 "**/NuGet.config",
74 "**/nuget.config",
75 "**/Rakefile",
77 "**/*.rake",
78 "**/Gemfile",
79 "**/*.gemspec",
80 "**/.rspec",
81 "**/composer.json",
83 "**/artisan",
84 "**/phpunit.xml",
85 "**/phpunit.xml.dist",
86 "**/conftest.py",
88 "**/pytest.ini",
89 "**/pyproject.toml",
90 "**/setup.py",
91 "**/setup.cfg",
92 "**/tox.ini",
93 "**/noxfile.py",
94 "**/Dockerfile",
96 "**/docker-compose*.yml",
97 "**/docker-compose*.yaml",
98 "**/compose*.yml",
99 "**/compose*.yaml",
100 "**/CLAUDE.md",
102 "**/AGENTS.md",
103 "**/.claude/**",
104 "**/.codex/**",
105 "**/.cursor/**",
106 "**/.cursorrules",
107 "**/.mcp.json",
108];
109
110#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(deny_unknown_fields)]
113pub struct TaintSettings {
114 #[serde(default)]
118 pub protected_writes: Vec<String>,
119}
120
121pub(crate) fn protected_writes(settings: Option<&TaintSettings>) -> impl Iterator<Item = &str> {
123 PROTECTED_WRITES.into_iter().chain(
124 settings
125 .into_iter()
126 .flat_map(|s| s.protected_writes.iter().map(String::as_str)),
127 )
128}
129
130pub(crate) fn check(settings: Option<&TaintSettings>) -> Result<(), PolicyError> {
133 let problem = |problem: String| PolicyError::Rule {
134 rule: "taint.protected_writes".to_owned(),
135 problem,
136 };
137 for p in settings.iter().flat_map(|s| &s.protected_writes) {
138 if p.trim_start().starts_with('!') {
139 return Err(problem(format!(
140 "`{p}` is an exclusion; the list can only add paths"
141 )));
142 }
143 GlobPattern::compile(p, false).map_err(|e| problem(e.to_string()))?;
144 }
145 Ok(())
146}
147
148#[cfg(test)]
149mod tests {
150 use crate::policy::{Policy, PolicyError};
151
152 #[test]
153 fn protected_writes_must_be_plain_globs() {
154 let ok = "version: 1\ntaint:\n protected_writes: ['**/deploy/**', '${project}/run.sh']\n";
155 assert_eq!(
156 Policy::parse(ok)
157 .unwrap()
158 .taint
159 .unwrap()
160 .protected_writes
161 .len(),
162 2
163 );
164 assert!(Policy::parse("version: 1\ntaint: {}\n").is_ok());
165 for bad in ["'**/[x'", "'!**/package.json'", "''"] {
166 let yaml = format!("version: 1\ntaint:\n protected_writes: [{bad}]\n");
167 assert!(
168 matches!(Policy::parse(&yaml), Err(PolicyError::Rule { rule, .. }) if rule == "taint.protected_writes"),
169 "{bad} must be rejected"
170 );
171 }
172 assert!(Policy::parse("version: 1\ntaint: { bogus: [] }\n").is_err());
173 }
174}