1use crate::rules::Confidence;
2use clap::{Parser, ValueEnum};
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq)]
6pub enum OutputFormat {
7 #[default]
8 Terminal,
9 Json,
10 Sarif,
11 Html,
12}
13
14#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq)]
15pub enum ScanType {
16 #[default]
17 Skill,
18 Hook,
19 Mcp,
20 Command,
21 Rules,
22 Docker,
23 Dependency,
24 Subagent,
26 Plugin,
28}
29
30#[derive(Parser, Debug)]
31#[command(
32 name = "cc-audit",
33 version,
34 about = "Security auditor for Claude Code skills, hooks, and MCP servers",
35 long_about = "cc-audit scans Claude Code skills, hooks, and MCP servers for security vulnerabilities before installation."
36)]
37pub struct Cli {
38 #[arg(required = true)]
40 pub paths: Vec<PathBuf>,
41
42 #[arg(short, long, value_enum, default_value_t = OutputFormat::Terminal)]
44 pub format: OutputFormat,
45
46 #[arg(short, long)]
48 pub strict: bool,
49
50 #[arg(short = 't', long = "type", value_enum, default_value_t = ScanType::Skill)]
52 pub scan_type: ScanType,
53
54 #[arg(short, long)]
56 pub recursive: bool,
57
58 #[arg(long)]
60 pub ci: bool,
61
62 #[arg(short, long)]
64 pub verbose: bool,
65
66 #[arg(long)]
68 pub include_tests: bool,
69
70 #[arg(long)]
72 pub include_node_modules: bool,
73
74 #[arg(long)]
76 pub include_vendor: bool,
77
78 #[arg(long, value_enum, default_value_t = Confidence::Tentative)]
80 pub min_confidence: Confidence,
81
82 #[arg(long)]
84 pub skip_comments: bool,
85
86 #[arg(long)]
88 pub fix_hint: bool,
89
90 #[arg(short, long)]
92 pub watch: bool,
93
94 #[arg(long)]
96 pub init_hook: bool,
97
98 #[arg(long)]
100 pub remove_hook: bool,
101
102 #[arg(long)]
104 pub malware_db: Option<PathBuf>,
105
106 #[arg(long)]
108 pub no_malware_scan: bool,
109
110 #[arg(long)]
112 pub custom_rules: Option<PathBuf>,
113
114 #[arg(long)]
116 pub baseline: bool,
117
118 #[arg(long)]
120 pub check_drift: bool,
121
122 #[arg(long)]
124 pub init: bool,
125
126 #[arg(short, long)]
128 pub output: Option<PathBuf>,
129
130 #[arg(long, value_name = "FILE")]
132 pub save_baseline: Option<PathBuf>,
133
134 #[arg(long, value_name = "FILE")]
136 pub baseline_file: Option<PathBuf>,
137
138 #[arg(long, num_args = 2, value_names = ["PATH1", "PATH2"])]
140 pub compare: Option<Vec<PathBuf>>,
141
142 #[arg(long)]
144 pub fix: bool,
145
146 #[arg(long)]
148 pub fix_dry_run: bool,
149
150 #[arg(long)]
152 pub mcp_server: bool,
153
154 #[arg(long)]
156 pub deep_scan: bool,
157
158 #[arg(long, value_name = "NAME")]
160 pub profile: Option<String>,
161
162 #[arg(long, value_name = "NAME")]
164 pub save_profile: Option<String>,
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::rules::Confidence;
171 use clap::CommandFactory;
172
173 #[test]
174 fn test_cli_valid() {
175 Cli::command().debug_assert();
176 }
177
178 #[test]
179 fn test_parse_basic_args() {
180 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
181 assert_eq!(cli.paths.len(), 1);
182 assert!(!cli.strict);
183 assert!(!cli.recursive);
184 }
185
186 #[test]
187 fn test_parse_multiple_paths() {
188 let cli = Cli::try_parse_from(["cc-audit", "./skill1/", "./skill2/"]).unwrap();
189 assert_eq!(cli.paths.len(), 2);
190 }
191
192 #[test]
193 fn test_parse_format_json() {
194 let cli = Cli::try_parse_from(["cc-audit", "--format", "json", "./skill/"]).unwrap();
195 assert!(matches!(cli.format, OutputFormat::Json));
196 }
197
198 #[test]
199 fn test_parse_strict_mode() {
200 let cli = Cli::try_parse_from(["cc-audit", "--strict", "./skill/"]).unwrap();
201 assert!(cli.strict);
202 }
203
204 #[test]
205 fn test_parse_recursive() {
206 let cli = Cli::try_parse_from(["cc-audit", "-r", "./skills/"]).unwrap();
207 assert!(cli.recursive);
208 }
209
210 #[test]
211 fn test_parse_format_sarif() {
212 let cli = Cli::try_parse_from(["cc-audit", "--format", "sarif", "./skill/"]).unwrap();
213 assert!(matches!(cli.format, OutputFormat::Sarif));
214 }
215
216 #[test]
217 fn test_parse_type_hook() {
218 let cli = Cli::try_parse_from(["cc-audit", "--type", "hook", "./settings.json"]).unwrap();
219 assert!(matches!(cli.scan_type, ScanType::Hook));
220 }
221
222 #[test]
223 fn test_parse_type_mcp() {
224 let cli = Cli::try_parse_from(["cc-audit", "--type", "mcp", "./mcp.json"]).unwrap();
225 assert!(matches!(cli.scan_type, ScanType::Mcp));
226 }
227
228 #[test]
229 fn test_parse_type_command() {
230 let cli = Cli::try_parse_from(["cc-audit", "--type", "command", "./"]).unwrap();
231 assert!(matches!(cli.scan_type, ScanType::Command));
232 }
233
234 #[test]
235 fn test_parse_type_rules() {
236 let cli = Cli::try_parse_from(["cc-audit", "--type", "rules", "./"]).unwrap();
237 assert!(matches!(cli.scan_type, ScanType::Rules));
238 }
239
240 #[test]
241 fn test_parse_type_docker() {
242 let cli = Cli::try_parse_from(["cc-audit", "--type", "docker", "./"]).unwrap();
243 assert!(matches!(cli.scan_type, ScanType::Docker));
244 }
245
246 #[test]
247 fn test_parse_type_dependency() {
248 let cli = Cli::try_parse_from(["cc-audit", "--type", "dependency", "./"]).unwrap();
249 assert!(matches!(cli.scan_type, ScanType::Dependency));
250 }
251
252 #[test]
253 fn test_parse_ci_mode() {
254 let cli = Cli::try_parse_from(["cc-audit", "--ci", "./skill/"]).unwrap();
255 assert!(cli.ci);
256 }
257
258 #[test]
259 fn test_parse_verbose() {
260 let cli = Cli::try_parse_from(["cc-audit", "-v", "./skill/"]).unwrap();
261 assert!(cli.verbose);
262 }
263
264 #[test]
265 fn test_parse_all_options() {
266 let cli = Cli::try_parse_from([
267 "cc-audit",
268 "--format",
269 "json",
270 "--strict",
271 "--type",
272 "hook",
273 "--recursive",
274 "--ci",
275 "--verbose",
276 "./path/",
277 ])
278 .unwrap();
279 assert!(matches!(cli.format, OutputFormat::Json));
280 assert!(cli.strict);
281 assert!(matches!(cli.scan_type, ScanType::Hook));
282 assert!(cli.recursive);
283 assert!(cli.ci);
284 assert!(cli.verbose);
285 }
286
287 #[test]
288 fn test_default_values() {
289 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
290 assert!(matches!(cli.format, OutputFormat::Terminal));
291 assert!(matches!(cli.scan_type, ScanType::Skill));
292 assert!(!cli.strict);
293 assert!(!cli.recursive);
294 assert!(!cli.ci);
295 assert!(!cli.verbose);
296 assert!(!cli.include_tests);
297 assert!(!cli.include_node_modules);
298 assert!(!cli.include_vendor);
299 assert!(matches!(cli.min_confidence, Confidence::Tentative));
300 }
301
302 #[test]
303 fn test_parse_include_tests() {
304 let cli = Cli::try_parse_from(["cc-audit", "--include-tests", "./skill/"]).unwrap();
305 assert!(cli.include_tests);
306 }
307
308 #[test]
309 fn test_parse_include_node_modules() {
310 let cli = Cli::try_parse_from(["cc-audit", "--include-node-modules", "./skill/"]).unwrap();
311 assert!(cli.include_node_modules);
312 }
313
314 #[test]
315 fn test_parse_include_vendor() {
316 let cli = Cli::try_parse_from(["cc-audit", "--include-vendor", "./skill/"]).unwrap();
317 assert!(cli.include_vendor);
318 }
319
320 #[test]
321 fn test_parse_all_include_options() {
322 let cli = Cli::try_parse_from([
323 "cc-audit",
324 "--include-tests",
325 "--include-node-modules",
326 "--include-vendor",
327 "./skill/",
328 ])
329 .unwrap();
330 assert!(cli.include_tests);
331 assert!(cli.include_node_modules);
332 assert!(cli.include_vendor);
333 }
334
335 #[test]
336 fn test_parse_min_confidence_tentative() {
337 let cli =
338 Cli::try_parse_from(["cc-audit", "--min-confidence", "tentative", "./skill/"]).unwrap();
339 assert!(matches!(cli.min_confidence, Confidence::Tentative));
340 }
341
342 #[test]
343 fn test_parse_min_confidence_firm() {
344 let cli =
345 Cli::try_parse_from(["cc-audit", "--min-confidence", "firm", "./skill/"]).unwrap();
346 assert!(matches!(cli.min_confidence, Confidence::Firm));
347 }
348
349 #[test]
350 fn test_parse_min_confidence_certain() {
351 let cli =
352 Cli::try_parse_from(["cc-audit", "--min-confidence", "certain", "./skill/"]).unwrap();
353 assert!(matches!(cli.min_confidence, Confidence::Certain));
354 }
355
356 #[test]
357 fn test_parse_skip_comments() {
358 let cli = Cli::try_parse_from(["cc-audit", "--skip-comments", "./skill/"]).unwrap();
359 assert!(cli.skip_comments);
360 }
361
362 #[test]
363 fn test_default_skip_comments_false() {
364 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
365 assert!(!cli.skip_comments);
366 }
367
368 #[test]
369 fn test_parse_fix_hint() {
370 let cli = Cli::try_parse_from(["cc-audit", "--fix-hint", "./skill/"]).unwrap();
371 assert!(cli.fix_hint);
372 }
373
374 #[test]
375 fn test_default_fix_hint_false() {
376 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
377 assert!(!cli.fix_hint);
378 }
379
380 #[test]
381 fn test_parse_watch() {
382 let cli = Cli::try_parse_from(["cc-audit", "--watch", "./skill/"]).unwrap();
383 assert!(cli.watch);
384 }
385
386 #[test]
387 fn test_parse_watch_short() {
388 let cli = Cli::try_parse_from(["cc-audit", "-w", "./skill/"]).unwrap();
389 assert!(cli.watch);
390 }
391
392 #[test]
393 fn test_default_watch_false() {
394 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
395 assert!(!cli.watch);
396 }
397
398 #[test]
399 fn test_parse_init_hook() {
400 let cli = Cli::try_parse_from(["cc-audit", "--init-hook", "./repo/"]).unwrap();
401 assert!(cli.init_hook);
402 }
403
404 #[test]
405 fn test_parse_remove_hook() {
406 let cli = Cli::try_parse_from(["cc-audit", "--remove-hook", "./repo/"]).unwrap();
407 assert!(cli.remove_hook);
408 }
409
410 #[test]
411 fn test_default_init_hook_false() {
412 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
413 assert!(!cli.init_hook);
414 }
415
416 #[test]
417 fn test_default_remove_hook_false() {
418 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
419 assert!(!cli.remove_hook);
420 }
421
422 #[test]
423 fn test_parse_malware_db() {
424 let cli =
425 Cli::try_parse_from(["cc-audit", "--malware-db", "./custom.json", "./skill/"]).unwrap();
426 assert!(cli.malware_db.is_some());
427 assert_eq!(cli.malware_db.unwrap().to_str().unwrap(), "./custom.json");
428 }
429
430 #[test]
431 fn test_default_malware_db_none() {
432 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
433 assert!(cli.malware_db.is_none());
434 }
435
436 #[test]
437 fn test_parse_no_malware_scan() {
438 let cli = Cli::try_parse_from(["cc-audit", "--no-malware-scan", "./skill/"]).unwrap();
439 assert!(cli.no_malware_scan);
440 }
441
442 #[test]
443 fn test_default_no_malware_scan_false() {
444 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
445 assert!(!cli.no_malware_scan);
446 }
447
448 #[test]
449 fn test_parse_custom_rules() {
450 let cli = Cli::try_parse_from(["cc-audit", "--custom-rules", "./rules.yaml", "./skill/"])
451 .unwrap();
452 assert!(cli.custom_rules.is_some());
453 assert_eq!(cli.custom_rules.unwrap().to_str().unwrap(), "./rules.yaml");
454 }
455
456 #[test]
457 fn test_default_custom_rules_none() {
458 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
459 assert!(cli.custom_rules.is_none());
460 }
461
462 #[test]
463 fn test_parse_init() {
464 let cli = Cli::try_parse_from(["cc-audit", "--init", "./"]).unwrap();
465 assert!(cli.init);
466 }
467
468 #[test]
469 fn test_default_init_false() {
470 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
471 assert!(!cli.init);
472 }
473}