use std::path::PathBuf;
use clap::{Parser, ValueEnum};
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
pub enum SeverityFilter {
Critical,
High,
Medium,
#[default]
Low,
Info,
}
impl SeverityFilter {
pub fn to_severity(self) -> crate::finding::Severity {
use crate::finding::Severity;
match self {
SeverityFilter::Critical => Severity::Critical,
SeverityFilter::High => Severity::High,
SeverityFilter::Medium => Severity::Medium,
SeverityFilter::Low => Severity::Low,
SeverityFilter::Info => Severity::Info,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum CategoryFilter {
Config,
Secrets,
Permissions,
Network,
Deps,
Hooks,
History,
}
impl CategoryFilter {
pub fn to_category(self) -> crate::finding::Category {
use crate::finding::Category;
match self {
CategoryFilter::Config => Category::ConfigSecurity,
CategoryFilter::Secrets => Category::SecretDetection,
CategoryFilter::Permissions => Category::FilePermissions,
CategoryFilter::Network => Category::NetworkSecurity,
CategoryFilter::Deps => Category::DependencySecurity,
CategoryFilter::Hooks => Category::HookSecurity,
CategoryFilter::History => Category::DataExposure,
}
}
}
#[derive(Parser, Debug)]
#[command(
name = "ocls",
version,
author,
about = "Security scanner for agentic AI framework installations",
long_about = None,
)]
pub struct Cli {
#[arg(value_name = "PATH")]
pub paths: Vec<PathBuf>,
#[arg(short = 'j', long)]
pub json: bool,
#[arg(short = 'q', long)]
pub quiet: bool,
#[arg(short = 'v', long)]
pub verbose: bool,
#[arg(long)]
pub no_color: bool,
#[arg(long, value_name = "CATEGORY")]
pub category: Option<CategoryFilter>,
#[arg(long, value_name = "SEVERITY", default_value = "low")]
pub min_severity: SeverityFilter,
#[arg(long, value_name = "GLOB")]
pub ignore_path: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn cli_debug_assert() {
Cli::command().debug_assert();
}
#[test]
fn severity_filter_default_is_low() {
let cli = Cli::parse_from(["ocls"]);
assert_eq!(cli.min_severity, SeverityFilter::Low);
}
#[test]
fn severity_filter_to_severity_roundtrip() {
use crate::finding::Severity;
assert_eq!(SeverityFilter::Critical.to_severity(), Severity::Critical);
assert_eq!(SeverityFilter::High.to_severity(), Severity::High);
assert_eq!(SeverityFilter::Medium.to_severity(), Severity::Medium);
assert_eq!(SeverityFilter::Low.to_severity(), Severity::Low);
assert_eq!(SeverityFilter::Info.to_severity(), Severity::Info);
}
#[test]
fn json_flag_parsed() {
let cli = Cli::parse_from(["ocls", "--json"]);
assert!(cli.json);
}
#[test]
fn verbose_flag_parsed() {
let cli = Cli::parse_from(["ocls", "-v"]);
assert!(cli.verbose);
}
#[test]
fn no_color_flag_parsed() {
let cli = Cli::parse_from(["ocls", "--no-color"]);
assert!(cli.no_color);
}
#[test]
fn multiple_paths_parsed() {
let cli = Cli::parse_from(["ocls", "/tmp/a", "/tmp/b"]);
assert_eq!(cli.paths.len(), 2);
}
#[test]
fn category_filter_to_category() {
use crate::finding::Category;
assert_eq!(
CategoryFilter::Secrets.to_category(),
Category::SecretDetection
);
assert_eq!(
CategoryFilter::Config.to_category(),
Category::ConfigSecurity
);
assert_eq!(
CategoryFilter::Permissions.to_category(),
Category::FilePermissions
);
assert_eq!(
CategoryFilter::Network.to_category(),
Category::NetworkSecurity
);
assert_eq!(
CategoryFilter::Deps.to_category(),
Category::DependencySecurity
);
assert_eq!(CategoryFilter::Hooks.to_category(), Category::HookSecurity);
assert_eq!(
CategoryFilter::History.to_category(),
Category::DataExposure
);
}
#[test]
fn min_severity_medium_parsed() {
let cli = Cli::parse_from(["ocls", "--min-severity", "medium"]);
assert_eq!(cli.min_severity, SeverityFilter::Medium);
}
#[test]
fn ignore_path_repeatable() {
let cli = Cli::parse_from(["ocls", "--ignore-path", "*.log", "--ignore-path", "tmp/"]);
assert_eq!(cli.ignore_path.len(), 2);
}
}