Skip to main content

cc_audit/run/
config.rs

1//! Effective configuration after merging CLI and config file.
2
3use crate::{
4    BadgeFormat, CheckArgs, ClientType, Confidence, Config, CustomRuleLoader, DynamicRule,
5    OutputFormat, RuleSeverity, ScanType, Severity,
6};
7use std::path::Path;
8
9/// Effective scan configuration after merging CLI and config file.
10#[derive(Debug, Clone)]
11pub struct EffectiveConfig {
12    pub format: OutputFormat,
13    pub strict: bool,
14    pub warn_only: bool,
15    pub min_severity: Option<Severity>,
16    pub min_rule_severity: Option<RuleSeverity>,
17    pub scan_type: ScanType,
18    pub recursive: bool,
19    pub ci: bool,
20    pub verbose: bool,
21    pub min_confidence: Confidence,
22    pub skip_comments: bool,
23    pub fix_hint: bool,
24    pub compact: bool,
25    pub no_malware_scan: bool,
26    pub deep_scan: bool,
27    pub watch: bool,
28    pub output: Option<String>,
29    pub fix: bool,
30    pub fix_dry_run: bool,
31    pub malware_db: Option<String>,
32    pub custom_rules: Option<String>,
33    /// Strict secrets mode: disable dummy key heuristics for test files.
34    pub strict_secrets: bool,
35    /// Honor in-band suppression directives read from scanned content. Off by
36    /// default: untrusted artifacts must not disable rules on themselves (#156).
37    pub allow_inline_suppression: bool,
38
39    // v1.1.0: Remote scan options
40    pub remote: Option<String>,
41    pub git_ref: String,
42    pub remote_auth: Option<String>,
43    pub parallel_clones: usize,
44    /// File containing list of repository URLs to scan.
45    pub remote_list: Option<String>,
46    /// Scan all repositories from awesome-claude-code.
47    pub awesome_claude_code: bool,
48
49    // v1.1.0: Badge options
50    pub badge: bool,
51    pub badge_format: BadgeFormat,
52    pub summary: bool,
53
54    // v1.1.0: Client scan options
55    pub all_clients: bool,
56    pub client: Option<ClientType>,
57
58    // v1.1.0: CVE scan options
59    pub no_cve_scan: bool,
60    pub cve_db: Option<String>,
61
62    // v1.2.0: SBOM options
63    /// Generate SBOM (Software Bill of Materials).
64    pub sbom: bool,
65    /// SBOM output format: "cyclonedx", "spdx".
66    pub sbom_format: Option<String>,
67    /// Include npm dependencies in SBOM.
68    pub sbom_npm: bool,
69    /// Include Cargo dependencies in SBOM.
70    pub sbom_cargo: bool,
71}
72
73impl EffectiveConfig {
74    /// Merge CheckArgs options with config file settings.
75    ///
76    /// - Boolean flags: CLI OR config (either can enable)
77    /// - Enum options: config provides defaults, CLI always takes precedence
78    /// - Path options: CLI takes precedence, fallback to config
79    ///
80    /// Note: CheckArgs uses `no_recursive` (default false = recursive enabled).
81    pub fn from_check_args_and_config(args: &CheckArgs, config: &Config) -> Self {
82        // For enum options: CLI takes precedence when explicitly set (non-default)
83        let format = if args.format != OutputFormat::default() {
84            args.format
85        } else {
86            parse_output_format(config.scan.format.as_deref()).unwrap_or(args.format)
87        };
88
89        // An explicit `--type` (anything other than the `Auto` default) wins;
90        // otherwise fall back to the config file's `scan_type`, and finally to
91        // `Auto`, which fans out across all scanners at scan time (#155).
92        let scan_type = if args.scan_type != ScanType::default() {
93            args.scan_type
94        } else {
95            parse_scan_type(config.scan.scan_type.as_deref()).unwrap_or(args.scan_type)
96        };
97
98        // min_confidence: CLI takes precedence if explicitly set, else config, else default
99        let min_confidence = args
100            .min_confidence
101            .or_else(|| parse_confidence(config.scan.min_confidence.as_deref()))
102            .unwrap_or(Confidence::Tentative);
103
104        // Path options: CLI takes precedence, fallback to config
105        let malware_db = args
106            .malware_db
107            .as_ref()
108            .map(|p| p.display().to_string())
109            .or_else(|| config.scan.malware_db.clone());
110
111        let custom_rules = args
112            .custom_rules
113            .as_ref()
114            .map(|p| p.display().to_string())
115            .or_else(|| config.scan.custom_rules.clone());
116
117        let output = args
118            .output
119            .as_ref()
120            .map(|p| p.display().to_string())
121            .or_else(|| config.scan.output.clone());
122
123        // Remote scan options
124        let remote = args.remote.clone().or_else(|| config.scan.remote.clone());
125        let git_ref = if args.git_ref != "HEAD" {
126            args.git_ref.clone()
127        } else {
128            config
129                .scan
130                .git_ref
131                .clone()
132                .unwrap_or_else(|| "HEAD".to_string())
133        };
134        let remote_auth = args
135            .remote_auth
136            .clone()
137            .or_else(|| config.scan.remote_auth.clone())
138            .or_else(|| std::env::var("GITHUB_TOKEN").ok());
139        let parallel_clones = config.scan.parallel_clones.unwrap_or(args.parallel_clones);
140
141        // Badge options
142        let badge = args.badge || config.scan.badge;
143        let badge_format =
144            parse_badge_format(config.scan.badge_format.as_deref()).unwrap_or(args.badge_format);
145        let summary = args.summary || config.scan.summary;
146
147        // Client scan options
148        let all_clients = args.all_clients || config.scan.all_clients;
149        let client = args
150            .client
151            .or_else(|| parse_client_type(config.scan.client.as_deref()));
152
153        // CVE scan options
154        let no_cve_scan = args.no_cve_scan || config.scan.no_cve_scan;
155        let cve_db = args
156            .cve_db
157            .as_ref()
158            .map(|p| p.display().to_string())
159            .or_else(|| config.scan.cve_db.clone());
160
161        // Additional remote options
162        let remote_list = args
163            .remote_list
164            .as_ref()
165            .map(|p| p.display().to_string())
166            .or_else(|| config.scan.remote_list.clone());
167        let awesome_claude_code = args.awesome_claude_code || config.scan.awesome_claude_code;
168
169        // SBOM options
170        let sbom = args.sbom || config.scan.sbom;
171        let sbom_format = args
172            .sbom_format
173            .clone()
174            .or_else(|| config.scan.sbom_format.clone());
175        let sbom_npm = args.sbom_npm || config.scan.sbom_npm;
176        let sbom_cargo = args.sbom_cargo || config.scan.sbom_cargo;
177
178        // strict_secrets: CLI OR config
179        let strict_secrets = args.strict_secrets || config.scan.strict_secrets;
180
181        // allow_inline_suppression: CLI OR config (default false = secure)
182        let allow_inline_suppression =
183            args.allow_inline_suppression || config.scan.allow_inline_suppression;
184
185        // Parse min_severity from config if CLI doesn't provide it
186        let min_severity = args
187            .min_severity
188            .or_else(|| parse_severity(config.scan.min_severity.as_deref()));
189
190        // Parse min_rule_severity from config if CLI doesn't provide it
191        let min_rule_severity = args
192            .min_rule_severity
193            .or_else(|| parse_rule_severity(config.scan.min_rule_severity.as_deref()));
194
195        // Note: args.no_recursive means NOT recursive (default false = recursive)
196        // If CLI says --no-recursive, disable recursion regardless of config
197        // Otherwise, use config value
198        let recursive = !args.no_recursive && config.scan.recursive;
199
200        Self {
201            format,
202            strict: args.strict || config.scan.strict,
203            warn_only: args.warn_only || config.scan.warn_only,
204            min_severity,
205            min_rule_severity,
206            scan_type,
207            recursive,
208            ci: args.ci || config.scan.ci,
209            verbose: config.scan.verbose, // Note: verbose is in Cli, not CheckArgs
210            min_confidence,
211            skip_comments: args.skip_comments || config.scan.skip_comments,
212            fix_hint: args.fix_hint || config.scan.fix_hint,
213            compact: args.compact || config.scan.compact,
214            no_malware_scan: args.no_malware_scan || config.scan.no_malware_scan,
215            deep_scan: args.deep_scan || config.scan.deep_scan,
216            watch: args.watch || config.scan.watch,
217            fix: args.fix || config.scan.fix,
218            fix_dry_run: args.fix_dry_run || config.scan.fix_dry_run,
219            output,
220            malware_db,
221            custom_rules,
222            strict_secrets,
223            allow_inline_suppression,
224            // Remote options
225            remote,
226            git_ref,
227            remote_auth,
228            parallel_clones,
229            remote_list,
230            awesome_claude_code,
231            badge,
232            badge_format,
233            summary,
234            all_clients,
235            client,
236            no_cve_scan,
237            cve_db,
238            // SBOM options
239            sbom,
240            sbom_format,
241            sbom_npm,
242            sbom_cargo,
243        }
244    }
245}
246
247// Re-export parse functions from L2 (config layer) for backward compatibility.
248// These functions were moved to `crate::config::parsers` in v3.3.0 to fix
249// architecture violation V3 (L2 → L1 reverse dependency).
250pub use crate::config::{
251    parse_badge_format, parse_client_type, parse_confidence, parse_output_format,
252    parse_rule_severity, parse_scan_type, parse_severity,
253};
254
255/// Load custom rules from effective config (CLI or config file).
256pub fn load_custom_rules_from_effective(effective: &EffectiveConfig) -> Vec<DynamicRule> {
257    match &effective.custom_rules {
258        Some(path_str) => {
259            let path = Path::new(path_str);
260            match CustomRuleLoader::load_from_file(path) {
261                Ok(rules) => {
262                    if !rules.is_empty() {
263                        eprintln!("Loaded {} custom rule(s) from {}", rules.len(), path_str);
264                    }
265                    rules
266                }
267                Err(e) => {
268                    eprintln!("Warning: Failed to load custom rules: {}", e);
269                    Vec::new()
270                }
271            }
272        }
273        None => Vec::new(),
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_parse_output_format() {
283        assert_eq!(
284            parse_output_format(Some("terminal")),
285            Some(OutputFormat::Terminal)
286        );
287        assert_eq!(parse_output_format(Some("json")), Some(OutputFormat::Json));
288        assert_eq!(
289            parse_output_format(Some("sarif")),
290            Some(OutputFormat::Sarif)
291        );
292        assert_eq!(parse_output_format(Some("html")), Some(OutputFormat::Html));
293        assert_eq!(
294            parse_output_format(Some("TERMINAL")),
295            Some(OutputFormat::Terminal)
296        );
297        assert_eq!(parse_output_format(Some("invalid")), None);
298        assert_eq!(parse_output_format(None), None);
299    }
300
301    #[test]
302    fn test_parse_scan_type() {
303        assert_eq!(parse_scan_type(Some("skill")), Some(ScanType::Skill));
304        assert_eq!(parse_scan_type(Some("hook")), Some(ScanType::Hook));
305        assert_eq!(parse_scan_type(Some("mcp")), Some(ScanType::Mcp));
306        assert_eq!(parse_scan_type(Some("docker")), Some(ScanType::Docker));
307        assert_eq!(parse_scan_type(Some("SKILL")), Some(ScanType::Skill));
308        assert_eq!(parse_scan_type(Some("invalid")), None);
309        assert_eq!(parse_scan_type(None), None);
310    }
311
312    #[test]
313    fn test_parse_confidence() {
314        assert_eq!(
315            parse_confidence(Some("tentative")),
316            Some(Confidence::Tentative)
317        );
318        assert_eq!(parse_confidence(Some("firm")), Some(Confidence::Firm));
319        assert_eq!(parse_confidence(Some("certain")), Some(Confidence::Certain));
320        assert_eq!(
321            parse_confidence(Some("TENTATIVE")),
322            Some(Confidence::Tentative)
323        );
324        assert_eq!(parse_confidence(Some("invalid")), None);
325        assert_eq!(parse_confidence(None), None);
326    }
327
328    #[test]
329    fn test_parse_client_type() {
330        assert_eq!(parse_client_type(Some("claude")), Some(ClientType::Claude));
331        assert_eq!(parse_client_type(Some("cursor")), Some(ClientType::Cursor));
332        assert_eq!(
333            parse_client_type(Some("windsurf")),
334            Some(ClientType::Windsurf)
335        );
336        assert_eq!(parse_client_type(Some("vscode")), Some(ClientType::Vscode));
337        assert_eq!(parse_client_type(Some("CLAUDE")), Some(ClientType::Claude));
338        assert_eq!(parse_client_type(Some("invalid")), None);
339        assert_eq!(parse_client_type(None), None);
340    }
341
342    #[test]
343    fn test_parse_badge_format() {
344        assert_eq!(
345            parse_badge_format(Some("markdown")),
346            Some(BadgeFormat::Markdown)
347        );
348        assert_eq!(parse_badge_format(Some("md")), Some(BadgeFormat::Markdown));
349        assert_eq!(parse_badge_format(Some("html")), Some(BadgeFormat::Html));
350        assert_eq!(parse_badge_format(Some("url")), Some(BadgeFormat::Url));
351        assert_eq!(parse_badge_format(Some("invalid")), None);
352        assert_eq!(parse_badge_format(None), None);
353    }
354}