use anyhow::{Context, Result};
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use std::path::{Path, PathBuf};
use std::time::Instant;
use walkdir::WalkDir;
use kindly_guard_server::{Config as ServerConfig, ScannerConfig, SecurityScanner, Threat};
use crate::output::{print_scan_results, OutputFormat};
pub async fn execute(
path: String,
format: String,
recursive: bool,
extensions: Option<String>,
max_size_mb: u64,
config_path: Option<String>,
) -> Result<()> {
let start_time = Instant::now();
let path = Path::new(&path);
if !path.exists() {
anyhow::bail!("Path does not exist: {}", path.display());
}
let output_format = OutputFormat::from_str(&format)?;
let allowed_extensions: Option<Vec<String>> =
extensions.map(|ext| ext.split(',').map(|s| s.trim().to_lowercase()).collect());
let scanner = if let Some(config_file) = config_path {
let server_config = ServerConfig::load_from_file(&config_file)
.context("Failed to load configuration file")?;
let mut scanner = SecurityScanner::new(server_config.scanner.clone())
.context("Failed to create security scanner")?;
if server_config.plugins.enabled {
use kindly_guard_server::component_selector::ComponentManager;
use std::sync::Arc;
let component_manager = Arc::new(
ComponentManager::new(&server_config)
.context("Failed to create component manager")?,
);
scanner.set_plugin_manager(component_manager.plugin_manager().clone());
}
scanner
} else {
let config = ScannerConfig {
unicode_detection: true,
injection_detection: true,
path_traversal_detection: true,
xss_detection: Some(true),
crypto_detection: true,
enhanced_mode: Some(false),
custom_patterns: None,
max_scan_depth: 10,
enable_event_buffer: false,
max_content_size: 5 * 1024 * 1024, max_input_size: Some(10 * 1024 * 1024), allow_text_control_chars: false,
};
SecurityScanner::new(config).context("Failed to create security scanner")?
};
let files_to_scan = collect_files(path, recursive, &allowed_extensions, max_size_mb)?;
if files_to_scan.is_empty() {
println!("{}", "No files found to scan".yellow());
return Ok(());
}
let progress = if output_format == OutputFormat::Json {
None
} else {
let pb = ProgressBar::new(files_to_scan.len() as u64);
let style = ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}")
.unwrap_or_else(|_| ProgressStyle::default_bar())
.progress_chars("#>-");
pb.set_style(style);
Some(pb)
};
let mut all_results = Vec::new();
let mut total_threats = 0;
for file_path in &files_to_scan {
if let Some(pb) = &progress {
pb.set_message(format!(
"Scanning {}",
file_path.file_name().unwrap_or_default().to_string_lossy()
));
}
match scan_file(&scanner, file_path).await {
Ok(threats) => {
if !threats.is_empty() {
total_threats += threats.len();
all_results.push((file_path.clone(), threats));
}
},
Err(e) => {
tracing::warn!("Failed to scan {}: {}", file_path.display(), e);
},
}
if let Some(pb) = &progress {
pb.inc(1);
}
}
if let Some(pb) = progress {
pb.finish_with_message("Scan complete");
}
let duration = start_time.elapsed();
print_scan_results(
&all_results,
files_to_scan.len(),
total_threats,
duration,
output_format,
);
Ok(())
}
async fn scan_file(scanner: &SecurityScanner, path: &Path) -> Result<Vec<Threat>> {
let content = tokio::fs::read_to_string(path)
.await
.context("Failed to read file")?;
scanner
.scan_text(&content)
.context("Failed to scan file content")
}
fn collect_files(
path: &Path,
recursive: bool,
allowed_extensions: &Option<Vec<String>>,
max_size_mb: u64,
) -> Result<Vec<PathBuf>> {
let max_size = max_size_mb * 1024 * 1024;
let mut files = Vec::new();
if path.is_file() {
let metadata = path.metadata()?;
if metadata.len() <= max_size {
files.push(path.to_path_buf());
} else {
tracing::warn!(
"Skipping large file: {} ({} MB)",
path.display(),
metadata.len() / 1024 / 1024
);
}
} else if path.is_dir() {
let walker = if recursive {
WalkDir::new(path)
} else {
WalkDir::new(path).max_depth(1)
};
for entry in walker {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ref extensions) = allowed_extensions {
if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
if !extensions.contains(&ext_str) {
continue;
}
} else {
continue; }
}
let metadata = entry.metadata()?;
if metadata.len() <= max_size {
files.push(path.to_path_buf());
} else {
tracing::debug!(
"Skipping large file: {} ({} MB)",
path.display(),
metadata.len() / 1024 / 1024
);
}
}
}
}
Ok(files)
}