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        let scan_type = if args.scan_type != ScanType::default() {
90            args.scan_type
91        } else {
92            parse_scan_type(config.scan.scan_type.as_deref()).unwrap_or(args.scan_type)
93        };
94
95        // min_confidence: CLI takes precedence if explicitly set, else config, else default
96        let min_confidence = args
97            .min_confidence
98            .or_else(|| parse_confidence(config.scan.min_confidence.as_deref()))
99            .unwrap_or(Confidence::Tentative);
100
101        // Path options: CLI takes precedence, fallback to config
102        let malware_db = args
103            .malware_db
104            .as_ref()
105            .map(|p| p.display().to_string())
106            .or_else(|| config.scan.malware_db.clone());
107
108        let custom_rules = args
109            .custom_rules
110            .as_ref()
111            .map(|p| p.display().to_string())
112            .or_else(|| config.scan.custom_rules.clone());
113
114        let output = args
115            .output
116            .as_ref()
117            .map(|p| p.display().to_string())
118            .or_else(|| config.scan.output.clone());
119
120        // Remote scan options
121        let remote = args.remote.clone().or_else(|| config.scan.remote.clone());
122        let git_ref = if args.git_ref != "HEAD" {
123            args.git_ref.clone()
124        } else {
125            config
126                .scan
127                .git_ref
128                .clone()
129                .unwrap_or_else(|| "HEAD".to_string())
130        };
131        let remote_auth = args
132            .remote_auth
133            .clone()
134            .or_else(|| config.scan.remote_auth.clone())
135            .or_else(|| std::env::var("GITHUB_TOKEN").ok());
136        let parallel_clones = config.scan.parallel_clones.unwrap_or(args.parallel_clones);
137
138        // Badge options
139        let badge = args.badge || config.scan.badge;
140        let badge_format =
141            parse_badge_format(config.scan.badge_format.as_deref()).unwrap_or(args.badge_format);
142        let summary = args.summary || config.scan.summary;
143
144        // Client scan options
145        let all_clients = args.all_clients || config.scan.all_clients;
146        let client = args
147            .client
148            .or_else(|| parse_client_type(config.scan.client.as_deref()));
149
150        // CVE scan options
151        let no_cve_scan = args.no_cve_scan || config.scan.no_cve_scan;
152        let cve_db = args
153            .cve_db
154            .as_ref()
155            .map(|p| p.display().to_string())
156            .or_else(|| config.scan.cve_db.clone());
157
158        // Additional remote options
159        let remote_list = args
160            .remote_list
161            .as_ref()
162            .map(|p| p.display().to_string())
163            .or_else(|| config.scan.remote_list.clone());
164        let awesome_claude_code = args.awesome_claude_code || config.scan.awesome_claude_code;
165
166        // SBOM options
167        let sbom = args.sbom || config.scan.sbom;
168        let sbom_format = args
169            .sbom_format
170            .clone()
171            .or_else(|| config.scan.sbom_format.clone());
172        let sbom_npm = args.sbom_npm || config.scan.sbom_npm;
173        let sbom_cargo = args.sbom_cargo || config.scan.sbom_cargo;
174
175        // strict_secrets: CLI OR config
176        let strict_secrets = args.strict_secrets || config.scan.strict_secrets;
177
178        // allow_inline_suppression: CLI OR config (default false = secure)
179        let allow_inline_suppression =
180            args.allow_inline_suppression || config.scan.allow_inline_suppression;
181
182        // Parse min_severity from config if CLI doesn't provide it
183        let min_severity = args
184            .min_severity
185            .or_else(|| parse_severity(config.scan.min_severity.as_deref()));
186
187        // Parse min_rule_severity from config if CLI doesn't provide it
188        let min_rule_severity = args
189            .min_rule_severity
190            .or_else(|| parse_rule_severity(config.scan.min_rule_severity.as_deref()));
191
192        // Note: args.no_recursive means NOT recursive (default false = recursive)
193        // If CLI says --no-recursive, disable recursion regardless of config
194        // Otherwise, use config value
195        let recursive = !args.no_recursive && config.scan.recursive;
196
197        Self {
198            format,
199            strict: args.strict || config.scan.strict,
200            warn_only: args.warn_only || config.scan.warn_only,
201            min_severity,
202            min_rule_severity,
203            scan_type,
204            recursive,
205            ci: args.ci || config.scan.ci,
206            verbose: config.scan.verbose, // Note: verbose is in Cli, not CheckArgs
207            min_confidence,
208            skip_comments: args.skip_comments || config.scan.skip_comments,
209            fix_hint: args.fix_hint || config.scan.fix_hint,
210            compact: args.compact || config.scan.compact,
211            no_malware_scan: args.no_malware_scan || config.scan.no_malware_scan,
212            deep_scan: args.deep_scan || config.scan.deep_scan,
213            watch: args.watch || config.scan.watch,
214            fix: args.fix || config.scan.fix,
215            fix_dry_run: args.fix_dry_run || config.scan.fix_dry_run,
216            output,
217            malware_db,
218            custom_rules,
219            strict_secrets,
220            allow_inline_suppression,
221            // Remote options
222            remote,
223            git_ref,
224            remote_auth,
225            parallel_clones,
226            remote_list,
227            awesome_claude_code,
228            badge,
229            badge_format,
230            summary,
231            all_clients,
232            client,
233            no_cve_scan,
234            cve_db,
235            // SBOM options
236            sbom,
237            sbom_format,
238            sbom_npm,
239            sbom_cargo,
240        }
241    }
242}
243
244// Re-export parse functions from L2 (config layer) for backward compatibility.
245// These functions were moved to `crate::config::parsers` in v3.3.0 to fix
246// architecture violation V3 (L2 → L1 reverse dependency).
247pub use crate::config::{
248    parse_badge_format, parse_client_type, parse_confidence, parse_output_format,
249    parse_rule_severity, parse_scan_type, parse_severity,
250};
251
252/// Load custom rules from effective config (CLI or config file).
253pub fn load_custom_rules_from_effective(effective: &EffectiveConfig) -> Vec<DynamicRule> {
254    match &effective.custom_rules {
255        Some(path_str) => {
256            let path = Path::new(path_str);
257            match CustomRuleLoader::load_from_file(path) {
258                Ok(rules) => {
259                    if !rules.is_empty() {
260                        eprintln!("Loaded {} custom rule(s) from {}", rules.len(), path_str);
261                    }
262                    rules
263                }
264                Err(e) => {
265                    eprintln!("Warning: Failed to load custom rules: {}", e);
266                    Vec::new()
267                }
268            }
269        }
270        None => Vec::new(),
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_parse_output_format() {
280        assert_eq!(
281            parse_output_format(Some("terminal")),
282            Some(OutputFormat::Terminal)
283        );
284        assert_eq!(parse_output_format(Some("json")), Some(OutputFormat::Json));
285        assert_eq!(
286            parse_output_format(Some("sarif")),
287            Some(OutputFormat::Sarif)
288        );
289        assert_eq!(parse_output_format(Some("html")), Some(OutputFormat::Html));
290        assert_eq!(
291            parse_output_format(Some("TERMINAL")),
292            Some(OutputFormat::Terminal)
293        );
294        assert_eq!(parse_output_format(Some("invalid")), None);
295        assert_eq!(parse_output_format(None), None);
296    }
297
298    #[test]
299    fn test_parse_scan_type() {
300        assert_eq!(parse_scan_type(Some("skill")), Some(ScanType::Skill));
301        assert_eq!(parse_scan_type(Some("hook")), Some(ScanType::Hook));
302        assert_eq!(parse_scan_type(Some("mcp")), Some(ScanType::Mcp));
303        assert_eq!(parse_scan_type(Some("docker")), Some(ScanType::Docker));
304        assert_eq!(parse_scan_type(Some("SKILL")), Some(ScanType::Skill));
305        assert_eq!(parse_scan_type(Some("invalid")), None);
306        assert_eq!(parse_scan_type(None), None);
307    }
308
309    #[test]
310    fn test_parse_confidence() {
311        assert_eq!(
312            parse_confidence(Some("tentative")),
313            Some(Confidence::Tentative)
314        );
315        assert_eq!(parse_confidence(Some("firm")), Some(Confidence::Firm));
316        assert_eq!(parse_confidence(Some("certain")), Some(Confidence::Certain));
317        assert_eq!(
318            parse_confidence(Some("TENTATIVE")),
319            Some(Confidence::Tentative)
320        );
321        assert_eq!(parse_confidence(Some("invalid")), None);
322        assert_eq!(parse_confidence(None), None);
323    }
324
325    #[test]
326    fn test_parse_client_type() {
327        assert_eq!(parse_client_type(Some("claude")), Some(ClientType::Claude));
328        assert_eq!(parse_client_type(Some("cursor")), Some(ClientType::Cursor));
329        assert_eq!(
330            parse_client_type(Some("windsurf")),
331            Some(ClientType::Windsurf)
332        );
333        assert_eq!(parse_client_type(Some("vscode")), Some(ClientType::Vscode));
334        assert_eq!(parse_client_type(Some("CLAUDE")), Some(ClientType::Claude));
335        assert_eq!(parse_client_type(Some("invalid")), None);
336        assert_eq!(parse_client_type(None), None);
337    }
338
339    #[test]
340    fn test_parse_badge_format() {
341        assert_eq!(
342            parse_badge_format(Some("markdown")),
343            Some(BadgeFormat::Markdown)
344        );
345        assert_eq!(parse_badge_format(Some("md")), Some(BadgeFormat::Markdown));
346        assert_eq!(parse_badge_format(Some("html")), Some(BadgeFormat::Html));
347        assert_eq!(parse_badge_format(Some("url")), Some(BadgeFormat::Url));
348        assert_eq!(parse_badge_format(Some("invalid")), None);
349        assert_eq!(parse_badge_format(None), None);
350    }
351}