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