solid_pod_rs_git/
hardened.rs1use std::process::Command;
24
25pub const HARDENED_CONFIG: [(&str, &str); 4] = [
27 ("core.fsmonitor", "false"),
28 ("core.hooksPath", "/dev/null"),
29 ("core.sshCommand", "false"),
30 ("commit.gpgSign", "false"),
31];
32
33pub const HARDENED_ENV: [(&str, &str); 3] = [
35 ("GIT_CONFIG_NOSYSTEM", "1"),
36 ("GIT_CONFIG_GLOBAL", "/dev/null"),
37 ("GIT_TERMINAL_PROMPT", "0"),
38];
39
40#[must_use]
44pub fn hardened_git() -> Command {
45 let mut cmd = Command::new("git");
46 for (k, v) in HARDENED_CONFIG {
47 cmd.arg("-c").arg(format!("{k}={v}"));
48 }
49 for (k, v) in HARDENED_ENV {
50 cmd.env(k, v);
51 }
52 cmd
53}
54
55#[must_use]
57pub fn hardened_git_async() -> tokio::process::Command {
58 tokio::process::Command::from(hardened_git())
59}
60
61#[must_use]
65pub fn hardened_cgi_env() -> Vec<(String, String)> {
66 let mut env: Vec<(String, String)> = HARDENED_ENV
67 .iter()
68 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
69 .collect();
70 env.push(("GIT_CONFIG_COUNT".into(), HARDENED_CONFIG.len().to_string()));
71 for (i, (k, v)) in HARDENED_CONFIG.iter().enumerate() {
72 env.push((format!("GIT_CONFIG_KEY_{i}"), (*k).to_string()));
73 env.push((format!("GIT_CONFIG_VALUE_{i}"), (*v).to_string()));
74 }
75 env
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use std::process::Stdio;
82
83 fn git_ok(dir: &std::path::Path, args: &[&str]) -> bool {
84 hardened_git()
85 .args(args)
86 .current_dir(dir)
87 .stdout(Stdio::null())
88 .stderr(Stdio::null())
89 .status()
90 .map(|s| s.success())
91 .unwrap_or(false)
92 }
93
94 #[test]
95 fn repo_local_hooks_do_not_fire() {
96 let td = tempfile::TempDir::new().unwrap();
97 let repo = td.path();
98 if Command::new("git").arg("--version").output().is_err() {
99 return;
100 }
101 assert!(git_ok(repo, &["init", "-q", "-b", "main"]));
102 let marker = repo.join("HOOK_FIRED");
103 for name in ["pre-commit", "post-checkout", "post-commit"] {
104 let hook = repo.join(".git/hooks").join(name);
105 std::fs::write(&hook, format!("#!/bin/sh\ntouch '{}'\n", marker.display())).unwrap();
106 #[cfg(unix)]
107 {
108 use std::os::unix::fs::PermissionsExt;
109 std::fs::set_permissions(&hook, std::fs::Permissions::from_mode(0o755)).unwrap();
110 }
111 }
112 std::fs::write(repo.join("f.txt"), "x").unwrap();
113 assert!(git_ok(repo, &["add", "f.txt"]));
114 assert!(git_ok(
115 repo,
116 &[
117 "-c",
118 "user.name=t",
119 "-c",
120 "user.email=t@e",
121 "commit",
122 "-q",
123 "-m",
124 "m"
125 ]
126 ));
127 assert!(git_ok(repo, &["checkout", "-q", "-b", "other"]));
128 assert!(!marker.exists(), "a repo-local hook fired");
129
130 let ok = Command::new("git")
133 .args(["checkout", "-q", "main"])
134 .current_dir(repo)
135 .env("GIT_CONFIG_GLOBAL", "/dev/null")
136 .status()
137 .unwrap()
138 .success();
139 assert!(ok);
140 #[cfg(unix)]
141 assert!(marker.exists(), "control: plain git should fire the hook");
142 }
143
144 #[test]
145 fn repo_local_fsmonitor_is_neutralised() {
146 let td = tempfile::TempDir::new().unwrap();
147 let repo = td.path();
148 if Command::new("git").arg("--version").output().is_err() {
149 return;
150 }
151 assert!(git_ok(repo, &["init", "-q"]));
152 let marker = repo.join("FSMON_FIRED");
153 let script = repo.join("fsmon.sh");
154 std::fs::write(
155 &script,
156 format!("#!/bin/sh\ntouch '{}'\n", marker.display()),
157 )
158 .unwrap();
159 #[cfg(unix)]
160 {
161 use std::os::unix::fs::PermissionsExt;
162 std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
163 }
164 assert!(Command::new("git")
165 .args(["config", "--local", "core.fsmonitor"])
166 .arg(script.display().to_string())
167 .current_dir(repo)
168 .status()
169 .unwrap()
170 .success());
171 assert!(git_ok(repo, &["status"]));
172 assert!(!marker.exists(), "repo-local fsmonitor fired");
173 }
174
175 #[test]
176 fn cgi_env_has_matching_count() {
177 let env = hardened_cgi_env();
178 let count: usize = env
179 .iter()
180 .find(|(k, _)| k == "GIT_CONFIG_COUNT")
181 .unwrap()
182 .1
183 .parse()
184 .unwrap();
185 assert_eq!(count, HARDENED_CONFIG.len());
186 assert!(env
187 .iter()
188 .any(|(k, v)| k == "GIT_CONFIG_KEY_1" && v == "core.hooksPath"));
189 }
190}