Skip to main content

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}