cc_audit/scanner/
common.rs1use crate::error::{AuditError, Result};
2use crate::ignore::IgnoreFilter;
3use crate::rules::{DynamicRule, Finding, RuleEngine};
4use std::fs;
5use std::path::Path;
6
7pub struct ScannerConfig {
12 engine: RuleEngine,
13 ignore_filter: Option<IgnoreFilter>,
14 skip_comments: bool,
15}
16
17impl ScannerConfig {
18 pub fn new() -> Self {
20 Self {
21 engine: RuleEngine::new(),
22 ignore_filter: None,
23 skip_comments: false,
24 }
25 }
26
27 pub fn with_skip_comments(mut self, skip: bool) -> Self {
29 self.skip_comments = skip;
30 self.engine = RuleEngine::new().with_skip_comments(skip);
31 self
32 }
33
34 pub fn with_ignore_filter(mut self, filter: IgnoreFilter) -> Self {
36 self.ignore_filter = Some(filter);
37 self
38 }
39
40 pub fn with_dynamic_rules(mut self, rules: Vec<DynamicRule>) -> Self {
42 self.engine = self.engine.with_dynamic_rules(rules);
43 self
44 }
45
46 pub fn is_ignored(&self, path: &Path) -> bool {
48 self.ignore_filter
49 .as_ref()
50 .is_some_and(|f| f.is_ignored(path))
51 }
52
53 pub fn read_file(&self, path: &Path) -> Result<String> {
55 fs::read_to_string(path).map_err(|e| AuditError::ReadError {
56 path: path.display().to_string(),
57 source: e,
58 })
59 }
60
61 pub fn check_content(&self, content: &str, file_path: &str) -> Vec<Finding> {
63 self.engine.check_content(content, file_path)
64 }
65
66 pub fn check_frontmatter(&self, frontmatter: &str, file_path: &str) -> Vec<Finding> {
68 self.engine.check_frontmatter(frontmatter, file_path)
69 }
70
71 pub fn skip_comments(&self) -> bool {
73 self.skip_comments
74 }
75
76 pub fn engine(&self) -> &RuleEngine {
78 &self.engine
79 }
80}
81
82impl Default for ScannerConfig {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use tempfile::TempDir;
92
93 #[test]
94 fn test_new_config() {
95 let config = ScannerConfig::new();
96 assert!(!config.skip_comments());
97 }
98
99 #[test]
100 fn test_with_skip_comments() {
101 let config = ScannerConfig::new().with_skip_comments(true);
102 assert!(config.skip_comments());
103 }
104
105 #[test]
106 fn test_default_config() {
107 let config = ScannerConfig::default();
108 assert!(!config.skip_comments());
109 }
110
111 #[test]
112 fn test_is_ignored_without_filter() {
113 let config = ScannerConfig::new();
114 assert!(!config.is_ignored(Path::new("test.rs")));
115 }
116
117 #[test]
118 fn test_read_file_success() {
119 let dir = TempDir::new().unwrap();
120 let file_path = dir.path().join("test.txt");
121 fs::write(&file_path, "test content").unwrap();
122
123 let config = ScannerConfig::new();
124 let content = config.read_file(&file_path).unwrap();
125 assert_eq!(content, "test content");
126 }
127
128 #[test]
129 fn test_read_file_not_found() {
130 let config = ScannerConfig::new();
131 let result = config.read_file(Path::new("/nonexistent/file.txt"));
132 assert!(result.is_err());
133 }
134
135 #[test]
136 fn test_check_content_detects_sudo() {
137 let config = ScannerConfig::new();
138 let findings = config.check_content("sudo rm -rf /", "test.sh");
139 assert!(findings.iter().any(|f| f.id == "PE-001"));
140 }
141
142 #[test]
143 fn test_check_content_skip_comments() {
144 let config = ScannerConfig::new().with_skip_comments(true);
145 let findings = config.check_content("# sudo rm -rf /", "test.sh");
146 assert!(findings.iter().all(|f| f.id != "PE-001"));
147 }
148
149 #[test]
150 fn test_check_frontmatter_wildcard() {
151 let config = ScannerConfig::new();
152 let findings = config.check_frontmatter("allowed-tools: *", "SKILL.md");
153 assert!(findings.iter().any(|f| f.id == "OP-001"));
154 }
155
156 #[test]
157 fn test_engine_accessor() {
158 let config = ScannerConfig::new();
159 let _engine = config.engine();
160 }
161}