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