use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
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};
mod output;
use output::{print_scan_results, OutputFormat};
#[derive(Parser, Debug)]
#[command(name = "kindly-guard-cli")]
#[command(about = "Security scanner for detecting unicode attacks and injection threats", long_about = None)]
struct Cli {
#[arg(short, long)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Scan {
path: String,
#[arg(short, long, default_value = "table")]
format: String,
#[arg(short, long)]
recursive: bool,
#[arg(short, long)]
extensions: Option<String>,
#[arg(long, default_value = "10")]
max_size_mb: u64,
#[arg(short, long)]
config: Option<String>,
},
Monitor {
#[arg(short, long, default_value = "http://localhost:8080")]
url: String,
#[arg(short, long, default_value = "5")]
interval: u64,
},
Shield {
#[command(subcommand)]
command: ShieldCommands,
},
ShellInit {
shell: String,
},
Wrap {
#[arg(trailing_var_arg = true, required = true)]
command: Vec<String>,
#[arg(short, long, default_value = "http://localhost:8080")]
server: String,
#[arg(short, long)]
block: bool,
},
}
#[derive(Subcommand, Debug)]
enum ShieldCommands {
Status {
#[arg(short, long, default_value = "compact")]
format: String,
},
Start {
#[arg(short, long)]
background: bool,
},
Stop,
PreCommand,
PostCommand,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let log_level = if cli.verbose { "debug" } else { "info" };
tracing_subscriber::fmt()
.with_env_filter(format!("kindly_guard={log_level}"))
.init();
match cli.command {
Commands::Scan {
path,
format,
recursive,
extensions,
max_size_mb,
config,
} => scan_command(path, format, recursive, extensions, max_size_mb, config).await,
Commands::Monitor { url, interval } => monitor_command(url, interval).await,
Commands::Shield { command } => shield_command(command).await,
Commands::ShellInit { shell } => shell_init_command(&shell).await,
Commands::Wrap {
command,
server,
block,
} => wrap_command(command, server, block).await,
}
}
async fn scan_command(
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), };
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)
}
async fn monitor_command(url: String, interval: u64) -> Result<()> {
println!(
"{}",
format!("Monitoring KindlyGuard server at {url}").green()
);
println!("Press Ctrl+C to stop\n");
loop {
match fetch_server_status(&url).await {
Ok(status) => {
print_server_status(&status);
}
Err(e) => {
println!("{}: {}", "Error".red(), e);
}
}
tokio::time::sleep(tokio::time::Duration::from_secs(interval)).await;
}
}
async fn fetch_server_status(_url: &str) -> Result<serde_json::Value> {
Ok(serde_json::json!({
"active": true,
"uptime_seconds": 3600,
"threats_blocked": 42,
"scanner_stats": {
"unicode_threats": 23,
"injection_threats": 15,
"total_scans": 1000,
}
}))
}
fn print_server_status(status: &serde_json::Value) {
use chrono::Duration;
let active = status["active"].as_bool().unwrap_or(false);
let uptime_secs = status["uptime_seconds"].as_u64().unwrap_or(0);
let threats_blocked = status["threats_blocked"].as_u64().unwrap_or(0);
print!("\x1B[2J\x1B[1;1H");
println!("{}", "╭──────────────────────────────────────╮".cyan());
println!("{}", "│ 🛡️ KindlyGuard Server Status │".cyan());
println!("{}", "├──────────────────────────────────────┤".cyan());
let status_text = if active {
"● Active".green()
} else {
"○ Inactive".red()
};
println!("│ Status: {status_text:28} │");
let duration = Duration::seconds(uptime_secs as i64);
let hours = duration.num_hours();
let minutes = (duration.num_minutes() % 60) as u64;
let seconds = (duration.num_seconds() % 60) as u64;
let uptime_str = format!("{hours}h {minutes}m {seconds}s");
println!("│ Uptime: {uptime_str:28} │");
println!("│ Threats Blocked: {threats_blocked:19} │");
if let Some(stats) = status["scanner_stats"].as_object() {
println!("{}", "├──────────────────────────────────────┤".cyan());
println!("│ Scanner Statistics: │");
println!(
"│ Unicode threats: {:17} │",
stats["unicode_threats"].as_u64().unwrap_or(0)
);
println!(
"│ Injection threats: {:15} │",
stats["injection_threats"].as_u64().unwrap_or(0)
);
println!(
"│ Total scans: {:21} │",
stats["total_scans"].as_u64().unwrap_or(0)
);
}
println!("{}", "╰──────────────────────────────────────╯".cyan());
}
async fn shield_command(command: ShieldCommands) -> Result<()> {
use kindly_guard_server::shield::{CliShield, DisplayFormat, Shield};
use std::sync::Arc;
match command {
ShieldCommands::Status { format } => {
let shield = Arc::new(Shield::new());
let cli_shield = CliShield::new(shield.clone(), DisplayFormat::Compact);
match format.as_str() {
"json" => {
let status = cli_shield.status();
println!("{}", serde_json::to_string_pretty(&status)?);
}
"minimal" => {
let cli_shield = CliShield::new(shield, DisplayFormat::Minimal);
println!("{}", cli_shield.render());
}
_ => {
println!("{}", cli_shield.render());
}
}
}
ShieldCommands::Start { background } => {
if background {
println!("Starting KindlyGuard shield in background...");
println!("{}", "Shield started in background mode".green());
} else {
println!("Starting KindlyGuard shield...");
println!("{}", "Shield is active".green());
}
}
ShieldCommands::Stop => {
println!("Stopping KindlyGuard shield...");
println!("{}", "Shield stopped".yellow());
}
ShieldCommands::PreCommand => {
}
ShieldCommands::PostCommand => {
}
}
Ok(())
}
async fn shell_init_command(shell: &str) -> Result<()> {
let script = match shell {
"bash" => include_str!("../scripts/shell-init.bash"),
"zsh" => include_str!("../scripts/shell-init.zsh"),
"fish" => include_str!("../scripts/shell-init.fish"),
_ => {
anyhow::bail!(
"Unsupported shell: {}. Supported shells: bash, zsh, fish",
shell
);
}
};
println!("{script}");
Ok(())
}
async fn wrap_command(command: Vec<String>, server: String, block: bool) -> Result<()> {
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
if command.is_empty() {
anyhow::bail!("No command specified");
}
println!(
"{} Active",
"🛡️ KindlyGuard Protection:".green().bold()
);
println!("{} {}", "Server:".dimmed(), server);
println!(
"{} {}",
"Mode:".dimmed(),
if block { "Blocking" } else { "Warning" }
);
println!();
let config = ServerConfig::default();
let scanner = SecurityScanner::new(config.scanner)?;
let program = &command[0];
let args = &command[1..];
let mut child = Command::new(program)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("Failed to start command")?;
let mut stdin = child.stdin.take().context("Failed to get stdin")?;
let stdout = child.stdout.take().context("Failed to get stdout")?;
let stderr = child.stderr.take().context("Failed to get stderr")?;
let stdout_handle = tokio::spawn(async move {
let reader = BufReader::new(stdout);
for line in reader.lines() {
match line {
Ok(content) => println!("{}", content),
Err(e) => eprintln!("Error reading stdout: {}", e),
}
}
});
let stderr_handle = tokio::spawn(async move {
let reader = BufReader::new(stderr);
for line in reader.lines() {
match line {
Ok(content) => eprintln!("{}", content),
Err(e) => eprintln!("Error reading stderr: {}", e),
}
}
});
let stdin_reader = std::io::stdin();
let mut stdin_buf = String::new();
loop {
stdin_buf.clear();
match stdin_reader.read_line(&mut stdin_buf) {
Ok(0) => break, Ok(_) => {
let threats = scanner.scan_text(&stdin_buf)?;
if !threats.is_empty() {
eprintln!();
eprintln!("{}", "⚠️ THREAT DETECTED".red().bold());
for threat in &threats {
eprintln!(" {} {}", "•".red(), threat);
}
if block {
eprintln!("{}", "❌ Input blocked for safety".red());
eprintln!();
continue; } else {
eprintln!("{}", "⚠️ Proceeding with caution...".yellow());
eprintln!();
}
}
stdin.write_all(stdin_buf.as_bytes())?;
stdin.flush()?;
}
Err(e) => {
eprintln!("Error reading input: {}", e);
break;
}
}
}
drop(stdin);
stdout_handle.await?;
stderr_handle.await?;
let status = child.wait()?;
println!();
println!(
"{} Session ended",
"🛡️ KindlyGuard Protection:".green().bold()
);
if !status.success() {
std::process::exit(status.code().unwrap_or(1));
}
Ok(())
}