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