use anyhow::Context;
use phishnano::{
convert_json_to_bincode, load_default_model, load_model_from_path, predict_url,
predict_url_detailed, IndicatorSource,
};
use std::env;
use std::process;
fn print_usage() {
eprintln!("Usage: phishnano-cli <URL> [--threshold <value>] [--detailed]");
eprintln!(" phishnano-cli --convert <json_path> <bincode_path>");
eprintln!();
eprintln!("Arguments:");
eprintln!(" <URL> URL to analyze");
eprintln!(" --threshold <v> Classification threshold (default: 0.45)");
eprintln!(" --detailed Show risk indicators explaining the score");
eprintln!(" --convert <json> <bin> Convert JSON model to bincode format");
eprintln!(" --model <path> Load model from file instead of embedded default");
eprintln!();
eprintln!("Classification:");
eprintln!(" score >= threshold: Phishing");
eprintln!(" score < threshold: Normal");
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
print_usage();
process::exit(1);
}
if args[1] == "--convert" {
if args.len() < 4 {
eprintln!("Error: --convert requires <json_path> and <bincode_path>");
process::exit(1);
}
let json_path = &args[2];
let bincode_path = &args[3];
match convert_json_to_bincode(json_path, bincode_path) {
Ok(size) => {
println!("Converted: {} -> {}", json_path, bincode_path);
println!(
"Bincode size: {} bytes ({:.2} KB)",
size,
size as f64 / 1024.0
);
if let Ok(json_meta) = std::fs::metadata(json_path) {
let json_size = json_meta.len();
println!(
"JSON size: {} bytes ({:.2} KB)",
json_size,
json_size as f64 / 1024.0
);
let ratio = size as f64 / json_size as f64 * 100.0;
println!("Compression: {:.1}% of original", ratio);
}
}
Err(e) => {
eprintln!("Error: Failed to convert: {}", e);
process::exit(1);
}
}
return;
}
let url = &args[1];
let mut threshold = 0.45;
let mut model_path: Option<String> = None;
let mut detailed = false;
let mut i = 2;
while i < args.len() {
if args[i] == "--threshold" && i + 1 < args.len() {
threshold = args[i + 1].parse().unwrap_or_else(|_| {
eprintln!("Error: Invalid threshold value");
process::exit(1);
});
i += 2;
} else if args[i] == "--model" && i + 1 < args.len() {
model_path = Some(args[i + 1].clone());
i += 2;
} else if args[i] == "--detailed" {
detailed = true;
i += 1;
} else {
i += 1;
}
}
let model = if let Some(path) = model_path {
load_model_from_path(&path).with_context(|| format!("Failed to load model from {}", path))
} else {
load_default_model().context("Failed to load embedded model")
}
.unwrap_or_else(|e| {
eprintln!("Error: {}", e);
process::exit(1);
});
if detailed {
let result = predict_url_detailed(url, &model);
let classification = if result.score >= threshold {
"Phishing"
} else {
"Normal"
};
println!("Score: {:.4}", result.score);
println!("Classification: {}", classification);
if !result.indicators.is_empty() {
println!();
println!("Risk Indicators ({}):", result.indicators.len());
for (i, ind) in result.indicators.iter().enumerate() {
let source_tag = match &ind.source {
IndicatorSource::Model {
tree_count,
total_trees,
} => format!("Model, {}/{} trees", tree_count, total_trees),
IndicatorSource::Heuristic => "Heuristic".to_string(),
};
println!(" {}. [{}] {}", i + 1, source_tag, ind.description);
}
}
} else {
let score = predict_url(url, &model);
let classification = if score >= threshold {
"Phishing"
} else {
"Normal"
};
println!("Score: {:.4}", score);
println!("Classification: {}", classification);
}
}