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 pub allow_inline_suppression: bool,
38
39 pub remote: Option<String>,
41 pub git_ref: String,
42 pub remote_auth: Option<String>,
43 pub parallel_clones: usize,
44 pub remote_list: Option<String>,
46 pub awesome_claude_code: bool,
48
49 pub badge: bool,
51 pub badge_format: BadgeFormat,
52 pub summary: bool,
53
54 pub all_clients: bool,
56 pub client: Option<ClientType>,
57
58 pub no_cve_scan: bool,
60 pub cve_db: Option<String>,
61
62 pub sbom: bool,
65 pub sbom_format: Option<String>,
67 pub sbom_npm: bool,
69 pub sbom_cargo: bool,
71}
72
73impl EffectiveConfig {
74 pub fn from_check_args_and_config(args: &CheckArgs, config: &Config) -> Self {
82 let format = if args.format != OutputFormat::default() {
84 args.format
85 } else {
86 parse_output_format(config.scan.format.as_deref()).unwrap_or(args.format)
87 };
88
89 let scan_type = if args.scan_type != ScanType::default() {
93 args.scan_type
94 } else {
95 parse_scan_type(config.scan.scan_type.as_deref()).unwrap_or(args.scan_type)
96 };
97
98 let min_confidence = args
100 .min_confidence
101 .or_else(|| parse_confidence(config.scan.min_confidence.as_deref()))
102 .unwrap_or(Confidence::Tentative);
103
104 let malware_db = args
106 .malware_db
107 .as_ref()
108 .map(|p| p.display().to_string())
109 .or_else(|| config.scan.malware_db.clone());
110
111 let custom_rules = args
112 .custom_rules
113 .as_ref()
114 .map(|p| p.display().to_string())
115 .or_else(|| config.scan.custom_rules.clone());
116
117 let output = args
118 .output
119 .as_ref()
120 .map(|p| p.display().to_string())
121 .or_else(|| config.scan.output.clone());
122
123 let remote = args.remote.clone().or_else(|| config.scan.remote.clone());
125 let git_ref = if args.git_ref != "HEAD" {
126 args.git_ref.clone()
127 } else {
128 config
129 .scan
130 .git_ref
131 .clone()
132 .unwrap_or_else(|| "HEAD".to_string())
133 };
134 let remote_auth = args
135 .remote_auth
136 .clone()
137 .or_else(|| config.scan.remote_auth.clone())
138 .or_else(|| std::env::var("GITHUB_TOKEN").ok());
139 let parallel_clones = config.scan.parallel_clones.unwrap_or(args.parallel_clones);
140
141 let badge = args.badge || config.scan.badge;
143 let badge_format =
144 parse_badge_format(config.scan.badge_format.as_deref()).unwrap_or(args.badge_format);
145 let summary = args.summary || config.scan.summary;
146
147 let all_clients = args.all_clients || config.scan.all_clients;
149 let client = args
150 .client
151 .or_else(|| parse_client_type(config.scan.client.as_deref()));
152
153 let no_cve_scan = args.no_cve_scan || config.scan.no_cve_scan;
155 let cve_db = args
156 .cve_db
157 .as_ref()
158 .map(|p| p.display().to_string())
159 .or_else(|| config.scan.cve_db.clone());
160
161 let remote_list = args
163 .remote_list
164 .as_ref()
165 .map(|p| p.display().to_string())
166 .or_else(|| config.scan.remote_list.clone());
167 let awesome_claude_code = args.awesome_claude_code || config.scan.awesome_claude_code;
168
169 let sbom = args.sbom || config.scan.sbom;
171 let sbom_format = args
172 .sbom_format
173 .clone()
174 .or_else(|| config.scan.sbom_format.clone());
175 let sbom_npm = args.sbom_npm || config.scan.sbom_npm;
176 let sbom_cargo = args.sbom_cargo || config.scan.sbom_cargo;
177
178 let strict_secrets = args.strict_secrets || config.scan.strict_secrets;
180
181 let allow_inline_suppression =
183 args.allow_inline_suppression || config.scan.allow_inline_suppression;
184
185 let min_severity = args
187 .min_severity
188 .or_else(|| parse_severity(config.scan.min_severity.as_deref()));
189
190 let min_rule_severity = args
192 .min_rule_severity
193 .or_else(|| parse_rule_severity(config.scan.min_rule_severity.as_deref()));
194
195 let recursive = !args.no_recursive && config.scan.recursive;
199
200 Self {
201 format,
202 strict: args.strict || config.scan.strict,
203 warn_only: args.warn_only || config.scan.warn_only,
204 min_severity,
205 min_rule_severity,
206 scan_type,
207 recursive,
208 ci: args.ci || config.scan.ci,
209 verbose: config.scan.verbose, min_confidence,
211 skip_comments: args.skip_comments || config.scan.skip_comments,
212 fix_hint: args.fix_hint || config.scan.fix_hint,
213 compact: args.compact || config.scan.compact,
214 no_malware_scan: args.no_malware_scan || config.scan.no_malware_scan,
215 deep_scan: args.deep_scan || config.scan.deep_scan,
216 watch: args.watch || config.scan.watch,
217 fix: args.fix || config.scan.fix,
218 fix_dry_run: args.fix_dry_run || config.scan.fix_dry_run,
219 output,
220 malware_db,
221 custom_rules,
222 strict_secrets,
223 allow_inline_suppression,
224 remote,
226 git_ref,
227 remote_auth,
228 parallel_clones,
229 remote_list,
230 awesome_claude_code,
231 badge,
232 badge_format,
233 summary,
234 all_clients,
235 client,
236 no_cve_scan,
237 cve_db,
238 sbom,
240 sbom_format,
241 sbom_npm,
242 sbom_cargo,
243 }
244 }
245}
246
247pub use crate::config::{
251 parse_badge_format, parse_client_type, parse_confidence, parse_output_format,
252 parse_rule_severity, parse_scan_type, parse_severity,
253};
254
255pub fn load_custom_rules_from_effective(effective: &EffectiveConfig) -> Vec<DynamicRule> {
257 match &effective.custom_rules {
258 Some(path_str) => {
259 let path = Path::new(path_str);
260 match CustomRuleLoader::load_from_file(path) {
261 Ok(rules) => {
262 if !rules.is_empty() {
263 eprintln!("Loaded {} custom rule(s) from {}", rules.len(), path_str);
264 }
265 rules
266 }
267 Err(e) => {
268 eprintln!("Warning: Failed to load custom rules: {}", e);
269 Vec::new()
270 }
271 }
272 }
273 None => Vec::new(),
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_parse_output_format() {
283 assert_eq!(
284 parse_output_format(Some("terminal")),
285 Some(OutputFormat::Terminal)
286 );
287 assert_eq!(parse_output_format(Some("json")), Some(OutputFormat::Json));
288 assert_eq!(
289 parse_output_format(Some("sarif")),
290 Some(OutputFormat::Sarif)
291 );
292 assert_eq!(parse_output_format(Some("html")), Some(OutputFormat::Html));
293 assert_eq!(
294 parse_output_format(Some("TERMINAL")),
295 Some(OutputFormat::Terminal)
296 );
297 assert_eq!(parse_output_format(Some("invalid")), None);
298 assert_eq!(parse_output_format(None), None);
299 }
300
301 #[test]
302 fn test_parse_scan_type() {
303 assert_eq!(parse_scan_type(Some("skill")), Some(ScanType::Skill));
304 assert_eq!(parse_scan_type(Some("hook")), Some(ScanType::Hook));
305 assert_eq!(parse_scan_type(Some("mcp")), Some(ScanType::Mcp));
306 assert_eq!(parse_scan_type(Some("docker")), Some(ScanType::Docker));
307 assert_eq!(parse_scan_type(Some("SKILL")), Some(ScanType::Skill));
308 assert_eq!(parse_scan_type(Some("invalid")), None);
309 assert_eq!(parse_scan_type(None), None);
310 }
311
312 #[test]
313 fn test_parse_confidence() {
314 assert_eq!(
315 parse_confidence(Some("tentative")),
316 Some(Confidence::Tentative)
317 );
318 assert_eq!(parse_confidence(Some("firm")), Some(Confidence::Firm));
319 assert_eq!(parse_confidence(Some("certain")), Some(Confidence::Certain));
320 assert_eq!(
321 parse_confidence(Some("TENTATIVE")),
322 Some(Confidence::Tentative)
323 );
324 assert_eq!(parse_confidence(Some("invalid")), None);
325 assert_eq!(parse_confidence(None), None);
326 }
327
328 #[test]
329 fn test_parse_client_type() {
330 assert_eq!(parse_client_type(Some("claude")), Some(ClientType::Claude));
331 assert_eq!(parse_client_type(Some("cursor")), Some(ClientType::Cursor));
332 assert_eq!(
333 parse_client_type(Some("windsurf")),
334 Some(ClientType::Windsurf)
335 );
336 assert_eq!(parse_client_type(Some("vscode")), Some(ClientType::Vscode));
337 assert_eq!(parse_client_type(Some("CLAUDE")), Some(ClientType::Claude));
338 assert_eq!(parse_client_type(Some("invalid")), None);
339 assert_eq!(parse_client_type(None), None);
340 }
341
342 #[test]
343 fn test_parse_badge_format() {
344 assert_eq!(
345 parse_badge_format(Some("markdown")),
346 Some(BadgeFormat::Markdown)
347 );
348 assert_eq!(parse_badge_format(Some("md")), Some(BadgeFormat::Markdown));
349 assert_eq!(parse_badge_format(Some("html")), Some(BadgeFormat::Html));
350 assert_eq!(parse_badge_format(Some("url")), Some(BadgeFormat::Url));
351 assert_eq!(parse_badge_format(Some("invalid")), None);
352 assert_eq!(parse_badge_format(None), None);
353 }
354}