1use crate::client::ClientType;
2use crate::rules::{Confidence, ParseEnumError, RuleSeverity, Severity};
3use clap::{Args, Parser, Subcommand, ValueEnum};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum OutputFormat {
10 #[default]
11 Terminal,
12 Json,
13 Sarif,
14 Html,
15 Markdown,
16}
17
18impl std::str::FromStr for OutputFormat {
19 type Err = ParseEnumError;
20
21 fn from_str(s: &str) -> Result<Self, Self::Err> {
22 match s.to_lowercase().as_str() {
23 "terminal" | "term" => Ok(OutputFormat::Terminal),
24 "json" => Ok(OutputFormat::Json),
25 "sarif" => Ok(OutputFormat::Sarif),
26 "html" => Ok(OutputFormat::Html),
27 "markdown" | "md" => Ok(OutputFormat::Markdown),
28 _ => Err(ParseEnumError::invalid("OutputFormat", s)),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum BadgeFormat {
37 Url,
39 #[default]
41 Markdown,
42 Html,
44}
45
46impl std::str::FromStr for BadgeFormat {
47 type Err = ParseEnumError;
48
49 fn from_str(s: &str) -> Result<Self, Self::Err> {
50 match s.to_lowercase().as_str() {
51 "url" => Ok(BadgeFormat::Url),
52 "markdown" | "md" => Ok(BadgeFormat::Markdown),
53 "html" => Ok(BadgeFormat::Html),
54 _ => Err(ParseEnumError::invalid("BadgeFormat", s)),
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "lowercase")]
61pub enum ScanType {
62 #[default]
63 Skill,
64 Hook,
65 Mcp,
66 Command,
67 Rules,
68 Docker,
69 Dependency,
70 Subagent,
72 Plugin,
74}
75
76impl std::str::FromStr for ScanType {
77 type Err = ParseEnumError;
78
79 fn from_str(s: &str) -> Result<Self, Self::Err> {
80 match s.to_lowercase().as_str() {
81 "skill" => Ok(ScanType::Skill),
82 "hook" => Ok(ScanType::Hook),
83 "mcp" => Ok(ScanType::Mcp),
84 "command" | "cmd" => Ok(ScanType::Command),
85 "rules" => Ok(ScanType::Rules),
86 "docker" => Ok(ScanType::Docker),
87 "dependency" | "dep" | "deps" => Ok(ScanType::Dependency),
88 "subagent" | "agent" => Ok(ScanType::Subagent),
89 "plugin" => Ok(ScanType::Plugin),
90 _ => Err(ParseEnumError::invalid("ScanType", s)),
91 }
92 }
93}
94
95#[derive(Subcommand, Debug, Clone)]
97pub enum HookAction {
98 Init {
100 #[arg(default_value = ".")]
102 path: PathBuf,
103 },
104 Remove {
106 #[arg(default_value = ".")]
108 path: PathBuf,
109 },
110}
111
112#[derive(Args, Debug, Clone)]
114pub struct CheckArgs {
115 #[arg(required_unless_present_any = ["remote", "remote_list", "awesome_claude_code", "all_clients", "client", "compare"])]
117 pub paths: Vec<PathBuf>,
118
119 #[arg(short = 'c', long = "config", value_name = "FILE")]
121 pub config: Option<PathBuf>,
122
123 #[arg(long, conflicts_with_all = ["remote", "remote_list", "awesome_claude_code", "client"])]
125 pub all_clients: bool,
126
127 #[arg(long, value_enum, conflicts_with_all = ["remote", "remote_list", "awesome_claude_code", "all_clients"])]
129 pub client: Option<ClientType>,
130
131 #[arg(long, value_name = "URL")]
133 pub remote: Option<String>,
134
135 #[arg(long, default_value = "HEAD")]
137 pub git_ref: String,
138
139 #[arg(long, env = "GITHUB_TOKEN", value_name = "TOKEN")]
141 pub remote_auth: Option<String>,
142
143 #[arg(long, conflicts_with = "remote", value_name = "FILE")]
145 pub remote_list: Option<PathBuf>,
146
147 #[arg(long, conflicts_with_all = ["remote", "remote_list"])]
149 pub awesome_claude_code: bool,
150
151 #[arg(long, default_value = "4")]
153 pub parallel_clones: usize,
154
155 #[arg(long)]
157 pub badge: bool,
158
159 #[arg(long, value_enum, default_value_t = BadgeFormat::Markdown)]
161 pub badge_format: BadgeFormat,
162
163 #[arg(long)]
165 pub summary: bool,
166
167 #[arg(short, long, value_enum, default_value_t = OutputFormat::Terminal)]
169 pub format: OutputFormat,
170
171 #[arg(short = 'S', long)]
173 pub strict: bool,
174
175 #[arg(long)]
177 pub warn_only: bool,
178
179 #[arg(long, value_enum)]
181 pub min_severity: Option<Severity>,
182
183 #[arg(long, value_enum)]
185 pub min_rule_severity: Option<RuleSeverity>,
186
187 #[arg(short = 't', long = "type", value_enum, default_value_t = ScanType::Skill)]
189 pub scan_type: ScanType,
190
191 #[arg(long = "no-recursive")]
193 pub no_recursive: bool,
194
195 #[arg(long)]
197 pub ci: bool,
198
199 #[arg(long, value_enum)]
201 pub min_confidence: Option<Confidence>,
202
203 #[arg(long)]
205 pub skip_comments: bool,
206
207 #[arg(long)]
209 pub strict_secrets: bool,
210
211 #[arg(long)]
213 pub fix_hint: bool,
214
215 #[arg(long)]
217 pub compact: bool,
218
219 #[arg(short, long)]
221 pub watch: bool,
222
223 #[arg(long)]
225 pub malware_db: Option<PathBuf>,
226
227 #[arg(long)]
229 pub no_malware_scan: bool,
230
231 #[arg(long)]
233 pub cve_db: Option<PathBuf>,
234
235 #[arg(long)]
237 pub no_cve_scan: bool,
238
239 #[arg(long)]
241 pub custom_rules: Option<PathBuf>,
242
243 #[arg(long)]
245 pub baseline: bool,
246
247 #[arg(long)]
249 pub check_drift: bool,
250
251 #[arg(short, long)]
253 pub output: Option<PathBuf>,
254
255 #[arg(long, value_name = "FILE")]
257 pub save_baseline: Option<PathBuf>,
258
259 #[arg(long, value_name = "FILE")]
261 pub baseline_file: Option<PathBuf>,
262
263 #[arg(long, num_args = 2, value_names = ["PATH1", "PATH2"])]
265 pub compare: Option<Vec<PathBuf>>,
266
267 #[arg(long)]
269 pub fix: bool,
270
271 #[arg(long)]
273 pub fix_dry_run: bool,
274
275 #[arg(long)]
277 pub hook_mode: bool,
278
279 #[arg(long)]
281 pub pin: bool,
282
283 #[arg(long)]
285 pub pin_verify: bool,
286
287 #[arg(long)]
289 pub pin_update: bool,
290
291 #[arg(long)]
293 pub pin_force: bool,
294
295 #[arg(long)]
297 pub ignore_pin: bool,
298
299 #[arg(long)]
301 pub deep_scan: bool,
302
303 #[arg(long, value_name = "NAME")]
305 pub profile: Option<String>,
306
307 #[arg(long, value_name = "NAME")]
309 pub save_profile: Option<String>,
310
311 #[arg(long)]
313 pub report_fp: bool,
314
315 #[arg(long)]
317 pub report_fp_dry_run: bool,
318
319 #[arg(long, value_name = "URL")]
321 pub report_fp_endpoint: Option<String>,
322
323 #[arg(long)]
325 pub no_telemetry: bool,
326
327 #[arg(long)]
329 pub sbom: bool,
330
331 #[arg(long, value_name = "FORMAT")]
333 pub sbom_format: Option<String>,
334
335 #[arg(long)]
337 pub sbom_npm: bool,
338
339 #[arg(long)]
341 pub sbom_cargo: bool,
342}
343
344#[derive(Args, Debug, Clone)]
346pub struct ProxyArgs {
347 #[arg(long, default_value = "8080")]
349 pub port: u16,
350
351 #[arg(long, required = true, value_name = "HOST:PORT")]
353 pub target: String,
354
355 #[arg(long)]
357 pub tls: bool,
358
359 #[arg(long)]
361 pub block: bool,
362
363 #[arg(long, value_name = "FILE")]
365 pub log: Option<PathBuf>,
366}
367
368#[derive(Subcommand, Debug, Clone)]
370pub enum Commands {
371 Init {
373 #[arg(default_value = ".cc-audit.yaml")]
375 path: PathBuf,
376 },
377
378 Check(Box<CheckArgs>),
380
381 Hook {
383 #[command(subcommand)]
384 action: HookAction,
385 },
386
387 Serve,
389
390 Proxy(ProxyArgs),
392}
393
394#[derive(Parser, Debug, Default)]
395#[command(
396 name = "cc-audit",
397 version,
398 about = "Security auditor for Claude Code skills, hooks, and MCP servers",
399 long_about = "cc-audit scans Claude Code skills, hooks, and MCP servers for security vulnerabilities before installation."
400)]
401pub struct Cli {
402 #[command(subcommand)]
404 pub command: Option<Commands>,
405
406 #[arg(short, long, global = true)]
408 pub verbose: bool,
409}
410
411impl Default for CheckArgs {
412 fn default() -> Self {
413 Self {
414 paths: Vec::new(),
415 config: None,
416 all_clients: false,
417 client: None,
418 remote: None,
419 git_ref: "HEAD".to_string(),
420 remote_auth: None,
421 remote_list: None,
422 awesome_claude_code: false,
423 parallel_clones: 4,
424 badge: false,
425 badge_format: BadgeFormat::Markdown,
426 summary: false,
427 format: OutputFormat::Terminal,
428 strict: false,
429 warn_only: false,
430 min_severity: None,
431 min_rule_severity: None,
432 scan_type: ScanType::Skill,
433 no_recursive: false,
434 ci: false,
435 min_confidence: None,
436 skip_comments: false,
437 strict_secrets: false,
438 fix_hint: false,
439 compact: false,
440 watch: false,
441 malware_db: None,
442 no_malware_scan: false,
443 cve_db: None,
444 no_cve_scan: false,
445 custom_rules: None,
446 baseline: false,
447 check_drift: false,
448 output: None,
449 save_baseline: None,
450 baseline_file: None,
451 compare: None,
452 fix: false,
453 fix_dry_run: false,
454 hook_mode: false,
455 pin: false,
456 pin_verify: false,
457 pin_update: false,
458 pin_force: false,
459 ignore_pin: false,
460 deep_scan: false,
461 profile: None,
462 save_profile: None,
463 report_fp: false,
464 report_fp_dry_run: false,
465 report_fp_endpoint: None,
466 no_telemetry: false,
467 sbom: false,
468 sbom_format: None,
469 sbom_npm: false,
470 sbom_cargo: false,
471 }
472 }
473}
474
475impl Default for ProxyArgs {
476 fn default() -> Self {
477 Self {
478 port: 8080,
479 target: String::new(),
480 tls: false,
481 block: false,
482 log: None,
483 }
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use crate::rules::{Confidence, RuleSeverity, Severity};
491 use clap::CommandFactory;
492
493 #[test]
494 fn test_cli_valid() {
495 Cli::command().debug_assert();
496 }
497
498 #[test]
501 fn test_no_args_succeeds() {
502 let cli = Cli::try_parse_from(["cc-audit"]).unwrap();
503 assert!(cli.command.is_none());
504 }
505
506 #[test]
509 fn test_parse_init_subcommand() {
510 let cli = Cli::try_parse_from(["cc-audit", "init"]).unwrap();
511 assert!(matches!(cli.command, Some(Commands::Init { .. })));
512 }
513
514 #[test]
515 fn test_parse_init_subcommand_with_path() {
516 let cli = Cli::try_parse_from(["cc-audit", "init", "custom-config.yaml"]).unwrap();
517 if let Some(Commands::Init { path }) = cli.command {
518 assert_eq!(path.to_str().unwrap(), "custom-config.yaml");
519 } else {
520 panic!("Expected Init command");
521 }
522 }
523
524 #[test]
527 fn test_parse_check_subcommand() {
528 let cli = Cli::try_parse_from(["cc-audit", "check", "./skill/"]).unwrap();
529 if let Some(Commands::Check(args)) = cli.command {
530 assert_eq!(args.paths.len(), 1);
531 assert!(!args.strict);
532 assert!(!args.no_recursive); } else {
534 panic!("Expected Check command");
535 }
536 }
537
538 #[test]
539 fn test_parse_check_multiple_paths() {
540 let cli = Cli::try_parse_from(["cc-audit", "check", "./skill1/", "./skill2/"]).unwrap();
541 if let Some(Commands::Check(args)) = cli.command {
542 assert_eq!(args.paths.len(), 2);
543 } else {
544 panic!("Expected Check command");
545 }
546 }
547
548 #[test]
549 fn test_parse_check_format_json() {
550 let cli =
551 Cli::try_parse_from(["cc-audit", "check", "--format", "json", "./skill/"]).unwrap();
552 if let Some(Commands::Check(args)) = cli.command {
553 assert!(matches!(args.format, OutputFormat::Json));
554 } else {
555 panic!("Expected Check command");
556 }
557 }
558
559 #[test]
560 fn test_parse_check_strict_mode() {
561 let cli = Cli::try_parse_from(["cc-audit", "check", "--strict", "./skill/"]).unwrap();
562 if let Some(Commands::Check(args)) = cli.command {
563 assert!(args.strict);
564 } else {
565 panic!("Expected Check command");
566 }
567 }
568
569 #[test]
570 fn test_parse_check_no_recursive() {
571 let cli =
572 Cli::try_parse_from(["cc-audit", "check", "--no-recursive", "./skills/"]).unwrap();
573 if let Some(Commands::Check(args)) = cli.command {
574 assert!(args.no_recursive);
575 } else {
576 panic!("Expected Check command");
577 }
578 }
579
580 #[test]
581 fn test_parse_check_format_sarif() {
582 let cli =
583 Cli::try_parse_from(["cc-audit", "check", "--format", "sarif", "./skill/"]).unwrap();
584 if let Some(Commands::Check(args)) = cli.command {
585 assert!(matches!(args.format, OutputFormat::Sarif));
586 } else {
587 panic!("Expected Check command");
588 }
589 }
590
591 #[test]
592 fn test_parse_check_type_hook() {
593 let cli = Cli::try_parse_from(["cc-audit", "check", "--type", "hook", "./settings.json"])
594 .unwrap();
595 if let Some(Commands::Check(args)) = cli.command {
596 assert!(matches!(args.scan_type, ScanType::Hook));
597 } else {
598 panic!("Expected Check command");
599 }
600 }
601
602 #[test]
603 fn test_parse_check_type_mcp() {
604 let cli =
605 Cli::try_parse_from(["cc-audit", "check", "--type", "mcp", "./mcp.json"]).unwrap();
606 if let Some(Commands::Check(args)) = cli.command {
607 assert!(matches!(args.scan_type, ScanType::Mcp));
608 } else {
609 panic!("Expected Check command");
610 }
611 }
612
613 #[test]
614 fn test_parse_check_ci_mode() {
615 let cli = Cli::try_parse_from(["cc-audit", "check", "--ci", "./skill/"]).unwrap();
616 if let Some(Commands::Check(args)) = cli.command {
617 assert!(args.ci);
618 } else {
619 panic!("Expected Check command");
620 }
621 }
622
623 #[test]
624 fn test_parse_check_verbose() {
625 let cli = Cli::try_parse_from(["cc-audit", "-v", "check", "./skill/"]).unwrap();
626 assert!(cli.verbose);
627 }
628
629 #[test]
630 fn test_parse_check_all_options() {
631 let cli = Cli::try_parse_from([
632 "cc-audit", "check", "--format", "json", "--strict", "--type", "hook", "--ci",
633 "./path/",
634 ])
635 .unwrap();
636 if let Some(Commands::Check(args)) = cli.command {
637 assert!(matches!(args.format, OutputFormat::Json));
638 assert!(args.strict);
639 assert!(matches!(args.scan_type, ScanType::Hook));
640 assert!(args.ci);
641 } else {
642 panic!("Expected Check command");
643 }
644 }
645
646 #[test]
647 fn test_parse_check_default_values() {
648 let cli = Cli::try_parse_from(["cc-audit", "check", "./skill/"]).unwrap();
649 if let Some(Commands::Check(args)) = cli.command {
650 assert!(matches!(args.format, OutputFormat::Terminal));
651 assert!(matches!(args.scan_type, ScanType::Skill));
652 assert!(!args.strict);
653 assert!(!args.no_recursive);
654 assert!(!args.ci);
655 assert!(args.min_confidence.is_none());
656 } else {
657 panic!("Expected Check command");
658 }
659 }
660
661 #[test]
662 fn test_parse_check_min_confidence() {
663 let cli = Cli::try_parse_from([
664 "cc-audit",
665 "check",
666 "--min-confidence",
667 "tentative",
668 "./skill/",
669 ])
670 .unwrap();
671 if let Some(Commands::Check(args)) = cli.command {
672 assert!(matches!(args.min_confidence, Some(Confidence::Tentative)));
673 } else {
674 panic!("Expected Check command");
675 }
676 }
677
678 #[test]
679 fn test_parse_check_skip_comments() {
680 let cli =
681 Cli::try_parse_from(["cc-audit", "check", "--skip-comments", "./skill/"]).unwrap();
682 if let Some(Commands::Check(args)) = cli.command {
683 assert!(args.skip_comments);
684 } else {
685 panic!("Expected Check command");
686 }
687 }
688
689 #[test]
690 fn test_parse_check_watch() {
691 let cli = Cli::try_parse_from(["cc-audit", "check", "--watch", "./skill/"]).unwrap();
692 if let Some(Commands::Check(args)) = cli.command {
693 assert!(args.watch);
694 } else {
695 panic!("Expected Check command");
696 }
697 }
698
699 #[test]
700 fn test_parse_check_watch_short() {
701 let cli = Cli::try_parse_from(["cc-audit", "check", "-w", "./skill/"]).unwrap();
702 if let Some(Commands::Check(args)) = cli.command {
703 assert!(args.watch);
704 } else {
705 panic!("Expected Check command");
706 }
707 }
708
709 #[test]
710 fn test_parse_check_malware_db() {
711 let cli = Cli::try_parse_from([
712 "cc-audit",
713 "check",
714 "--malware-db",
715 "./custom.json",
716 "./skill/",
717 ])
718 .unwrap();
719 if let Some(Commands::Check(args)) = cli.command {
720 assert!(args.malware_db.is_some());
721 assert_eq!(args.malware_db.unwrap().to_str().unwrap(), "./custom.json");
722 } else {
723 panic!("Expected Check command");
724 }
725 }
726
727 #[test]
728 fn test_parse_check_custom_rules() {
729 let cli = Cli::try_parse_from([
730 "cc-audit",
731 "check",
732 "--custom-rules",
733 "./rules.yaml",
734 "./skill/",
735 ])
736 .unwrap();
737 if let Some(Commands::Check(args)) = cli.command {
738 assert!(args.custom_rules.is_some());
739 assert_eq!(args.custom_rules.unwrap().to_str().unwrap(), "./rules.yaml");
740 } else {
741 panic!("Expected Check command");
742 }
743 }
744
745 #[test]
746 fn test_parse_check_config_option() {
747 let cli =
748 Cli::try_parse_from(["cc-audit", "check", "-c", "custom.yaml", "./skill/"]).unwrap();
749 if let Some(Commands::Check(args)) = cli.command {
750 assert_eq!(args.config.unwrap().to_str().unwrap(), "custom.yaml");
751 } else {
752 panic!("Expected Check command");
753 }
754 }
755
756 #[test]
757 fn test_parse_check_warn_only() {
758 let cli = Cli::try_parse_from(["cc-audit", "check", "--warn-only", "./skill/"]).unwrap();
759 if let Some(Commands::Check(args)) = cli.command {
760 assert!(args.warn_only);
761 } else {
762 panic!("Expected Check command");
763 }
764 }
765
766 #[test]
767 fn test_parse_check_min_severity() {
768 let cli = Cli::try_parse_from([
769 "cc-audit",
770 "check",
771 "--min-severity",
772 "critical",
773 "./skill/",
774 ])
775 .unwrap();
776 if let Some(Commands::Check(args)) = cli.command {
777 assert_eq!(args.min_severity, Some(Severity::Critical));
778 } else {
779 panic!("Expected Check command");
780 }
781 }
782
783 #[test]
784 fn test_parse_check_min_rule_severity() {
785 let cli = Cli::try_parse_from([
786 "cc-audit",
787 "check",
788 "--min-rule-severity",
789 "error",
790 "./skill/",
791 ])
792 .unwrap();
793 if let Some(Commands::Check(args)) = cli.command {
794 assert_eq!(args.min_rule_severity, Some(RuleSeverity::Error));
795 } else {
796 panic!("Expected Check command");
797 }
798 }
799
800 #[test]
801 fn test_parse_check_all_clients() {
802 let cli = Cli::try_parse_from(["cc-audit", "check", "--all-clients"]).unwrap();
803 if let Some(Commands::Check(args)) = cli.command {
804 assert!(args.all_clients);
805 assert!(args.paths.is_empty());
806 } else {
807 panic!("Expected Check command");
808 }
809 }
810
811 #[test]
812 fn test_parse_check_client_claude() {
813 let cli = Cli::try_parse_from(["cc-audit", "check", "--client", "claude"]).unwrap();
814 if let Some(Commands::Check(args)) = cli.command {
815 assert_eq!(args.client, Some(ClientType::Claude));
816 assert!(args.paths.is_empty());
817 } else {
818 panic!("Expected Check command");
819 }
820 }
821
822 #[test]
823 fn test_check_all_clients_conflicts_with_client() {
824 let result =
825 Cli::try_parse_from(["cc-audit", "check", "--all-clients", "--client", "claude"]);
826 assert!(result.is_err());
827 }
828
829 #[test]
832 fn test_parse_hook_init() {
833 let cli = Cli::try_parse_from(["cc-audit", "hook", "init"]).unwrap();
834 if let Some(Commands::Hook { action }) = cli.command {
835 assert!(matches!(action, HookAction::Init { .. }));
836 } else {
837 panic!("Expected Hook command");
838 }
839 }
840
841 #[test]
842 fn test_parse_hook_init_with_path() {
843 let cli = Cli::try_parse_from(["cc-audit", "hook", "init", "./repo/"]).unwrap();
844 if let Some(Commands::Hook { action }) = cli.command {
845 if let HookAction::Init { path } = action {
846 assert_eq!(path.to_str().unwrap(), "./repo/");
847 } else {
848 panic!("Expected HookAction::Init");
849 }
850 } else {
851 panic!("Expected Hook command");
852 }
853 }
854
855 #[test]
856 fn test_parse_hook_remove() {
857 let cli = Cli::try_parse_from(["cc-audit", "hook", "remove"]).unwrap();
858 if let Some(Commands::Hook { action }) = cli.command {
859 assert!(matches!(action, HookAction::Remove { .. }));
860 } else {
861 panic!("Expected Hook command");
862 }
863 }
864
865 #[test]
866 fn test_parse_hook_remove_with_path() {
867 let cli = Cli::try_parse_from(["cc-audit", "hook", "remove", "./repo/"]).unwrap();
868 if let Some(Commands::Hook { action }) = cli.command {
869 if let HookAction::Remove { path } = action {
870 assert_eq!(path.to_str().unwrap(), "./repo/");
871 } else {
872 panic!("Expected HookAction::Remove");
873 }
874 } else {
875 panic!("Expected Hook command");
876 }
877 }
878
879 #[test]
882 fn test_parse_serve() {
883 let cli = Cli::try_parse_from(["cc-audit", "serve"]).unwrap();
884 assert!(matches!(cli.command, Some(Commands::Serve)));
885 }
886
887 #[test]
890 fn test_parse_proxy() {
891 let cli = Cli::try_parse_from(["cc-audit", "proxy", "--target", "localhost:9000"]).unwrap();
892 if let Some(Commands::Proxy(args)) = cli.command {
893 assert_eq!(args.target, "localhost:9000");
894 assert_eq!(args.port, 8080); assert!(!args.tls);
896 assert!(!args.block);
897 } else {
898 panic!("Expected Proxy command");
899 }
900 }
901
902 #[test]
903 fn test_parse_proxy_with_all_options() {
904 let cli = Cli::try_parse_from([
905 "cc-audit",
906 "proxy",
907 "--target",
908 "localhost:9000",
909 "--port",
910 "3000",
911 "--tls",
912 "--block",
913 "--log",
914 "proxy.log",
915 ])
916 .unwrap();
917 if let Some(Commands::Proxy(args)) = cli.command {
918 assert_eq!(args.target, "localhost:9000");
919 assert_eq!(args.port, 3000);
920 assert!(args.tls);
921 assert!(args.block);
922 assert_eq!(args.log.unwrap().to_str().unwrap(), "proxy.log");
923 } else {
924 panic!("Expected Proxy command");
925 }
926 }
927
928 #[test]
929 fn test_proxy_requires_target() {
930 let result = Cli::try_parse_from(["cc-audit", "proxy"]);
931 assert!(result.is_err());
932 }
933
934 #[test]
937 fn test_verbose_global_flag() {
938 let cli = Cli::try_parse_from(["cc-audit", "-v", "check", "./skill/"]).unwrap();
939 assert!(cli.verbose);
940
941 let cli2 = Cli::try_parse_from(["cc-audit", "check", "-v", "./skill/"]).unwrap();
942 assert!(cli2.verbose);
943
944 let cli3 = Cli::try_parse_from(["cc-audit", "check", "./skill/", "-v"]).unwrap();
945 assert!(cli3.verbose);
946 }
947}