Skip to main content

aster/git/
safety.rs

1//! Git 安全检查工具
2//!
3//! 提供 Git 操作的安全检查功能,包括:
4//! - 危险命令检测
5//! - 敏感文件检查
6//! - 强制推送保护
7//! - 配置修改检查
8
9use regex::Regex;
10use serde::{Deserialize, Serialize};
11use std::sync::LazyLock;
12
13/// 安全检查结果
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SafetyCheckResult {
16    /// 是否安全
17    pub safe: bool,
18    /// 危险原因
19    pub reason: Option<String>,
20    /// 警告信息
21    pub warning: Option<String>,
22    /// 建议操作
23    pub suggestion: Option<String>,
24}
25
26impl SafetyCheckResult {
27    /// 创建安全结果
28    pub fn safe() -> Self {
29        Self {
30            safe: true,
31            reason: None,
32            warning: None,
33            suggestion: None,
34        }
35    }
36
37    /// 创建带警告的安全结果
38    pub fn safe_with_warning(warning: impl Into<String>, suggestion: impl Into<String>) -> Self {
39        Self {
40            safe: true,
41            reason: None,
42            warning: Some(warning.into()),
43            suggestion: Some(suggestion.into()),
44        }
45    }
46
47    /// 创建不安全结果
48    pub fn unsafe_result(reason: impl Into<String>, suggestion: impl Into<String>) -> Self {
49        Self {
50            safe: false,
51            reason: Some(reason.into()),
52            warning: None,
53            suggestion: Some(suggestion.into()),
54        }
55    }
56}
57
58/// 敏感文件检查结果
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SensitiveFilesCheck {
61    /// 是否有敏感文件
62    pub has_sensitive_files: bool,
63    /// 敏感文件列表
64    pub sensitive_files: Vec<String>,
65    /// 警告信息
66    pub warnings: Vec<String>,
67}
68
69/// 危险的 Git 命令列表
70static DANGEROUS_COMMANDS: &[&str] = &[
71    "push --force",
72    "push -f",
73    "reset --hard",
74    "clean -fd",
75    "clean -fdx",
76    "clean -f",
77    "filter-branch",
78    "rebase --force",
79];
80
81/// 需要谨慎使用的命令模式
82static CAUTION_PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
83    vec![
84        Regex::new(r"git\s+push.*--force").unwrap(),
85        Regex::new(r"git\s+push.*-f\b").unwrap(),
86        Regex::new(r"git\s+reset\s+--hard").unwrap(),
87        Regex::new(r"git\s+clean\s+-[fdx]+").unwrap(),
88        Regex::new(r"git\s+commit.*--amend").unwrap(),
89        Regex::new(r"git\s+rebase.*-i").unwrap(),
90        Regex::new(r"git\s+config").unwrap(),
91        Regex::new(r"--no-verify").unwrap(),
92        Regex::new(r"--no-gpg-sign").unwrap(),
93    ]
94});
95
96/// 敏感文件模式
97static SENSITIVE_FILE_PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
98    vec![
99        Regex::new(r"\.env$").unwrap(),
100        Regex::new(r"\.env\.").unwrap(),
101        Regex::new(r"credentials\.json$").unwrap(),
102        Regex::new(r"secrets\.json$").unwrap(),
103        Regex::new(r"\.pem$").unwrap(),
104        Regex::new(r"\.key$").unwrap(),
105        Regex::new(r"\.cert$").unwrap(),
106        Regex::new(r"id_rsa$").unwrap(),
107        Regex::new(r"id_ed25519$").unwrap(),
108        Regex::new(r"\.aws/credentials$").unwrap(),
109        Regex::new(r"\.ssh/id_").unwrap(),
110        Regex::new(r"(?i)password").unwrap(),
111        Regex::new(r"(?i)secret").unwrap(),
112        Regex::new(r"(?i)token").unwrap(),
113        Regex::new(r"(?i)api[_-]?key").unwrap(),
114    ]
115});
116
117/// Git 安全检查工具类
118pub struct GitSafety;
119
120impl GitSafety {
121    /// 检查 Git 命令是否安全
122    pub fn validate_git_command(command: &str) -> SafetyCheckResult {
123        // 检查是否包含危险命令
124        for dangerous in DANGEROUS_COMMANDS {
125            if command.contains(dangerous) {
126                return SafetyCheckResult::unsafe_result(
127                    format!("检测到危险命令: {}", dangerous),
128                    "此操作具有破坏性且不可逆。如需继续,请明确确认。",
129                );
130            }
131        }
132
133        // 检查是否匹配谨慎模式
134        for pattern in CAUTION_PATTERNS.iter() {
135            if pattern.is_match(command) {
136                return SafetyCheckResult::safe_with_warning(
137                    "检测到潜在危险的命令模式,请谨慎使用。",
138                    "请确保您了解此操作的后果。",
139                );
140            }
141        }
142
143        SafetyCheckResult::safe()
144    }
145
146    /// 检查是否是危险的 Git 命令
147    pub fn is_dangerous(command: &str) -> bool {
148        !Self::validate_git_command(command).safe
149    }
150
151    /// 检查是否强制推送到 main/master
152    pub fn check_force_push_to_main(command: &str, current_branch: &str) -> SafetyCheckResult {
153        let force_push_re = Regex::new(r"push.*--force|push.*-f\b").unwrap();
154        let is_force_push = force_push_re.is_match(command);
155        let is_main_branch = current_branch == "main" || current_branch == "master";
156
157        if is_force_push && is_main_branch {
158            return SafetyCheckResult::unsafe_result(
159                format!("强制推送到 {} 分支非常危险", current_branch),
160                "永远不要强制推送到 main/master。请创建新分支并提交 PR。",
161            );
162        }
163
164        if is_force_push {
165            return SafetyCheckResult::safe_with_warning(
166                format!("检测到强制推送到分支: {}", current_branch),
167                "请确保没有其他人在此分支上工作。",
168            );
169        }
170
171        SafetyCheckResult::safe()
172    }
173
174    /// 检查敏感文件
175    pub fn check_sensitive_files(files: &[String]) -> SensitiveFilesCheck {
176        let mut sensitive_files = Vec::new();
177        let mut warnings = Vec::new();
178
179        for file in files {
180            for pattern in SENSITIVE_FILE_PATTERNS.iter() {
181                if pattern.is_match(file) {
182                    sensitive_files.push(file.clone());
183                    warnings.push(format!("检测到敏感文件: {}", file));
184                    break;
185                }
186            }
187        }
188
189        SensitiveFilesCheck {
190            has_sensitive_files: !sensitive_files.is_empty(),
191            sensitive_files,
192            warnings,
193        }
194    }
195
196    /// 检查是否跳过钩子
197    pub fn check_skip_hooks(command: &str) -> SafetyCheckResult {
198        if command.contains("--no-verify") {
199            return SafetyCheckResult::unsafe_result(
200                "尝试使用 --no-verify 跳过 Git 钩子",
201                "除非用户明确要求,否则不要跳过钩子。",
202            );
203        }
204
205        if command.contains("--no-gpg-sign") {
206            return SafetyCheckResult::unsafe_result(
207                "尝试使用 --no-gpg-sign 跳过 GPG 签名",
208                "除非用户明确要求,否则不要跳过 GPG 签名。",
209            );
210        }
211
212        SafetyCheckResult::safe()
213    }
214
215    /// 检查 Git 配置修改
216    pub fn check_config_change(command: &str) -> SafetyCheckResult {
217        let config_re = Regex::new(r"git\s+config").unwrap();
218        if config_re.is_match(command) {
219            return SafetyCheckResult::unsafe_result(
220                "尝试修改 Git 配置",
221                "除非用户明确要求,否则永远不要修改 git 配置。",
222            );
223        }
224
225        SafetyCheckResult::safe()
226    }
227
228    /// 综合安全检查
229    pub fn comprehensive_check(
230        command: &str,
231        current_branch: Option<&str>,
232        files: Option<&[String]>,
233    ) -> SafetyCheckResult {
234        // 1. 检查配置修改
235        let config_check = Self::check_config_change(command);
236        if !config_check.safe {
237            return config_check;
238        }
239
240        // 2. 检查跳过钩子
241        let hooks_check = Self::check_skip_hooks(command);
242        if !hooks_check.safe {
243            return hooks_check;
244        }
245
246        // 3. 检查危险命令
247        let danger_check = Self::validate_git_command(command);
248        if !danger_check.safe {
249            return danger_check;
250        }
251
252        // 4. 检查强制推送
253        if let Some(branch) = current_branch {
254            let force_push_check = Self::check_force_push_to_main(command, branch);
255            if !force_push_check.safe {
256                return force_push_check;
257            }
258        }
259
260        // 5. 检查敏感文件 (如果是 commit 或 add 命令)
261        if let Some(file_list) = files {
262            if command.contains("git add") || command.contains("git commit") {
263                let sensitive_check = Self::check_sensitive_files(file_list);
264                if sensitive_check.has_sensitive_files {
265                    return SafetyCheckResult::safe_with_warning(
266                        format!(
267                            "检测到敏感文件: {}",
268                            sensitive_check.sensitive_files.join(", ")
269                        ),
270                        "不要提交可能包含密钥的文件 (.env, credentials.json 等)。",
271                    );
272                }
273            }
274        }
275
276        // 如果有警告,返回警告
277        if danger_check.warning.is_some() {
278            return danger_check;
279        }
280
281        SafetyCheckResult::safe()
282    }
283}
284
285/// 检查是否是危险的 Git 命令(便捷函数)
286pub fn is_dangerous_command(command: &str) -> bool {
287    GitSafety::is_dangerous(command)
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_dangerous_commands() {
296        assert!(is_dangerous_command("git push --force"));
297        assert!(is_dangerous_command("git push -f origin main"));
298        assert!(is_dangerous_command("git reset --hard HEAD~1"));
299        assert!(is_dangerous_command("git clean -fd"));
300        assert!(!is_dangerous_command("git push origin main"));
301        assert!(!is_dangerous_command("git commit -m 'test'"));
302    }
303
304    #[test]
305    fn test_force_push_to_main() {
306        let result = GitSafety::check_force_push_to_main("git push --force", "main");
307        assert!(!result.safe);
308
309        let result = GitSafety::check_force_push_to_main("git push --force", "feature");
310        assert!(result.safe);
311        assert!(result.warning.is_some());
312
313        let result = GitSafety::check_force_push_to_main("git push", "main");
314        assert!(result.safe);
315    }
316
317    #[test]
318    fn test_sensitive_files() {
319        let files = vec![
320            ".env".to_string(),
321            "config.json".to_string(),
322            "credentials.json".to_string(),
323        ];
324        let result = GitSafety::check_sensitive_files(&files);
325        assert!(result.has_sensitive_files);
326        assert_eq!(result.sensitive_files.len(), 2);
327    }
328
329    #[test]
330    fn test_skip_hooks() {
331        let result = GitSafety::check_skip_hooks("git commit --no-verify -m 'test'");
332        assert!(!result.safe);
333
334        let result = GitSafety::check_skip_hooks("git commit -m 'test'");
335        assert!(result.safe);
336    }
337
338    #[test]
339    fn test_config_change() {
340        let result = GitSafety::check_config_change("git config user.email test@test.com");
341        assert!(!result.safe);
342
343        let result = GitSafety::check_config_change("git status");
344        assert!(result.safe);
345    }
346}