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    /// Warn-only mode: treat all findings as warnings (always exit 0).
101    pub warn_only: bool,
102    /// Minimum severity level to include: "critical", "high", "medium", "low".
103    pub min_severity: Option<String>,
104    /// Minimum rule severity to treat as errors: "error", "warn".
105    pub min_rule_severity: Option<String>,
106    /// Strict secrets mode: disable dummy key heuristics for test files.
107    pub strict_secrets: bool,
108
109    // ============ Remote Scanning Options (v1.1.0) ============
110    /// Remote repository URL to scan.
111    pub remote: Option<String>,
112    /// Git reference to checkout (branch, tag, commit).
113    pub git_ref: Option<String>,
114    /// GitHub authentication token (also reads from GITHUB_TOKEN env var).
115    pub remote_auth: Option<String>,
116    /// Number of parallel clones for batch scanning.
117    pub parallel_clones: Option<usize>,
118    /// File containing list of repository URLs to scan.
119    pub remote_list: Option<String>,
120    /// Scan all repositories from awesome-claude-code.
121    pub awesome_claude_code: bool,
122
123    // ============ Badge Options (v1.1.0) ============
124    /// Generate a badge for the scan result.
125    pub badge: bool,
126    /// Badge format: "markdown", "html", "json".
127    pub badge_format: Option<String>,
128    /// Show summary only (useful for batch scanning).
129    pub summary: bool,
130
131    // ============ Client Scan Options (v1.1.0) ============
132    /// Scan all installed AI coding clients (Claude Code, Cursor, etc.).
133    pub all_clients: bool,
134    /// Specific client to scan: "claude-code", "cursor", "windsurf", "cline", "roo-code", "claude-desktop", "amazon-q".
135    pub client: Option<String>,
136
137    // ============ CVE Scan Options (v1.1.0) ============
138    /// Disable CVE vulnerability scanning.
139    pub no_cve_scan: bool,
140    /// Path to a custom CVE database (JSON).
141    pub cve_db: Option<String>,
142
143    // ============ SBOM Options (v1.2.0) ============
144    /// Generate SBOM (Software Bill of Materials).
145    pub sbom: bool,
146    /// SBOM output format: "cyclonedx", "spdx".
147    pub sbom_format: Option<String>,
148    /// Include npm dependencies in SBOM.
149    pub sbom_npm: bool,
150    /// Include Cargo dependencies in SBOM.
151    pub sbom_cargo: bool,
152}
153
154/// Watch mode configuration.
155#[derive(Debug, Clone, Serialize, Deserialize)]
156#[serde(default)]
157pub struct WatchConfig {
158    /// Debounce duration in milliseconds.
159    pub debounce_ms: u64,
160    /// Poll interval in milliseconds.
161    pub poll_interval_ms: u64,
162}
163
164impl Default for WatchConfig {
165    fn default() -> Self {
166        Self {
167            debounce_ms: 300,
168            poll_interval_ms: 500,
169        }
170    }
171}
172
173/// Baseline configuration for drift detection (rug pull prevention).
174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175#[serde(default)]
176pub struct BaselineConfig {
177    /// Create a baseline snapshot when scanning.
178    pub enabled: bool,
179    /// Check for drift against saved baseline.
180    pub check_drift: bool,
181    /// Path to save baseline to.
182    pub save_to: Option<String>,
183    /// Path to baseline file to compare against.
184    pub compare_with: Option<String>,
185}
186
187/// Text file detection configuration.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(default)]
190pub struct TextFilesConfig {
191    /// File extensions that should be treated as text.
192    pub extensions: HashSet<String>,
193    /// Special file names that should be treated as text (without extension).
194    pub special_names: HashSet<String>,
195}
196
197impl Default for TextFilesConfig {
198    fn default() -> Self {
199        let extensions: HashSet<String> = [
200            // Markdown and text
201            "md",
202            "txt",
203            "rst",
204            // Configuration
205            "json",
206            "yaml",
207            "yml",
208            "toml",
209            "xml",
210            "ini",
211            "conf",
212            "cfg",
213            "env",
214            // Shell
215            "sh",
216            "bash",
217            "zsh",
218            "fish",
219            // Scripting
220            "py",
221            "rb",
222            "pl",
223            "pm",
224            "lua",
225            "r",
226            // Web
227            "js",
228            "ts",
229            "jsx",
230            "tsx",
231            "html",
232            "css",
233            "scss",
234            "sass",
235            "less",
236            // Systems
237            "rs",
238            "go",
239            "c",
240            "cpp",
241            "h",
242            "hpp",
243            "cc",
244            "cxx",
245            // JVM
246            "java",
247            "kt",
248            "kts",
249            "scala",
250            "clj",
251            "groovy",
252            // .NET
253            "cs",
254            "fs",
255            "vb",
256            // Mobile
257            "swift",
258            "m",
259            "mm",
260            // Other languages
261            "php",
262            "ex",
263            "exs",
264            "hs",
265            "ml",
266            "vim",
267            "el",
268            "lisp",
269            // Docker
270            "dockerfile",
271            // Build
272            "makefile",
273            "cmake",
274            "gradle",
275        ]
276        .into_iter()
277        .map(String::from)
278        .collect();
279
280        let special_names: HashSet<String> = [
281            "Dockerfile",
282            "Makefile",
283            "Rakefile",
284            "Gemfile",
285            "Podfile",
286            "Vagrantfile",
287            "Procfile",
288            "LICENSE",
289            "README",
290            "CHANGELOG",
291            "CONTRIBUTING",
292            "AUTHORS",
293            "CMakeLists.txt",
294            "Justfile",
295        ]
296        .into_iter()
297        .map(String::from)
298        .collect();
299
300        Self {
301            extensions,
302            special_names,
303        }
304    }
305}
306
307impl TextFilesConfig {
308    /// Check if a path should be treated as a text file.
309    pub fn is_text_file(&self, path: &Path) -> bool {
310        // Check by extension
311        if let Some(ext) = path.extension().and_then(|e| e.to_str())
312            && self.extensions.contains(&ext.to_lowercase())
313        {
314            return true;
315        }
316
317        // Check by filename (case-insensitive for special names)
318        if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
319            // Check exact match first
320            if self.special_names.contains(name) {
321                return true;
322            }
323            // Check case-insensitive match
324            let name_lower = name.to_lowercase();
325            if self
326                .special_names
327                .iter()
328                .any(|n| n.to_lowercase() == name_lower)
329            {
330                return true;
331            }
332        }
333
334        false
335    }
336}
337
338/// Ignore configuration for scanning.
339#[derive(Debug, Clone, Serialize, Deserialize)]
340#[serde(default)]
341pub struct IgnoreConfig {
342    /// Directories to ignore (e.g., ["node_modules", "target", ".git"]).
343    pub directories: HashSet<String>,
344    /// Glob patterns to ignore (e.g., ["*.log", "build/**"]).
345    pub patterns: Vec<String>,
346    /// Whether to include test directories in scan.
347    pub include_tests: bool,
348    /// Whether to include node_modules in scan.
349    pub include_node_modules: bool,
350    /// Whether to include vendor directories in scan.
351    pub include_vendor: bool,
352}
353
354impl Default for IgnoreConfig {
355    fn default() -> Self {
356        let directories: HashSet<String> = [
357            // Common build output directories
358            "target",
359            "dist",
360            "build",
361            "out",
362            // Package manager directories
363            "node_modules",
364            ".pnpm",
365            ".yarn",
366            // Version control
367            ".git",
368            ".svn",
369            ".hg",
370            // IDE directories
371            ".idea",
372            ".vscode",
373            // Cache directories
374            ".cache",
375            "__pycache__",
376            ".pytest_cache",
377            ".mypy_cache",
378            // Coverage directories
379            "coverage",
380            ".nyc_output",
381        ]
382        .into_iter()
383        .map(String::from)
384        .collect();
385
386        Self {
387            directories,
388            patterns: Vec::new(),
389            include_tests: false,
390            include_node_modules: false,
391            include_vendor: false,
392        }
393    }
394}