1use crate::rules::{Confidence, RuleSeverity, Severity};
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(long)]
52 pub warn_only: bool,
53
54 #[arg(long, value_enum)]
56 pub min_severity: Option<Severity>,
57
58 #[arg(long, value_enum)]
60 pub min_rule_severity: Option<RuleSeverity>,
61
62 #[arg(short = 't', long = "type", value_enum, default_value_t = ScanType::Skill)]
64 pub scan_type: ScanType,
65
66 #[arg(short, long)]
68 pub recursive: bool,
69
70 #[arg(long)]
72 pub ci: bool,
73
74 #[arg(short, long)]
76 pub verbose: bool,
77
78 #[arg(long)]
80 pub include_tests: bool,
81
82 #[arg(long)]
84 pub include_node_modules: bool,
85
86 #[arg(long)]
88 pub include_vendor: bool,
89
90 #[arg(long, value_enum, default_value_t = Confidence::Tentative)]
92 pub min_confidence: Confidence,
93
94 #[arg(long)]
96 pub skip_comments: bool,
97
98 #[arg(long)]
100 pub fix_hint: bool,
101
102 #[arg(short, long)]
104 pub watch: bool,
105
106 #[arg(long)]
108 pub init_hook: bool,
109
110 #[arg(long)]
112 pub remove_hook: bool,
113
114 #[arg(long)]
116 pub malware_db: Option<PathBuf>,
117
118 #[arg(long)]
120 pub no_malware_scan: bool,
121
122 #[arg(long)]
124 pub custom_rules: Option<PathBuf>,
125
126 #[arg(long)]
128 pub baseline: bool,
129
130 #[arg(long)]
132 pub check_drift: bool,
133
134 #[arg(long)]
136 pub init: bool,
137
138 #[arg(short, long)]
140 pub output: Option<PathBuf>,
141
142 #[arg(long, value_name = "FILE")]
144 pub save_baseline: Option<PathBuf>,
145
146 #[arg(long, value_name = "FILE")]
148 pub baseline_file: Option<PathBuf>,
149
150 #[arg(long, num_args = 2, value_names = ["PATH1", "PATH2"])]
152 pub compare: Option<Vec<PathBuf>>,
153
154 #[arg(long)]
156 pub fix: bool,
157
158 #[arg(long)]
160 pub fix_dry_run: bool,
161
162 #[arg(long)]
164 pub mcp_server: bool,
165
166 #[arg(long)]
168 pub deep_scan: bool,
169
170 #[arg(long, value_name = "NAME")]
172 pub profile: Option<String>,
173
174 #[arg(long, value_name = "NAME")]
176 pub save_profile: Option<String>,
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::rules::{Confidence, RuleSeverity, Severity};
183 use clap::CommandFactory;
184
185 #[test]
186 fn test_cli_valid() {
187 Cli::command().debug_assert();
188 }
189
190 #[test]
191 fn test_parse_basic_args() {
192 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
193 assert_eq!(cli.paths.len(), 1);
194 assert!(!cli.strict);
195 assert!(!cli.recursive);
196 }
197
198 #[test]
199 fn test_parse_multiple_paths() {
200 let cli = Cli::try_parse_from(["cc-audit", "./skill1/", "./skill2/"]).unwrap();
201 assert_eq!(cli.paths.len(), 2);
202 }
203
204 #[test]
205 fn test_parse_format_json() {
206 let cli = Cli::try_parse_from(["cc-audit", "--format", "json", "./skill/"]).unwrap();
207 assert!(matches!(cli.format, OutputFormat::Json));
208 }
209
210 #[test]
211 fn test_parse_strict_mode() {
212 let cli = Cli::try_parse_from(["cc-audit", "--strict", "./skill/"]).unwrap();
213 assert!(cli.strict);
214 }
215
216 #[test]
217 fn test_parse_recursive() {
218 let cli = Cli::try_parse_from(["cc-audit", "-r", "./skills/"]).unwrap();
219 assert!(cli.recursive);
220 }
221
222 #[test]
223 fn test_parse_format_sarif() {
224 let cli = Cli::try_parse_from(["cc-audit", "--format", "sarif", "./skill/"]).unwrap();
225 assert!(matches!(cli.format, OutputFormat::Sarif));
226 }
227
228 #[test]
229 fn test_parse_type_hook() {
230 let cli = Cli::try_parse_from(["cc-audit", "--type", "hook", "./settings.json"]).unwrap();
231 assert!(matches!(cli.scan_type, ScanType::Hook));
232 }
233
234 #[test]
235 fn test_parse_type_mcp() {
236 let cli = Cli::try_parse_from(["cc-audit", "--type", "mcp", "./mcp.json"]).unwrap();
237 assert!(matches!(cli.scan_type, ScanType::Mcp));
238 }
239
240 #[test]
241 fn test_parse_type_command() {
242 let cli = Cli::try_parse_from(["cc-audit", "--type", "command", "./"]).unwrap();
243 assert!(matches!(cli.scan_type, ScanType::Command));
244 }
245
246 #[test]
247 fn test_parse_type_rules() {
248 let cli = Cli::try_parse_from(["cc-audit", "--type", "rules", "./"]).unwrap();
249 assert!(matches!(cli.scan_type, ScanType::Rules));
250 }
251
252 #[test]
253 fn test_parse_type_docker() {
254 let cli = Cli::try_parse_from(["cc-audit", "--type", "docker", "./"]).unwrap();
255 assert!(matches!(cli.scan_type, ScanType::Docker));
256 }
257
258 #[test]
259 fn test_parse_type_dependency() {
260 let cli = Cli::try_parse_from(["cc-audit", "--type", "dependency", "./"]).unwrap();
261 assert!(matches!(cli.scan_type, ScanType::Dependency));
262 }
263
264 #[test]
265 fn test_parse_ci_mode() {
266 let cli = Cli::try_parse_from(["cc-audit", "--ci", "./skill/"]).unwrap();
267 assert!(cli.ci);
268 }
269
270 #[test]
271 fn test_parse_verbose() {
272 let cli = Cli::try_parse_from(["cc-audit", "-v", "./skill/"]).unwrap();
273 assert!(cli.verbose);
274 }
275
276 #[test]
277 fn test_parse_all_options() {
278 let cli = Cli::try_parse_from([
279 "cc-audit",
280 "--format",
281 "json",
282 "--strict",
283 "--type",
284 "hook",
285 "--recursive",
286 "--ci",
287 "--verbose",
288 "./path/",
289 ])
290 .unwrap();
291 assert!(matches!(cli.format, OutputFormat::Json));
292 assert!(cli.strict);
293 assert!(matches!(cli.scan_type, ScanType::Hook));
294 assert!(cli.recursive);
295 assert!(cli.ci);
296 assert!(cli.verbose);
297 }
298
299 #[test]
300 fn test_default_values() {
301 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
302 assert!(matches!(cli.format, OutputFormat::Terminal));
303 assert!(matches!(cli.scan_type, ScanType::Skill));
304 assert!(!cli.strict);
305 assert!(!cli.recursive);
306 assert!(!cli.ci);
307 assert!(!cli.verbose);
308 assert!(!cli.include_tests);
309 assert!(!cli.include_node_modules);
310 assert!(!cli.include_vendor);
311 assert!(matches!(cli.min_confidence, Confidence::Tentative));
312 }
313
314 #[test]
315 fn test_parse_include_tests() {
316 let cli = Cli::try_parse_from(["cc-audit", "--include-tests", "./skill/"]).unwrap();
317 assert!(cli.include_tests);
318 }
319
320 #[test]
321 fn test_parse_include_node_modules() {
322 let cli = Cli::try_parse_from(["cc-audit", "--include-node-modules", "./skill/"]).unwrap();
323 assert!(cli.include_node_modules);
324 }
325
326 #[test]
327 fn test_parse_include_vendor() {
328 let cli = Cli::try_parse_from(["cc-audit", "--include-vendor", "./skill/"]).unwrap();
329 assert!(cli.include_vendor);
330 }
331
332 #[test]
333 fn test_parse_all_include_options() {
334 let cli = Cli::try_parse_from([
335 "cc-audit",
336 "--include-tests",
337 "--include-node-modules",
338 "--include-vendor",
339 "./skill/",
340 ])
341 .unwrap();
342 assert!(cli.include_tests);
343 assert!(cli.include_node_modules);
344 assert!(cli.include_vendor);
345 }
346
347 #[test]
348 fn test_parse_min_confidence_tentative() {
349 let cli =
350 Cli::try_parse_from(["cc-audit", "--min-confidence", "tentative", "./skill/"]).unwrap();
351 assert!(matches!(cli.min_confidence, Confidence::Tentative));
352 }
353
354 #[test]
355 fn test_parse_min_confidence_firm() {
356 let cli =
357 Cli::try_parse_from(["cc-audit", "--min-confidence", "firm", "./skill/"]).unwrap();
358 assert!(matches!(cli.min_confidence, Confidence::Firm));
359 }
360
361 #[test]
362 fn test_parse_min_confidence_certain() {
363 let cli =
364 Cli::try_parse_from(["cc-audit", "--min-confidence", "certain", "./skill/"]).unwrap();
365 assert!(matches!(cli.min_confidence, Confidence::Certain));
366 }
367
368 #[test]
369 fn test_parse_skip_comments() {
370 let cli = Cli::try_parse_from(["cc-audit", "--skip-comments", "./skill/"]).unwrap();
371 assert!(cli.skip_comments);
372 }
373
374 #[test]
375 fn test_default_skip_comments_false() {
376 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
377 assert!(!cli.skip_comments);
378 }
379
380 #[test]
381 fn test_parse_fix_hint() {
382 let cli = Cli::try_parse_from(["cc-audit", "--fix-hint", "./skill/"]).unwrap();
383 assert!(cli.fix_hint);
384 }
385
386 #[test]
387 fn test_default_fix_hint_false() {
388 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
389 assert!(!cli.fix_hint);
390 }
391
392 #[test]
393 fn test_parse_watch() {
394 let cli = Cli::try_parse_from(["cc-audit", "--watch", "./skill/"]).unwrap();
395 assert!(cli.watch);
396 }
397
398 #[test]
399 fn test_parse_watch_short() {
400 let cli = Cli::try_parse_from(["cc-audit", "-w", "./skill/"]).unwrap();
401 assert!(cli.watch);
402 }
403
404 #[test]
405 fn test_default_watch_false() {
406 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
407 assert!(!cli.watch);
408 }
409
410 #[test]
411 fn test_parse_init_hook() {
412 let cli = Cli::try_parse_from(["cc-audit", "--init-hook", "./repo/"]).unwrap();
413 assert!(cli.init_hook);
414 }
415
416 #[test]
417 fn test_parse_remove_hook() {
418 let cli = Cli::try_parse_from(["cc-audit", "--remove-hook", "./repo/"]).unwrap();
419 assert!(cli.remove_hook);
420 }
421
422 #[test]
423 fn test_default_init_hook_false() {
424 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
425 assert!(!cli.init_hook);
426 }
427
428 #[test]
429 fn test_default_remove_hook_false() {
430 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
431 assert!(!cli.remove_hook);
432 }
433
434 #[test]
435 fn test_parse_malware_db() {
436 let cli =
437 Cli::try_parse_from(["cc-audit", "--malware-db", "./custom.json", "./skill/"]).unwrap();
438 assert!(cli.malware_db.is_some());
439 assert_eq!(cli.malware_db.unwrap().to_str().unwrap(), "./custom.json");
440 }
441
442 #[test]
443 fn test_default_malware_db_none() {
444 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
445 assert!(cli.malware_db.is_none());
446 }
447
448 #[test]
449 fn test_parse_no_malware_scan() {
450 let cli = Cli::try_parse_from(["cc-audit", "--no-malware-scan", "./skill/"]).unwrap();
451 assert!(cli.no_malware_scan);
452 }
453
454 #[test]
455 fn test_default_no_malware_scan_false() {
456 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
457 assert!(!cli.no_malware_scan);
458 }
459
460 #[test]
461 fn test_parse_custom_rules() {
462 let cli = Cli::try_parse_from(["cc-audit", "--custom-rules", "./rules.yaml", "./skill/"])
463 .unwrap();
464 assert!(cli.custom_rules.is_some());
465 assert_eq!(cli.custom_rules.unwrap().to_str().unwrap(), "./rules.yaml");
466 }
467
468 #[test]
469 fn test_default_custom_rules_none() {
470 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
471 assert!(cli.custom_rules.is_none());
472 }
473
474 #[test]
475 fn test_parse_init() {
476 let cli = Cli::try_parse_from(["cc-audit", "--init", "./"]).unwrap();
477 assert!(cli.init);
478 }
479
480 #[test]
481 fn test_default_init_false() {
482 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
483 assert!(!cli.init);
484 }
485
486 #[test]
487 fn test_parse_warn_only() {
488 let cli = Cli::try_parse_from(["cc-audit", "--warn-only", "./skill/"]).unwrap();
489 assert!(cli.warn_only);
490 }
491
492 #[test]
493 fn test_default_warn_only_false() {
494 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
495 assert!(!cli.warn_only);
496 }
497
498 #[test]
499 fn test_parse_min_severity_critical() {
500 let cli =
501 Cli::try_parse_from(["cc-audit", "--min-severity", "critical", "./skill/"]).unwrap();
502 assert_eq!(cli.min_severity, Some(Severity::Critical));
503 }
504
505 #[test]
506 fn test_parse_min_severity_high() {
507 let cli = Cli::try_parse_from(["cc-audit", "--min-severity", "high", "./skill/"]).unwrap();
508 assert_eq!(cli.min_severity, Some(Severity::High));
509 }
510
511 #[test]
512 fn test_parse_min_severity_medium() {
513 let cli =
514 Cli::try_parse_from(["cc-audit", "--min-severity", "medium", "./skill/"]).unwrap();
515 assert_eq!(cli.min_severity, Some(Severity::Medium));
516 }
517
518 #[test]
519 fn test_parse_min_severity_low() {
520 let cli = Cli::try_parse_from(["cc-audit", "--min-severity", "low", "./skill/"]).unwrap();
521 assert_eq!(cli.min_severity, Some(Severity::Low));
522 }
523
524 #[test]
525 fn test_default_min_severity_none() {
526 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
527 assert!(cli.min_severity.is_none());
528 }
529
530 #[test]
531 fn test_parse_min_rule_severity_error() {
532 let cli =
533 Cli::try_parse_from(["cc-audit", "--min-rule-severity", "error", "./skill/"]).unwrap();
534 assert_eq!(cli.min_rule_severity, Some(RuleSeverity::Error));
535 }
536
537 #[test]
538 fn test_parse_min_rule_severity_warn() {
539 let cli =
540 Cli::try_parse_from(["cc-audit", "--min-rule-severity", "warn", "./skill/"]).unwrap();
541 assert_eq!(cli.min_rule_severity, Some(RuleSeverity::Warn));
542 }
543
544 #[test]
545 fn test_default_min_rule_severity_none() {
546 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
547 assert!(cli.min_rule_severity.is_none());
548 }
549
550 #[test]
551 fn test_warn_only_with_strict_conflict() {
552 let cli = Cli::try_parse_from(["cc-audit", "--warn-only", "--strict", "./skill/"]).unwrap();
554 assert!(cli.warn_only);
555 assert!(cli.strict);
556 }
557}