1use crate::{
4 BadgeFormat, CheckArgs, ClientType, Confidence, Config, CustomRuleLoader, DynamicRule,
5 OutputFormat, 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_check_args_and_config(args: &CheckArgs, config: &Config) -> Self {
79 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 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 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 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 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 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 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 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 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 let strict_secrets = args.strict_secrets || config.scan.strict_secrets;
174
175 let min_severity = args
177 .min_severity
178 .or_else(|| parse_severity(config.scan.min_severity.as_deref()));
179
180 let min_rule_severity = args
182 .min_rule_severity
183 .or_else(|| parse_rule_severity(config.scan.min_rule_severity.as_deref()));
184
185 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, 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,
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,
229 sbom_format,
230 sbom_npm,
231 sbom_cargo,
232 }
233 }
234}
235
236pub fn parse_badge_format(s: Option<&str>) -> Option<BadgeFormat> {
238 s?.parse().ok()
239}
240
241pub fn parse_client_type(s: Option<&str>) -> Option<ClientType> {
243 s?.parse().ok()
244}
245
246pub fn parse_output_format(s: Option<&str>) -> Option<OutputFormat> {
248 s?.parse().ok()
249}
250
251pub fn parse_scan_type(s: Option<&str>) -> Option<ScanType> {
253 s?.parse().ok()
254}
255
256pub fn parse_confidence(s: Option<&str>) -> Option<Confidence> {
258 s?.parse().ok()
259}
260
261pub fn parse_severity(s: Option<&str>) -> Option<Severity> {
263 s?.parse().ok()
264}
265
266pub fn parse_rule_severity(s: Option<&str>) -> Option<RuleSeverity> {
268 s?.parse().ok()
269}
270
271pub 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}