1use std::path::PathBuf;
4
5use clap::{Parser, ValueEnum};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
11pub enum SeverityFilter {
12 Critical,
13 High,
14 Medium,
15 #[default]
16 Low,
17 Info,
18}
19
20impl SeverityFilter {
21 pub fn to_severity(self) -> crate::finding::Severity {
23 use crate::finding::Severity;
24 match self {
25 SeverityFilter::Critical => Severity::Critical,
26 SeverityFilter::High => Severity::High,
27 SeverityFilter::Medium => Severity::Medium,
28 SeverityFilter::Low => Severity::Low,
29 SeverityFilter::Info => Severity::Info,
30 }
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
38pub enum CategoryFilter {
39 Config,
40 Secrets,
41 Permissions,
42 Network,
43 Deps,
44 Hooks,
45 History,
46}
47
48impl CategoryFilter {
49 pub fn to_category(self) -> crate::finding::Category {
51 use crate::finding::Category;
52 match self {
53 CategoryFilter::Config => Category::ConfigSecurity,
54 CategoryFilter::Secrets => Category::SecretDetection,
55 CategoryFilter::Permissions => Category::FilePermissions,
56 CategoryFilter::Network => Category::NetworkSecurity,
57 CategoryFilter::Deps => Category::DependencySecurity,
58 CategoryFilter::Hooks => Category::HookSecurity,
59 CategoryFilter::History => Category::DataExposure,
60 }
61 }
62}
63
64#[derive(Parser, Debug)]
76#[command(
77 name = "ocls",
78 version,
79 author,
80 about = "Security scanner for agentic AI framework installations",
81 long_about = None,
82)]
83pub struct Cli {
84 #[arg(value_name = "PATH")]
89 pub paths: Vec<PathBuf>,
90
91 #[arg(short = 'j', long)]
93 pub json: bool,
94
95 #[arg(short = 'q', long)]
97 pub quiet: bool,
98
99 #[arg(short = 'v', long)]
101 pub verbose: bool,
102
103 #[arg(long)]
107 pub no_color: bool,
108
109 #[arg(long, value_name = "CATEGORY")]
111 pub category: Option<CategoryFilter>,
112
113 #[arg(long, value_name = "SEVERITY", default_value = "low")]
115 pub min_severity: SeverityFilter,
116
117 #[arg(long, value_name = "GLOB")]
119 pub ignore_path: Vec<String>,
120}
121
122#[cfg(test)]
125mod tests {
126 use super::*;
127 use clap::CommandFactory;
128
129 #[test]
130 fn cli_debug_assert() {
131 Cli::command().debug_assert();
133 }
134
135 #[test]
136 fn severity_filter_default_is_low() {
137 let cli = Cli::parse_from(["ocls"]);
138 assert_eq!(cli.min_severity, SeverityFilter::Low);
139 }
140
141 #[test]
142 fn severity_filter_to_severity_roundtrip() {
143 use crate::finding::Severity;
144 assert_eq!(SeverityFilter::Critical.to_severity(), Severity::Critical);
145 assert_eq!(SeverityFilter::High.to_severity(), Severity::High);
146 assert_eq!(SeverityFilter::Medium.to_severity(), Severity::Medium);
147 assert_eq!(SeverityFilter::Low.to_severity(), Severity::Low);
148 assert_eq!(SeverityFilter::Info.to_severity(), Severity::Info);
149 }
150
151 #[test]
152 fn json_flag_parsed() {
153 let cli = Cli::parse_from(["ocls", "--json"]);
154 assert!(cli.json);
155 }
156
157 #[test]
158 fn verbose_flag_parsed() {
159 let cli = Cli::parse_from(["ocls", "-v"]);
160 assert!(cli.verbose);
161 }
162
163 #[test]
164 fn no_color_flag_parsed() {
165 let cli = Cli::parse_from(["ocls", "--no-color"]);
166 assert!(cli.no_color);
167 }
168
169 #[test]
170 fn multiple_paths_parsed() {
171 let cli = Cli::parse_from(["ocls", "/tmp/a", "/tmp/b"]);
172 assert_eq!(cli.paths.len(), 2);
173 }
174
175 #[test]
176 fn category_filter_to_category() {
177 use crate::finding::Category;
178 assert_eq!(
179 CategoryFilter::Secrets.to_category(),
180 Category::SecretDetection
181 );
182 assert_eq!(
183 CategoryFilter::Config.to_category(),
184 Category::ConfigSecurity
185 );
186 assert_eq!(
187 CategoryFilter::Permissions.to_category(),
188 Category::FilePermissions
189 );
190 assert_eq!(
191 CategoryFilter::Network.to_category(),
192 Category::NetworkSecurity
193 );
194 assert_eq!(
195 CategoryFilter::Deps.to_category(),
196 Category::DependencySecurity
197 );
198 assert_eq!(CategoryFilter::Hooks.to_category(), Category::HookSecurity);
199 assert_eq!(
200 CategoryFilter::History.to_category(),
201 Category::DataExposure
202 );
203 }
204
205 #[test]
206 fn min_severity_medium_parsed() {
207 let cli = Cli::parse_from(["ocls", "--min-severity", "medium"]);
208 assert_eq!(cli.min_severity, SeverityFilter::Medium);
209 }
210
211 #[test]
212 fn ignore_path_repeatable() {
213 let cli = Cli::parse_from(["ocls", "--ignore-path", "*.log", "--ignore-path", "tmp/"]);
214 assert_eq!(cli.ignore_path.len(), 2);
215 }
216}