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