1use crate::{
4 BadgeFormat, Cli, ClientType, Confidence, Config, CustomRuleLoader, DynamicRule, OutputFormat,
5 RuleSeverity, ScanType, Severity,
6};
7use std::path::Path;
8
9#[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 pub strict_secrets: bool,
35
36 pub remote: Option<String>,
38 pub git_ref: String,
39 pub remote_auth: Option<String>,
40 pub parallel_clones: usize,
41 pub remote_list: Option<String>,
43 pub awesome_claude_code: bool,
45
46 pub badge: bool,
48 pub badge_format: BadgeFormat,
49 pub summary: bool,
50
51 pub all_clients: bool,
53 pub client: Option<ClientType>,
54
55 pub no_cve_scan: bool,
57 pub cve_db: Option<String>,
58
59 pub sbom: bool,
62 pub sbom_format: Option<String>,
64 pub sbom_npm: bool,
66 pub sbom_cargo: bool,
68}
69
70impl EffectiveConfig {
71 pub fn from_cli_and_config(cli: &Cli, config: &Config) -> Self {
77 let format = parse_output_format(config.scan.format.as_deref()).unwrap_or(cli.format);
79
80 let scan_type = parse_scan_type(config.scan.scan_type.as_deref()).unwrap_or(cli.scan_type);
82
83 let min_confidence =
85 parse_confidence(config.scan.min_confidence.as_deref()).unwrap_or(cli.min_confidence);
86
87 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 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 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 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 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 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 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 let strict_secrets = cli.strict_secrets || config.scan.strict_secrets;
163
164 let min_severity = cli
166 .min_severity
167 .or_else(|| parse_severity(config.scan.min_severity.as_deref()));
168
169 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 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 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 sbom,
214 sbom_format,
215 sbom_npm,
216 sbom_cargo,
217 }
218 }
219}
220
221pub fn parse_badge_format(s: Option<&str>) -> Option<BadgeFormat> {
223 s?.parse().ok()
224}
225
226pub fn parse_client_type(s: Option<&str>) -> Option<ClientType> {
228 s?.parse().ok()
229}
230
231pub fn parse_output_format(s: Option<&str>) -> Option<OutputFormat> {
233 s?.parse().ok()
234}
235
236pub fn parse_scan_type(s: Option<&str>) -> Option<ScanType> {
238 s?.parse().ok()
239}
240
241pub fn parse_confidence(s: Option<&str>) -> Option<Confidence> {
243 s?.parse().ok()
244}
245
246pub fn parse_severity(s: Option<&str>) -> Option<Severity> {
248 s?.parse().ok()
249}
250
251pub fn parse_rule_severity(s: Option<&str>) -> Option<RuleSeverity> {
253 s?.parse().ok()
254}
255
256pub 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}