Skip to main content

cc_audit/run/
config.rs

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