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