cc_audit/config/types.rs
1//! Configuration type definitions.
2
3use crate::malware_db::MalwareSignature;
4use crate::rules::custom::YamlRule;
5use serde::{Deserialize, Serialize};
6use std::collections::HashSet;
7use std::path::Path;
8
9use super::severity::SeverityConfig;
10
11/// Main configuration structure for cc-audit.
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
13#[serde(default)]
14pub struct Config {
15 /// Scan configuration (CLI options).
16 pub scan: ScanConfig,
17 /// Watch mode configuration.
18 pub watch: WatchConfig,
19 /// Text file detection configuration.
20 pub text_files: TextFilesConfig,
21 /// Ignore configuration for scanning.
22 pub ignore: IgnoreConfig,
23 /// Baseline configuration for drift detection.
24 #[serde(default)]
25 pub baseline: BaselineConfig,
26 /// Rule severity configuration (v0.5.0).
27 #[serde(default)]
28 pub severity: SeverityConfig,
29 /// Rule IDs to disable.
30 #[serde(default)]
31 pub disabled_rules: HashSet<String>,
32 /// Custom rules defined in config file.
33 #[serde(default)]
34 pub rules: Vec<YamlRule>,
35 /// Custom malware signatures defined in config file.
36 #[serde(default)]
37 pub malware_signatures: Vec<MalwareSignature>,
38}
39
40impl Config {
41 /// Get the effective set of disabled rules (merges severity.ignore and disabled_rules).
42 pub fn effective_disabled_rules(&self) -> HashSet<String> {
43 let mut disabled = self.disabled_rules.clone();
44 disabled.extend(self.severity.ignore.iter().cloned());
45 disabled
46 }
47
48 /// Check if a rule should be ignored based on both disabled_rules and severity.ignore.
49 pub fn is_rule_disabled(&self, rule_id: &str) -> bool {
50 self.disabled_rules.contains(rule_id) || self.severity.ignore.contains(rule_id)
51 }
52
53 /// Get the RuleSeverity for a rule, considering both severity config and disabled_rules.
54 pub fn get_rule_severity(&self, rule_id: &str) -> Option<crate::rules::RuleSeverity> {
55 if self.is_rule_disabled(rule_id) {
56 return None;
57 }
58 self.severity.get_rule_severity(rule_id)
59 }
60}
61
62/// Scan configuration (corresponds to CLI options).
63#[derive(Debug, Clone, Default, Serialize, Deserialize)]
64#[serde(default)]
65pub struct ScanConfig {
66 /// Output format: "terminal", "json", "sarif", "html", "markdown".
67 pub format: Option<String>,
68 /// Strict mode: show medium/low severity findings and treat warnings as errors.
69 pub strict: bool,
70 /// Scan type: "skill", "hook", "mcp", "command", "rules", "docker", "dependency", "subagent", "plugin".
71 pub scan_type: Option<String>,
72 /// Recursive scan.
73 pub recursive: bool,
74 /// CI mode: non-interactive output.
75 pub ci: bool,
76 /// Verbose output.
77 pub verbose: bool,
78 /// Minimum confidence level: "tentative", "firm", "certain".
79 pub min_confidence: Option<String>,
80 /// Skip comment lines when scanning.
81 pub skip_comments: bool,
82 /// Show fix hints in terminal output.
83 pub fix_hint: bool,
84 /// Disable malware signature scanning.
85 pub no_malware_scan: bool,
86 /// Watch mode: continuously monitor files for changes.
87 pub watch: bool,
88 /// Path to a custom malware signatures database (JSON).
89 pub malware_db: Option<String>,
90 /// Path to a custom rules file (YAML format).
91 pub custom_rules: Option<String>,
92 /// Output file path (for HTML/JSON/SARIF output).
93 pub output: Option<String>,
94 /// Enable deep scan with deobfuscation.
95 pub deep_scan: bool,
96 /// Auto-fix issues (where possible).
97 pub fix: bool,
98 /// Preview auto-fix changes without applying them.
99 pub fix_dry_run: bool,
100
101 // ============ Remote Scanning Options (v1.1.0) ============
102 /// Remote repository URL to scan.
103 pub remote: Option<String>,
104 /// Git reference to checkout (branch, tag, commit).
105 pub git_ref: Option<String>,
106 /// GitHub authentication token (also reads from GITHUB_TOKEN env var).
107 pub remote_auth: Option<String>,
108 /// Number of parallel clones for batch scanning.
109 pub parallel_clones: Option<usize>,
110
111 // ============ Badge Options (v1.1.0) ============
112 /// Generate a badge for the scan result.
113 pub badge: bool,
114 /// Badge format: "markdown", "html", "json".
115 pub badge_format: Option<String>,
116 /// Show summary only (useful for batch scanning).
117 pub summary: bool,
118
119 // ============ Client Scan Options (v1.1.0) ============
120 /// Scan all installed AI coding clients (Claude Code, Cursor, etc.).
121 pub all_clients: bool,
122 /// Specific client to scan: "claude-code", "cursor", "windsurf", "cline", "roo-code", "claude-desktop", "amazon-q".
123 pub client: Option<String>,
124
125 // ============ CVE Scan Options (v1.1.0) ============
126 /// Disable CVE vulnerability scanning.
127 pub no_cve_scan: bool,
128 /// Path to a custom CVE database (JSON).
129 pub cve_db: Option<String>,
130}
131
132/// Watch mode configuration.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(default)]
135pub struct WatchConfig {
136 /// Debounce duration in milliseconds.
137 pub debounce_ms: u64,
138 /// Poll interval in milliseconds.
139 pub poll_interval_ms: u64,
140}
141
142impl Default for WatchConfig {
143 fn default() -> Self {
144 Self {
145 debounce_ms: 300,
146 poll_interval_ms: 500,
147 }
148 }
149}
150
151/// Baseline configuration for drift detection (rug pull prevention).
152#[derive(Debug, Clone, Default, Serialize, Deserialize)]
153#[serde(default)]
154pub struct BaselineConfig {
155 /// Create a baseline snapshot when scanning.
156 pub enabled: bool,
157 /// Check for drift against saved baseline.
158 pub check_drift: bool,
159 /// Path to save baseline to.
160 pub save_to: Option<String>,
161 /// Path to baseline file to compare against.
162 pub compare_with: Option<String>,
163}
164
165/// Text file detection configuration.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(default)]
168pub struct TextFilesConfig {
169 /// File extensions that should be treated as text.
170 pub extensions: HashSet<String>,
171 /// Special file names that should be treated as text (without extension).
172 pub special_names: HashSet<String>,
173}
174
175impl Default for TextFilesConfig {
176 fn default() -> Self {
177 let extensions: HashSet<String> = [
178 // Markdown and text
179 "md",
180 "txt",
181 "rst",
182 // Configuration
183 "json",
184 "yaml",
185 "yml",
186 "toml",
187 "xml",
188 "ini",
189 "conf",
190 "cfg",
191 "env",
192 // Shell
193 "sh",
194 "bash",
195 "zsh",
196 "fish",
197 // Scripting
198 "py",
199 "rb",
200 "pl",
201 "pm",
202 "lua",
203 "r",
204 // Web
205 "js",
206 "ts",
207 "jsx",
208 "tsx",
209 "html",
210 "css",
211 "scss",
212 "sass",
213 "less",
214 // Systems
215 "rs",
216 "go",
217 "c",
218 "cpp",
219 "h",
220 "hpp",
221 "cc",
222 "cxx",
223 // JVM
224 "java",
225 "kt",
226 "kts",
227 "scala",
228 "clj",
229 "groovy",
230 // .NET
231 "cs",
232 "fs",
233 "vb",
234 // Mobile
235 "swift",
236 "m",
237 "mm",
238 // Other languages
239 "php",
240 "ex",
241 "exs",
242 "hs",
243 "ml",
244 "vim",
245 "el",
246 "lisp",
247 // Docker
248 "dockerfile",
249 // Build
250 "makefile",
251 "cmake",
252 "gradle",
253 ]
254 .into_iter()
255 .map(String::from)
256 .collect();
257
258 let special_names: HashSet<String> = [
259 "Dockerfile",
260 "Makefile",
261 "Rakefile",
262 "Gemfile",
263 "Podfile",
264 "Vagrantfile",
265 "Procfile",
266 "LICENSE",
267 "README",
268 "CHANGELOG",
269 "CONTRIBUTING",
270 "AUTHORS",
271 "CMakeLists.txt",
272 "Justfile",
273 ]
274 .into_iter()
275 .map(String::from)
276 .collect();
277
278 Self {
279 extensions,
280 special_names,
281 }
282 }
283}
284
285impl TextFilesConfig {
286 /// Check if a path should be treated as a text file.
287 pub fn is_text_file(&self, path: &Path) -> bool {
288 // Check by extension
289 if let Some(ext) = path.extension().and_then(|e| e.to_str())
290 && self.extensions.contains(&ext.to_lowercase())
291 {
292 return true;
293 }
294
295 // Check by filename (case-insensitive for special names)
296 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
297 // Check exact match first
298 if self.special_names.contains(name) {
299 return true;
300 }
301 // Check case-insensitive match
302 let name_lower = name.to_lowercase();
303 if self
304 .special_names
305 .iter()
306 .any(|n| n.to_lowercase() == name_lower)
307 {
308 return true;
309 }
310 }
311
312 false
313 }
314}
315
316/// Ignore configuration for scanning.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(default)]
319pub struct IgnoreConfig {
320 /// Directories to ignore (e.g., ["node_modules", "target", ".git"]).
321 pub directories: HashSet<String>,
322 /// Glob patterns to ignore (e.g., ["*.log", "build/**"]).
323 pub patterns: Vec<String>,
324 /// Whether to include test directories in scan.
325 pub include_tests: bool,
326 /// Whether to include node_modules in scan.
327 pub include_node_modules: bool,
328 /// Whether to include vendor directories in scan.
329 pub include_vendor: bool,
330}
331
332impl Default for IgnoreConfig {
333 fn default() -> Self {
334 let directories: HashSet<String> = [
335 // Common build output directories
336 "target",
337 "dist",
338 "build",
339 "out",
340 // Package manager directories
341 "node_modules",
342 ".pnpm",
343 ".yarn",
344 // Version control
345 ".git",
346 ".svn",
347 ".hg",
348 // IDE directories
349 ".idea",
350 ".vscode",
351 // Cache directories
352 ".cache",
353 "__pycache__",
354 ".pytest_cache",
355 ".mypy_cache",
356 // Coverage directories
357 "coverage",
358 ".nyc_output",
359 ]
360 .into_iter()
361 .map(String::from)
362 .collect();
363
364 Self {
365 directories,
366 patterns: Vec::new(),
367 include_tests: false,
368 include_node_modules: false,
369 include_vendor: false,
370 }
371 }
372}