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 no_malware_scan: bool,
25    pub deep_scan: bool,
26    pub watch: bool,
27    pub output: Option<String>,
28    pub fix: bool,
29    pub fix_dry_run: bool,
30    pub malware_db: Option<String>,
31    pub custom_rules: Option<String>,
32
33    // v1.1.0: Remote scan options
34    pub remote: Option<String>,
35    pub git_ref: String,
36    pub remote_auth: Option<String>,
37    pub parallel_clones: usize,
38
39    // v1.1.0: Badge options
40    pub badge: bool,
41    pub badge_format: BadgeFormat,
42    pub summary: bool,
43
44    // v1.1.0: Client scan options
45    pub all_clients: bool,
46    pub client: Option<ClientType>,
47
48    // v1.1.0: CVE scan options
49    pub no_cve_scan: bool,
50    pub cve_db: Option<String>,
51}
52
53impl EffectiveConfig {
54    /// Merge CLI options with config file settings.
55    ///
56    /// - Boolean flags: CLI OR config (either can enable)
57    /// - Enum options: config provides defaults, CLI always takes precedence
58    /// - Path options: CLI takes precedence, fallback to config
59    pub fn from_cli_and_config(cli: &Cli, config: &Config) -> Self {
60        // Parse format from config if available
61        let format = parse_output_format(config.scan.format.as_deref()).unwrap_or(cli.format);
62
63        // Parse scan_type from config if available
64        let scan_type = parse_scan_type(config.scan.scan_type.as_deref()).unwrap_or(cli.scan_type);
65
66        // Parse min_confidence from config if available
67        let min_confidence =
68            parse_confidence(config.scan.min_confidence.as_deref()).unwrap_or(cli.min_confidence);
69
70        // Path options: CLI takes precedence, fallback to config
71        let malware_db = cli
72            .malware_db
73            .as_ref()
74            .map(|p| p.display().to_string())
75            .or_else(|| config.scan.malware_db.clone());
76
77        let custom_rules = cli
78            .custom_rules
79            .as_ref()
80            .map(|p| p.display().to_string())
81            .or_else(|| config.scan.custom_rules.clone());
82
83        let output = cli
84            .output
85            .as_ref()
86            .map(|p| p.display().to_string())
87            .or_else(|| config.scan.output.clone());
88
89        // v1.1.0: Remote scan options
90        let remote = cli.remote.clone().or_else(|| config.scan.remote.clone());
91        let git_ref = if cli.git_ref != "HEAD" {
92            cli.git_ref.clone()
93        } else {
94            config
95                .scan
96                .git_ref
97                .clone()
98                .unwrap_or_else(|| "HEAD".to_string())
99        };
100        let remote_auth = cli
101            .remote_auth
102            .clone()
103            .or_else(|| config.scan.remote_auth.clone())
104            .or_else(|| std::env::var("GITHUB_TOKEN").ok());
105        let parallel_clones = config.scan.parallel_clones.unwrap_or(cli.parallel_clones);
106
107        // v1.1.0: Badge options
108        let badge = cli.badge || config.scan.badge;
109        let badge_format =
110            parse_badge_format(config.scan.badge_format.as_deref()).unwrap_or(cli.badge_format);
111        let summary = cli.summary || config.scan.summary;
112
113        // v1.1.0: Client scan options
114        let all_clients = cli.all_clients || config.scan.all_clients;
115        let client = cli
116            .client
117            .or_else(|| parse_client_type(config.scan.client.as_deref()));
118
119        // v1.1.0: CVE scan options
120        let no_cve_scan = cli.no_cve_scan || config.scan.no_cve_scan;
121        let cve_db = cli
122            .cve_db
123            .as_ref()
124            .map(|p| p.display().to_string())
125            .or_else(|| config.scan.cve_db.clone());
126
127        Self {
128            format,
129            // Boolean flags: OR operation (config can enable, CLI can enable)
130            strict: cli.strict || config.scan.strict,
131            warn_only: cli.warn_only,
132            min_severity: cli.min_severity,
133            min_rule_severity: cli.min_rule_severity,
134            scan_type,
135            recursive: cli.recursive || config.scan.recursive,
136            ci: cli.ci || config.scan.ci,
137            verbose: cli.verbose || config.scan.verbose,
138            min_confidence,
139            skip_comments: cli.skip_comments || config.scan.skip_comments,
140            fix_hint: cli.fix_hint || config.scan.fix_hint,
141            no_malware_scan: cli.no_malware_scan || config.scan.no_malware_scan,
142            deep_scan: cli.deep_scan || config.scan.deep_scan,
143            watch: cli.watch || config.scan.watch,
144            fix: cli.fix || config.scan.fix,
145            fix_dry_run: cli.fix_dry_run || config.scan.fix_dry_run,
146            output,
147            malware_db,
148            custom_rules,
149            // v1.1.0 options
150            remote,
151            git_ref,
152            remote_auth,
153            parallel_clones,
154            badge,
155            badge_format,
156            summary,
157            all_clients,
158            client,
159            no_cve_scan,
160            cve_db,
161        }
162    }
163}
164
165/// Parse badge format from string using FromStr.
166pub fn parse_badge_format(s: Option<&str>) -> Option<BadgeFormat> {
167    s?.parse().ok()
168}
169
170/// Parse client type from string using FromStr.
171pub fn parse_client_type(s: Option<&str>) -> Option<ClientType> {
172    s?.parse().ok()
173}
174
175/// Parse output format from string using FromStr.
176pub fn parse_output_format(s: Option<&str>) -> Option<OutputFormat> {
177    s?.parse().ok()
178}
179
180/// Parse scan type from string using FromStr.
181pub fn parse_scan_type(s: Option<&str>) -> Option<ScanType> {
182    s?.parse().ok()
183}
184
185/// Parse confidence level from string using FromStr.
186pub fn parse_confidence(s: Option<&str>) -> Option<Confidence> {
187    s?.parse().ok()
188}
189
190/// Load custom rules from effective config (CLI or config file).
191pub fn load_custom_rules_from_effective(effective: &EffectiveConfig) -> Vec<DynamicRule> {
192    match &effective.custom_rules {
193        Some(path_str) => {
194            let path = Path::new(path_str);
195            match CustomRuleLoader::load_from_file(path) {
196                Ok(rules) => {
197                    if !rules.is_empty() {
198                        eprintln!("Loaded {} custom rule(s) from {}", rules.len(), path_str);
199                    }
200                    rules
201                }
202                Err(e) => {
203                    eprintln!("Warning: Failed to load custom rules: {}", e);
204                    Vec::new()
205                }
206            }
207        }
208        None => Vec::new(),
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_parse_output_format() {
218        assert_eq!(
219            parse_output_format(Some("terminal")),
220            Some(OutputFormat::Terminal)
221        );
222        assert_eq!(parse_output_format(Some("json")), Some(OutputFormat::Json));
223        assert_eq!(
224            parse_output_format(Some("sarif")),
225            Some(OutputFormat::Sarif)
226        );
227        assert_eq!(parse_output_format(Some("html")), Some(OutputFormat::Html));
228        assert_eq!(
229            parse_output_format(Some("TERMINAL")),
230            Some(OutputFormat::Terminal)
231        );
232        assert_eq!(parse_output_format(Some("invalid")), None);
233        assert_eq!(parse_output_format(None), None);
234    }
235
236    #[test]
237    fn test_parse_scan_type() {
238        assert_eq!(parse_scan_type(Some("skill")), Some(ScanType::Skill));
239        assert_eq!(parse_scan_type(Some("hook")), Some(ScanType::Hook));
240        assert_eq!(parse_scan_type(Some("mcp")), Some(ScanType::Mcp));
241        assert_eq!(parse_scan_type(Some("docker")), Some(ScanType::Docker));
242        assert_eq!(parse_scan_type(Some("SKILL")), Some(ScanType::Skill));
243        assert_eq!(parse_scan_type(Some("invalid")), None);
244        assert_eq!(parse_scan_type(None), None);
245    }
246
247    #[test]
248    fn test_parse_confidence() {
249        assert_eq!(
250            parse_confidence(Some("tentative")),
251            Some(Confidence::Tentative)
252        );
253        assert_eq!(parse_confidence(Some("firm")), Some(Confidence::Firm));
254        assert_eq!(parse_confidence(Some("certain")), Some(Confidence::Certain));
255        assert_eq!(
256            parse_confidence(Some("TENTATIVE")),
257            Some(Confidence::Tentative)
258        );
259        assert_eq!(parse_confidence(Some("invalid")), None);
260        assert_eq!(parse_confidence(None), None);
261    }
262
263    #[test]
264    fn test_parse_client_type() {
265        assert_eq!(parse_client_type(Some("claude")), Some(ClientType::Claude));
266        assert_eq!(parse_client_type(Some("cursor")), Some(ClientType::Cursor));
267        assert_eq!(
268            parse_client_type(Some("windsurf")),
269            Some(ClientType::Windsurf)
270        );
271        assert_eq!(parse_client_type(Some("vscode")), Some(ClientType::Vscode));
272        assert_eq!(parse_client_type(Some("CLAUDE")), Some(ClientType::Claude));
273        assert_eq!(parse_client_type(Some("invalid")), None);
274        assert_eq!(parse_client_type(None), None);
275    }
276
277    #[test]
278    fn test_parse_badge_format() {
279        assert_eq!(
280            parse_badge_format(Some("markdown")),
281            Some(BadgeFormat::Markdown)
282        );
283        assert_eq!(parse_badge_format(Some("md")), Some(BadgeFormat::Markdown));
284        assert_eq!(parse_badge_format(Some("html")), Some(BadgeFormat::Html));
285        assert_eq!(parse_badge_format(Some("url")), Some(BadgeFormat::Url));
286        assert_eq!(parse_badge_format(Some("invalid")), None);
287        assert_eq!(parse_badge_format(None), None);
288    }
289}