Skip to main content

cc_audit/
cli.rs

1use crate::client::ClientType;
2use crate::rules::{Confidence, ParseEnumError, RuleSeverity, Severity};
3use clap::{Parser, 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/// Badge output format for security badges
34#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum BadgeFormat {
37    /// shields.io URL only
38    Url,
39    /// Markdown badge with link
40    #[default]
41    Markdown,
42    /// HTML image tag
43    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    /// Scan .claude/agents/ subagent definitions
71    Subagent,
72    /// Scan marketplace.json plugin definitions
73    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(Parser, Debug)]
96#[command(
97    name = "cc-audit",
98    version,
99    about = "Security auditor for Claude Code skills, hooks, and MCP servers",
100    long_about = "cc-audit scans Claude Code skills, hooks, and MCP servers for security vulnerabilities before installation."
101)]
102pub struct Cli {
103    /// Paths to scan (files or directories)
104    #[arg(required_unless_present_any = ["remote", "remote_list", "awesome_claude_code", "init", "all_clients", "client", "hook_mode", "mcp_server"])]
105    pub paths: Vec<PathBuf>,
106
107    /// Scan all installed AI coding clients (Claude, Cursor, Windsurf, VS Code)
108    #[arg(long, conflicts_with_all = ["remote", "remote_list", "awesome_claude_code", "client"])]
109    pub all_clients: bool,
110
111    /// Scan a specific AI coding client
112    #[arg(long, value_enum, conflicts_with_all = ["remote", "remote_list", "awesome_claude_code", "all_clients"])]
113    pub client: Option<ClientType>,
114
115    /// Remote repository URL to scan (e.g., `https://github.com/user/repo`)
116    #[arg(long, value_name = "URL")]
117    pub remote: Option<String>,
118
119    /// Git ref (branch, tag, or commit) for remote scan
120    #[arg(long, default_value = "HEAD")]
121    pub git_ref: String,
122
123    /// GitHub token for authentication (or use GITHUB_TOKEN env var)
124    #[arg(long, env = "GITHUB_TOKEN", value_name = "TOKEN")]
125    pub remote_auth: Option<String>,
126
127    /// File containing list of repository URLs to scan (one per line)
128    #[arg(long, conflicts_with = "remote", value_name = "FILE")]
129    pub remote_list: Option<PathBuf>,
130
131    /// Scan all repositories from awesome-claude-code
132    #[arg(long, conflicts_with_all = ["remote", "remote_list"])]
133    pub awesome_claude_code: bool,
134
135    /// Maximum number of parallel repository clones
136    #[arg(long, default_value = "4")]
137    pub parallel_clones: usize,
138
139    /// Generate security badge
140    #[arg(long)]
141    pub badge: bool,
142
143    /// Badge output format (url, markdown, html)
144    #[arg(long, value_enum, default_value_t = BadgeFormat::Markdown)]
145    pub badge_format: BadgeFormat,
146
147    /// Show summary only (for batch scans)
148    #[arg(long)]
149    pub summary: bool,
150
151    /// Output format
152    #[arg(short, long, value_enum, default_value_t = OutputFormat::Terminal)]
153    pub format: OutputFormat,
154
155    /// Strict mode: show medium/low severity findings and treat warnings as errors
156    #[arg(short, long)]
157    pub strict: bool,
158
159    /// Warn-only mode: treat all findings as warnings (exit code 0)
160    #[arg(long)]
161    pub warn_only: bool,
162
163    /// Minimum severity level to include in output (critical, high, medium, low)
164    #[arg(long, value_enum)]
165    pub min_severity: Option<Severity>,
166
167    /// Minimum rule severity to treat as errors (error, warn)
168    #[arg(long, value_enum)]
169    pub min_rule_severity: Option<RuleSeverity>,
170
171    /// Scan type
172    #[arg(short = 't', long = "type", value_enum, default_value_t = ScanType::Skill)]
173    pub scan_type: ScanType,
174
175    /// Recursive scan
176    #[arg(short, long)]
177    pub recursive: bool,
178
179    /// CI mode: non-interactive output
180    #[arg(long)]
181    pub ci: bool,
182
183    /// Verbose output
184    #[arg(short, long)]
185    pub verbose: bool,
186
187    /// Include test directories (tests/, spec/, __tests__, etc.) in scan
188    #[arg(long)]
189    pub include_tests: bool,
190
191    /// Include node_modules directories in scan
192    #[arg(long)]
193    pub include_node_modules: bool,
194
195    /// Include vendor directories (vendor/, third_party/) in scan
196    #[arg(long)]
197    pub include_vendor: bool,
198
199    /// Minimum confidence level for findings to be reported
200    #[arg(long, value_enum, default_value_t = Confidence::Tentative)]
201    pub min_confidence: Confidence,
202
203    /// Skip comment lines when scanning (lines starting with #, //, --, etc.)
204    #[arg(long)]
205    pub skip_comments: bool,
206
207    /// Strict secrets mode: disable dummy key heuristics for test files
208    /// By default, findings in test files have their confidence downgraded.
209    /// This option disables that behavior and treats all secrets equally.
210    #[arg(long)]
211    pub strict_secrets: bool,
212
213    /// Show fix hints in terminal output
214    #[arg(long)]
215    pub fix_hint: bool,
216
217    /// Watch mode: continuously monitor files for changes and re-scan
218    #[arg(short, long)]
219    pub watch: bool,
220
221    /// Install cc-audit pre-commit hook in the git repository
222    #[arg(long)]
223    pub init_hook: bool,
224
225    /// Remove cc-audit pre-commit hook from the git repository
226    #[arg(long)]
227    pub remove_hook: bool,
228
229    /// Path to a custom malware signatures database (JSON)
230    #[arg(long)]
231    pub malware_db: Option<PathBuf>,
232
233    /// Disable malware signature scanning
234    #[arg(long)]
235    pub no_malware_scan: bool,
236
237    /// Path to a custom CVE database (JSON)
238    #[arg(long)]
239    pub cve_db: Option<PathBuf>,
240
241    /// Disable CVE vulnerability scanning
242    #[arg(long)]
243    pub no_cve_scan: bool,
244
245    /// Path to a custom rules file (YAML format)
246    #[arg(long)]
247    pub custom_rules: Option<PathBuf>,
248
249    /// Create a baseline snapshot for drift detection (rug pull prevention)
250    #[arg(long)]
251    pub baseline: bool,
252
253    /// Check for drift against saved baseline
254    #[arg(long)]
255    pub check_drift: bool,
256
257    /// Generate a default configuration file template
258    #[arg(long)]
259    pub init: bool,
260
261    /// Output file path (for HTML/JSON output)
262    #[arg(short, long)]
263    pub output: Option<PathBuf>,
264
265    /// Save baseline to specified file
266    #[arg(long, value_name = "FILE")]
267    pub save_baseline: Option<PathBuf>,
268
269    /// Compare against baseline file (show only new findings)
270    #[arg(long, value_name = "FILE")]
271    pub baseline_file: Option<PathBuf>,
272
273    /// Compare two paths and show differences
274    #[arg(long, num_args = 2, value_names = ["PATH1", "PATH2"])]
275    pub compare: Option<Vec<PathBuf>>,
276
277    /// Auto-fix issues (where possible)
278    #[arg(long)]
279    pub fix: bool,
280
281    /// Preview auto-fix changes without applying them
282    #[arg(long)]
283    pub fix_dry_run: bool,
284
285    /// Run as MCP server
286    #[arg(long)]
287    pub mcp_server: bool,
288
289    /// Run as Claude Code Hook (reads from stdin, writes to stdout)
290    #[arg(long)]
291    pub hook_mode: bool,
292
293    /// Pin MCP tool configurations for rug-pull detection
294    #[arg(long)]
295    pub pin: bool,
296
297    /// Verify MCP tool pins against current configuration
298    #[arg(long)]
299    pub pin_verify: bool,
300
301    /// Update MCP tool pins with current configuration
302    #[arg(long)]
303    pub pin_update: bool,
304
305    /// Force overwrite existing pins
306    #[arg(long)]
307    pub pin_force: bool,
308
309    /// Skip pin verification during scan
310    #[arg(long)]
311    pub ignore_pin: bool,
312
313    /// Enable deep scan with deobfuscation
314    #[arg(long)]
315    pub deep_scan: bool,
316
317    /// Load settings from a named profile
318    #[arg(long, value_name = "NAME")]
319    pub profile: Option<String>,
320
321    /// Save current settings as a named profile
322    #[arg(long, value_name = "NAME")]
323    pub save_profile: Option<String>,
324
325    /// Report a false positive finding
326    #[arg(long)]
327    pub report_fp: bool,
328
329    /// Dry run mode for false positive reporting (print without submitting)
330    #[arg(long)]
331    pub report_fp_dry_run: bool,
332
333    /// Custom endpoint URL for false positive reporting
334    #[arg(long, value_name = "URL")]
335    pub report_fp_endpoint: Option<String>,
336
337    /// Disable telemetry and false positive reporting
338    #[arg(long)]
339    pub no_telemetry: bool,
340
341    /// Generate SBOM (Software Bill of Materials)
342    #[arg(long)]
343    pub sbom: bool,
344
345    /// SBOM output format (cyclonedx, spdx)
346    #[arg(long, value_name = "FORMAT")]
347    pub sbom_format: Option<String>,
348
349    /// Include npm dependencies in SBOM
350    #[arg(long)]
351    pub sbom_npm: bool,
352
353    /// Include Cargo dependencies in SBOM
354    #[arg(long)]
355    pub sbom_cargo: bool,
356
357    /// Enable proxy mode for runtime MCP monitoring
358    #[arg(long)]
359    pub proxy: bool,
360
361    /// Proxy listen port (default: 8080)
362    #[arg(long, value_name = "PORT")]
363    pub proxy_port: Option<u16>,
364
365    /// Target MCP server address (host:port)
366    #[arg(long, value_name = "HOST:PORT")]
367    pub proxy_target: Option<String>,
368
369    /// Enable TLS termination in proxy mode
370    #[arg(long)]
371    pub proxy_tls: bool,
372
373    /// Enable blocking mode (block messages with findings)
374    #[arg(long)]
375    pub proxy_block: bool,
376
377    /// Log file for proxy traffic (JSONL format)
378    #[arg(long, value_name = "FILE")]
379    pub proxy_log: Option<std::path::PathBuf>,
380}
381
382impl Default for Cli {
383    fn default() -> Self {
384        Self {
385            paths: Vec::new(),
386            all_clients: false,
387            client: None,
388            remote: None,
389            git_ref: "HEAD".to_string(),
390            remote_auth: None,
391            remote_list: None,
392            awesome_claude_code: false,
393            parallel_clones: 4,
394            badge: false,
395            badge_format: BadgeFormat::Markdown,
396            summary: false,
397            format: OutputFormat::Terminal,
398            strict: false,
399            warn_only: false,
400            min_severity: None,
401            min_rule_severity: None,
402            scan_type: ScanType::Skill,
403            recursive: false,
404            ci: false,
405            verbose: false,
406            include_tests: false,
407            include_node_modules: false,
408            include_vendor: false,
409            min_confidence: Confidence::Tentative,
410            skip_comments: false,
411            strict_secrets: false,
412            fix_hint: false,
413            watch: false,
414            init_hook: false,
415            remove_hook: false,
416            malware_db: None,
417            no_malware_scan: false,
418            cve_db: None,
419            no_cve_scan: false,
420            custom_rules: None,
421            baseline: false,
422            check_drift: false,
423            init: false,
424            output: None,
425            save_baseline: None,
426            baseline_file: None,
427            compare: None,
428            fix: false,
429            fix_dry_run: false,
430            mcp_server: false,
431            hook_mode: false,
432            pin: false,
433            pin_verify: false,
434            pin_update: false,
435            pin_force: false,
436            ignore_pin: false,
437            deep_scan: false,
438            profile: None,
439            save_profile: None,
440            report_fp: false,
441            report_fp_dry_run: false,
442            report_fp_endpoint: None,
443            no_telemetry: false,
444            sbom: false,
445            sbom_format: None,
446            sbom_npm: false,
447            sbom_cargo: false,
448            proxy: false,
449            proxy_port: None,
450            proxy_target: None,
451            proxy_tls: false,
452            proxy_block: false,
453            proxy_log: None,
454        }
455    }
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461    use crate::rules::{Confidence, RuleSeverity, Severity};
462    use clap::CommandFactory;
463
464    #[test]
465    fn test_cli_valid() {
466        Cli::command().debug_assert();
467    }
468
469    #[test]
470    fn test_parse_basic_args() {
471        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
472        assert_eq!(cli.paths.len(), 1);
473        assert!(!cli.strict);
474        assert!(!cli.recursive);
475    }
476
477    #[test]
478    fn test_parse_multiple_paths() {
479        let cli = Cli::try_parse_from(["cc-audit", "./skill1/", "./skill2/"]).unwrap();
480        assert_eq!(cli.paths.len(), 2);
481    }
482
483    #[test]
484    fn test_parse_format_json() {
485        let cli = Cli::try_parse_from(["cc-audit", "--format", "json", "./skill/"]).unwrap();
486        assert!(matches!(cli.format, OutputFormat::Json));
487    }
488
489    #[test]
490    fn test_parse_strict_mode() {
491        let cli = Cli::try_parse_from(["cc-audit", "--strict", "./skill/"]).unwrap();
492        assert!(cli.strict);
493    }
494
495    #[test]
496    fn test_parse_recursive() {
497        let cli = Cli::try_parse_from(["cc-audit", "-r", "./skills/"]).unwrap();
498        assert!(cli.recursive);
499    }
500
501    #[test]
502    fn test_parse_format_sarif() {
503        let cli = Cli::try_parse_from(["cc-audit", "--format", "sarif", "./skill/"]).unwrap();
504        assert!(matches!(cli.format, OutputFormat::Sarif));
505    }
506
507    #[test]
508    fn test_parse_type_hook() {
509        let cli = Cli::try_parse_from(["cc-audit", "--type", "hook", "./settings.json"]).unwrap();
510        assert!(matches!(cli.scan_type, ScanType::Hook));
511    }
512
513    #[test]
514    fn test_parse_type_mcp() {
515        let cli = Cli::try_parse_from(["cc-audit", "--type", "mcp", "./mcp.json"]).unwrap();
516        assert!(matches!(cli.scan_type, ScanType::Mcp));
517    }
518
519    #[test]
520    fn test_parse_type_command() {
521        let cli = Cli::try_parse_from(["cc-audit", "--type", "command", "./"]).unwrap();
522        assert!(matches!(cli.scan_type, ScanType::Command));
523    }
524
525    #[test]
526    fn test_parse_type_rules() {
527        let cli = Cli::try_parse_from(["cc-audit", "--type", "rules", "./"]).unwrap();
528        assert!(matches!(cli.scan_type, ScanType::Rules));
529    }
530
531    #[test]
532    fn test_parse_type_docker() {
533        let cli = Cli::try_parse_from(["cc-audit", "--type", "docker", "./"]).unwrap();
534        assert!(matches!(cli.scan_type, ScanType::Docker));
535    }
536
537    #[test]
538    fn test_parse_type_dependency() {
539        let cli = Cli::try_parse_from(["cc-audit", "--type", "dependency", "./"]).unwrap();
540        assert!(matches!(cli.scan_type, ScanType::Dependency));
541    }
542
543    #[test]
544    fn test_parse_ci_mode() {
545        let cli = Cli::try_parse_from(["cc-audit", "--ci", "./skill/"]).unwrap();
546        assert!(cli.ci);
547    }
548
549    #[test]
550    fn test_parse_verbose() {
551        let cli = Cli::try_parse_from(["cc-audit", "-v", "./skill/"]).unwrap();
552        assert!(cli.verbose);
553    }
554
555    #[test]
556    fn test_parse_all_options() {
557        let cli = Cli::try_parse_from([
558            "cc-audit",
559            "--format",
560            "json",
561            "--strict",
562            "--type",
563            "hook",
564            "--recursive",
565            "--ci",
566            "--verbose",
567            "./path/",
568        ])
569        .unwrap();
570        assert!(matches!(cli.format, OutputFormat::Json));
571        assert!(cli.strict);
572        assert!(matches!(cli.scan_type, ScanType::Hook));
573        assert!(cli.recursive);
574        assert!(cli.ci);
575        assert!(cli.verbose);
576    }
577
578    #[test]
579    fn test_default_values() {
580        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
581        assert!(matches!(cli.format, OutputFormat::Terminal));
582        assert!(matches!(cli.scan_type, ScanType::Skill));
583        assert!(!cli.strict);
584        assert!(!cli.recursive);
585        assert!(!cli.ci);
586        assert!(!cli.verbose);
587        assert!(!cli.include_tests);
588        assert!(!cli.include_node_modules);
589        assert!(!cli.include_vendor);
590        assert!(matches!(cli.min_confidence, Confidence::Tentative));
591    }
592
593    #[test]
594    fn test_parse_include_tests() {
595        let cli = Cli::try_parse_from(["cc-audit", "--include-tests", "./skill/"]).unwrap();
596        assert!(cli.include_tests);
597    }
598
599    #[test]
600    fn test_parse_include_node_modules() {
601        let cli = Cli::try_parse_from(["cc-audit", "--include-node-modules", "./skill/"]).unwrap();
602        assert!(cli.include_node_modules);
603    }
604
605    #[test]
606    fn test_parse_include_vendor() {
607        let cli = Cli::try_parse_from(["cc-audit", "--include-vendor", "./skill/"]).unwrap();
608        assert!(cli.include_vendor);
609    }
610
611    #[test]
612    fn test_parse_all_include_options() {
613        let cli = Cli::try_parse_from([
614            "cc-audit",
615            "--include-tests",
616            "--include-node-modules",
617            "--include-vendor",
618            "./skill/",
619        ])
620        .unwrap();
621        assert!(cli.include_tests);
622        assert!(cli.include_node_modules);
623        assert!(cli.include_vendor);
624    }
625
626    #[test]
627    fn test_parse_min_confidence_tentative() {
628        let cli =
629            Cli::try_parse_from(["cc-audit", "--min-confidence", "tentative", "./skill/"]).unwrap();
630        assert!(matches!(cli.min_confidence, Confidence::Tentative));
631    }
632
633    #[test]
634    fn test_parse_min_confidence_firm() {
635        let cli =
636            Cli::try_parse_from(["cc-audit", "--min-confidence", "firm", "./skill/"]).unwrap();
637        assert!(matches!(cli.min_confidence, Confidence::Firm));
638    }
639
640    #[test]
641    fn test_parse_min_confidence_certain() {
642        let cli =
643            Cli::try_parse_from(["cc-audit", "--min-confidence", "certain", "./skill/"]).unwrap();
644        assert!(matches!(cli.min_confidence, Confidence::Certain));
645    }
646
647    #[test]
648    fn test_parse_skip_comments() {
649        let cli = Cli::try_parse_from(["cc-audit", "--skip-comments", "./skill/"]).unwrap();
650        assert!(cli.skip_comments);
651    }
652
653    #[test]
654    fn test_default_skip_comments_false() {
655        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
656        assert!(!cli.skip_comments);
657    }
658
659    #[test]
660    fn test_parse_fix_hint() {
661        let cli = Cli::try_parse_from(["cc-audit", "--fix-hint", "./skill/"]).unwrap();
662        assert!(cli.fix_hint);
663    }
664
665    #[test]
666    fn test_default_fix_hint_false() {
667        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
668        assert!(!cli.fix_hint);
669    }
670
671    #[test]
672    fn test_parse_watch() {
673        let cli = Cli::try_parse_from(["cc-audit", "--watch", "./skill/"]).unwrap();
674        assert!(cli.watch);
675    }
676
677    #[test]
678    fn test_parse_watch_short() {
679        let cli = Cli::try_parse_from(["cc-audit", "-w", "./skill/"]).unwrap();
680        assert!(cli.watch);
681    }
682
683    #[test]
684    fn test_default_watch_false() {
685        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
686        assert!(!cli.watch);
687    }
688
689    #[test]
690    fn test_parse_init_hook() {
691        let cli = Cli::try_parse_from(["cc-audit", "--init-hook", "./repo/"]).unwrap();
692        assert!(cli.init_hook);
693    }
694
695    #[test]
696    fn test_parse_remove_hook() {
697        let cli = Cli::try_parse_from(["cc-audit", "--remove-hook", "./repo/"]).unwrap();
698        assert!(cli.remove_hook);
699    }
700
701    #[test]
702    fn test_default_init_hook_false() {
703        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
704        assert!(!cli.init_hook);
705    }
706
707    #[test]
708    fn test_default_remove_hook_false() {
709        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
710        assert!(!cli.remove_hook);
711    }
712
713    #[test]
714    fn test_parse_malware_db() {
715        let cli =
716            Cli::try_parse_from(["cc-audit", "--malware-db", "./custom.json", "./skill/"]).unwrap();
717        assert!(cli.malware_db.is_some());
718        assert_eq!(cli.malware_db.unwrap().to_str().unwrap(), "./custom.json");
719    }
720
721    #[test]
722    fn test_default_malware_db_none() {
723        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
724        assert!(cli.malware_db.is_none());
725    }
726
727    #[test]
728    fn test_parse_no_malware_scan() {
729        let cli = Cli::try_parse_from(["cc-audit", "--no-malware-scan", "./skill/"]).unwrap();
730        assert!(cli.no_malware_scan);
731    }
732
733    #[test]
734    fn test_default_no_malware_scan_false() {
735        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
736        assert!(!cli.no_malware_scan);
737    }
738
739    #[test]
740    fn test_parse_custom_rules() {
741        let cli = Cli::try_parse_from(["cc-audit", "--custom-rules", "./rules.yaml", "./skill/"])
742            .unwrap();
743        assert!(cli.custom_rules.is_some());
744        assert_eq!(cli.custom_rules.unwrap().to_str().unwrap(), "./rules.yaml");
745    }
746
747    #[test]
748    fn test_default_custom_rules_none() {
749        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
750        assert!(cli.custom_rules.is_none());
751    }
752
753    #[test]
754    fn test_parse_init() {
755        let cli = Cli::try_parse_from(["cc-audit", "--init", "./"]).unwrap();
756        assert!(cli.init);
757    }
758
759    #[test]
760    fn test_default_init_false() {
761        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
762        assert!(!cli.init);
763    }
764
765    #[test]
766    fn test_parse_warn_only() {
767        let cli = Cli::try_parse_from(["cc-audit", "--warn-only", "./skill/"]).unwrap();
768        assert!(cli.warn_only);
769    }
770
771    #[test]
772    fn test_default_warn_only_false() {
773        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
774        assert!(!cli.warn_only);
775    }
776
777    #[test]
778    fn test_parse_min_severity_critical() {
779        let cli =
780            Cli::try_parse_from(["cc-audit", "--min-severity", "critical", "./skill/"]).unwrap();
781        assert_eq!(cli.min_severity, Some(Severity::Critical));
782    }
783
784    #[test]
785    fn test_parse_min_severity_high() {
786        let cli = Cli::try_parse_from(["cc-audit", "--min-severity", "high", "./skill/"]).unwrap();
787        assert_eq!(cli.min_severity, Some(Severity::High));
788    }
789
790    #[test]
791    fn test_parse_min_severity_medium() {
792        let cli =
793            Cli::try_parse_from(["cc-audit", "--min-severity", "medium", "./skill/"]).unwrap();
794        assert_eq!(cli.min_severity, Some(Severity::Medium));
795    }
796
797    #[test]
798    fn test_parse_min_severity_low() {
799        let cli = Cli::try_parse_from(["cc-audit", "--min-severity", "low", "./skill/"]).unwrap();
800        assert_eq!(cli.min_severity, Some(Severity::Low));
801    }
802
803    #[test]
804    fn test_default_min_severity_none() {
805        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
806        assert!(cli.min_severity.is_none());
807    }
808
809    #[test]
810    fn test_parse_min_rule_severity_error() {
811        let cli =
812            Cli::try_parse_from(["cc-audit", "--min-rule-severity", "error", "./skill/"]).unwrap();
813        assert_eq!(cli.min_rule_severity, Some(RuleSeverity::Error));
814    }
815
816    #[test]
817    fn test_parse_min_rule_severity_warn() {
818        let cli =
819            Cli::try_parse_from(["cc-audit", "--min-rule-severity", "warn", "./skill/"]).unwrap();
820        assert_eq!(cli.min_rule_severity, Some(RuleSeverity::Warn));
821    }
822
823    #[test]
824    fn test_default_min_rule_severity_none() {
825        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
826        assert!(cli.min_rule_severity.is_none());
827    }
828
829    #[test]
830    fn test_warn_only_with_strict_conflict() {
831        // Both options can be parsed, but logic will determine behavior
832        let cli = Cli::try_parse_from(["cc-audit", "--warn-only", "--strict", "./skill/"]).unwrap();
833        assert!(cli.warn_only);
834        assert!(cli.strict);
835    }
836
837    #[test]
838    fn test_parse_all_clients() {
839        let cli = Cli::try_parse_from(["cc-audit", "--all-clients"]).unwrap();
840        assert!(cli.all_clients);
841        assert!(cli.paths.is_empty());
842    }
843
844    #[test]
845    fn test_parse_client_claude() {
846        let cli = Cli::try_parse_from(["cc-audit", "--client", "claude"]).unwrap();
847        assert_eq!(cli.client, Some(ClientType::Claude));
848        assert!(cli.paths.is_empty());
849    }
850
851    #[test]
852    fn test_parse_client_cursor() {
853        let cli = Cli::try_parse_from(["cc-audit", "--client", "cursor"]).unwrap();
854        assert_eq!(cli.client, Some(ClientType::Cursor));
855    }
856
857    #[test]
858    fn test_parse_client_windsurf() {
859        let cli = Cli::try_parse_from(["cc-audit", "--client", "windsurf"]).unwrap();
860        assert_eq!(cli.client, Some(ClientType::Windsurf));
861    }
862
863    #[test]
864    fn test_parse_client_vscode() {
865        let cli = Cli::try_parse_from(["cc-audit", "--client", "vscode"]).unwrap();
866        assert_eq!(cli.client, Some(ClientType::Vscode));
867    }
868
869    #[test]
870    fn test_all_clients_conflicts_with_client() {
871        let result = Cli::try_parse_from(["cc-audit", "--all-clients", "--client", "claude"]);
872        assert!(result.is_err());
873    }
874
875    #[test]
876    fn test_all_clients_conflicts_with_remote() {
877        let result = Cli::try_parse_from([
878            "cc-audit",
879            "--all-clients",
880            "--remote",
881            "https://github.com/x/y",
882        ]);
883        assert!(result.is_err());
884    }
885
886    #[test]
887    fn test_default_client_none() {
888        let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
889        assert!(cli.client.is_none());
890        assert!(!cli.all_clients);
891    }
892}