use std::{path::PathBuf, time::Instant};
use anyhow::Result;
use clap::Args;
use serde::Serialize;
use crate::{
cli::output,
config::CONFIG,
scan::{Scanner, types::ScanMode},
shared::time::format_time,
};
#[derive(Args, Clone, Serialize)]
pub struct ScanArgs {
#[arg(value_name = "PATH")]
pub paths: Vec<PathBuf>,
#[arg(long)]
pub include_binary: Option<bool>,
#[arg(long)]
pub max_file_size: Option<usize>,
#[arg(long)]
pub stats: bool,
#[arg(long)]
pub follow_symlinks: Option<bool>,
#[arg(long)]
pub no_entropy: Option<bool>,
#[arg(long)]
pub entropy_threshold: Option<f64>,
#[arg(long, value_delimiter = ',')]
pub ignore_patterns: Vec<String>,
#[arg(long, value_delimiter = ',')]
pub ignore_paths: Vec<String>,
#[arg(long, value_delimiter = ',')]
pub ignore_comments: Vec<String>,
#[arg(long, value_delimiter = ',')]
pub custom_patterns: Vec<String>,
#[arg(long, default_value = "text")]
pub format: OutputFormat,
#[arg(long)]
pub count_only: Option<bool>,
#[arg(long)]
pub show_content: Option<bool>,
#[arg(long)]
pub show: Option<bool>,
#[arg(long)]
pub sensitive: Option<bool>,
#[arg(long)]
pub report: Option<String>,
#[arg(long)]
pub list_patterns: Option<bool>,
#[arg(long, value_enum)]
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<ScanMode>,
#[arg(long)]
pub tty: Option<bool>,
#[arg(long)]
pub plain: bool,
}
#[derive(Clone, Debug, clap::ValueEnum, serde::Serialize, PartialEq)]
pub enum OutputFormat {
Text,
Json,
Csv,
Files,
}
pub async fn execute(args: ScanArgs, verbose_level: u8) -> Result<()> {
use crate::scan::static_data::patterns::get_pattern_library;
let pattern_lib = get_pattern_library();
if args.list_patterns.unwrap_or(false) {
output::styled!(
"{} Available Secret Detection Patterns ({} total):",
("📋", "info_symbol"),
(pattern_lib.count().to_string(), "property")
);
println!();
for pattern in pattern_lib.patterns() {
if verbose_level > 0 {
output::styled!(
"📋 {} - {}",
(pattern.name.clone(), "property"),
(pattern.description.clone(), "symbol")
);
} else {
output::styled!(" - {}", (pattern.name.clone(), "property"));
}
}
return Ok(());
}
let scanner = Scanner::new()?;
crate::cli::banner::print_banner(None);
output::styled!("{} Starting security scan...", ("ℹ", "info_symbol"));
let start_time = Instant::now();
let scan_paths = if args.paths.is_empty() {
vec![PathBuf::from(".")]
} else {
args.paths.clone()
};
let stats = scanner.scan(&scan_paths)?;
let elapsed = start_time.elapsed();
let total_files = stats.files_scanned;
let total_skipped = stats.files_skipped;
let total_matches = stats.total_matches;
let processing_errors = stats.processing_errors;
if args.count_only.unwrap_or(false) {
println!("{}", total_matches);
if total_matches > 0 {
std::process::exit(1);
}
return Ok(());
}
match args.format {
OutputFormat::Text => {
print_text_summary(
total_files,
total_skipped,
total_matches,
processing_errors,
elapsed,
&args,
)?;
}
OutputFormat::Json => {
println!(
"{{\"files_scanned\":{},\"files_skipped\":{},\"matches\":{},\"duration_ms\":{}}}",
total_files,
total_skipped,
total_matches,
elapsed.as_millis()
);
}
OutputFormat::Csv => {
println!("files_scanned,files_skipped,matches,duration_ms");
println!(
"{},{},{},{}",
total_files,
total_skipped,
total_matches,
elapsed.as_millis()
);
}
OutputFormat::Files => {
println!("Files-only output not yet supported in streaming mode");
}
}
if total_matches > 0 {
std::process::exit(1);
}
Ok(())
}
fn print_text_summary(
total_files: usize,
total_skipped: usize,
total_matches: usize,
processing_errors: usize,
elapsed: std::time::Duration,
args: &ScanArgs,
) -> Result<()> {
if total_matches == 0 {
println!();
output::styled!("{} No secrets detected!", ("✔", "success_symbol"));
if args.stats || args.format == OutputFormat::Text {
println!();
output::styled!(
"{} {}",
("📊", "info_symbol"),
("Scan Statistics", "property")
);
output::styled!(" Files scanned: {}", (total_files.to_string(), "symbol"));
if total_skipped > 0 {
output::styled!(" Files skipped: {}", (total_skipped.to_string(), "symbol"));
}
if processing_errors > 0 {
output::styled!(
" Processing errors: {}",
(processing_errors.to_string(), "error")
);
}
output::styled!(" Secrets found: {}", ("0", "symbol"));
output::styled!(" Scan time: {}", (format_time(elapsed), "symbol"));
}
return Ok(());
}
println!();
output::styled!(
"{} {} potential secrets found!",
("⚠️", "warning_symbol"),
(total_matches.to_string(), "error")
);
if CONFIG.scanner.show {
output::styled!(
"{}",
(
" Use --show flag to see detailed findings (already enabled)",
"muted"
)
);
} else {
output::styled!(
"{}",
(" Use --show flag to see detailed findings", "muted")
);
}
if args.stats || args.format == OutputFormat::Text {
println!();
output::styled!(
"{} {}",
("📊", "info_symbol"),
("Scan Statistics", "property")
);
output::styled!(" Files scanned: {}", (total_files.to_string(), "symbol"));
if total_skipped > 0 {
output::styled!(" Files skipped: {}", (total_skipped.to_string(), "symbol"));
}
if processing_errors > 0 {
output::styled!(
" Processing errors: {}",
(processing_errors.to_string(), "error")
);
}
output::styled!(" Secrets found: {}", (total_matches.to_string(), "error"));
output::styled!(" Scan time: {}", (format_time(elapsed), "symbol"));
}
Ok(())
}