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