use crate::cli::Cli;
use crate::results::{self, Analysis};
use crate::steps;
use crate::user_config;
use anyhow::Result;
use clap::Parser;
use console::style;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::Client;
use std::env;
fn normalize_target(input: &str) -> String {
let mut s = input.trim();
if let Some(stripped) = s.strip_prefix("http://") {
s = stripped;
} else if let Some(stripped) = s.strip_prefix("https://") {
s = stripped;
}
if let Some(idx) = s.find('/') {
s = &s[..idx];
}
s.to_string()
}
fn new_spinner(msg: &str) -> ProgressBar {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::with_template("{spinner} {msg}")
.expect("valid spinner template")
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
);
pb.enable_steady_tick(std::time::Duration::from_millis(80));
pb.set_message(msg.to_owned());
pb
}
fn spinner(enabled: bool, label: &str) -> Option<ProgressBar> {
if enabled {
Some(new_spinner(label))
} else {
None
}
}
pub struct App {
cli: Cli,
client: Client,
results: Analysis,
vt_api_key: Option<String>,
llm_api_key: Option<String>,
user_cfg: user_config::UserConfig,
}
impl App {
pub fn new() -> Result<Self> {
let mut cli = Cli::parse();
cli.target = normalize_target(&cli.target);
let client = Client::builder()
.user_agent(format!("goran_cli/{}", env!("CARGO_PKG_VERSION")))
.build()?;
let initial_results = Analysis {
target: cli.target.clone(),
..Default::default()
};
let mut cfg = user_config::load();
let vt_api_key = if cli.vt {
cli
.vt_api_key_flag
.clone()
.or_else(|| env::var("VT_API_KEY").ok().filter(|k| !k.is_empty()))
.or_else(|| cfg.vt_api_key.clone())
} else {
None
};
let llm_api_key = if cli.llm_report {
cli
.llm_api_key_flag
.clone()
.or_else(|| {
std::env::var("GEMINI_API_KEY")
.ok()
.filter(|k| !k.is_empty())
})
.or_else(|| cfg.gemini_api_key.clone())
} else {
None
};
if cli.save_keys {
if let Some(k) = &vt_api_key {
cfg.vt_api_key = Some(k.clone());
}
if let Some(k) = &llm_api_key {
cfg.gemini_api_key = Some(k.clone());
}
if let Err(e) = user_config::store(&cfg) {
eprintln!("⚠️ Could not save keys: {e}");
}
}
Ok(Self {
cli,
client,
results: initial_results,
vt_api_key,
llm_api_key,
user_cfg: cfg,
})
}
pub async fn run(&mut self) -> Result<()> {
if self.cli.config_show {
println!("{:#?}", self.user_cfg);
return Ok(());
}
self.run_geo_lookup().await;
self.run_whois_lookup().await;
self.run_dns_lookup().await;
self.run_ssl_lookup().await;
self.run_vt_lookup().await;
if self.cli.llm_report {
self.run_llm_report().await
} else {
self.print_results()
}
}
async fn run_llm_report(&mut self) -> Result<()> {
let pb = spinner(!self.cli.json, "🤖 Gemini LLM Report");
let res = async {
let key = self
.llm_api_key
.as_deref()
.ok_or_else(|| anyhow::anyhow!(
"Gemini report requested, but no API key supplied (--llm-api-key or GEMINI_API_KEY)"
))?;
crate::providers::llm::generate_report(&self.results, key, &self.client).await
}
.await;
match res {
Ok(report) => {
if let Some(pb) = pb {
pb.finish_with_message(format!("{} Gemini LLM", style("✅").green()));
}
println!("\n{report}\n");
}
Err(err) => {
if let Some(pb) = pb {
pb.finish_with_message(format!("{} Gemini LLM", style("❌").red()));
}
eprintln!("❌ Gemini request failed: {err}");
self.results.errors.push(err.to_string());
}
}
Ok(())
}
async fn run_geo_lookup(&mut self) {
let pb = spinner(!self.cli.json, "🌐 Geolocation");
match steps::fetch_geo_step(&self.cli.target, &self.client).await {
Ok(info) => {
self.results.geo_info = Some(info);
if let Some(pb) = pb {
pb.finish_with_message(format!(
"{} Geolocation",
style("✅").green()
));
}
}
Err(e) => {
self.results.errors.push(e);
if let Some(pb) = pb {
pb.finish_with_message(format!("{} Geolocation", style("❌").red()));
}
}
}
}
async fn run_whois_lookup(&mut self) {
let pb = spinner(!self.cli.json && !self.cli.no_whois, "📜 WHOIS");
match steps::fetch_whois_step(&self.cli.target, &self.cli).await {
Ok(Some(info)) => {
self.results.whois_info = Some(info);
if let Some(pb) = pb {
pb.finish_with_message(format!("{} WHOIS", style("✅").green()));
}
}
Ok(None) => {
if self.cli.no_whois {
self
.results
.skipped_steps
.push("WHOIS (skipped by --no-whois flag)".into());
}
if let Some(pb) = pb {
pb.finish_with_message(format!("{} WHOIS", style("⚠️").yellow()));
}
}
Err(e) => {
self.results.errors.push(e);
if let Some(pb) = pb {
pb.finish_with_message(format!("{} WHOIS", style("❌").red()));
}
}
}
}
async fn run_dns_lookup(&mut self) {
let pb = spinner(!self.cli.json && !self.cli.no_dns, "🧭 DNS");
match steps::fetch_dns_step(&self.cli.target, &self.cli).await {
Ok(Some(info)) => {
self.results.dns_info = Some(info);
if let Some(pb) = pb {
pb.finish_with_message(format!("{} DNS", style("✅").green()));
}
}
Ok(None) => {
if self.cli.no_dns {
self
.results
.skipped_steps
.push("DNS (skipped by --no-dns flag)".into());
}
if let Some(pb) = pb {
pb.finish_with_message(format!("{} DNS", style("⚠️").yellow()));
}
}
Err(e) => {
self.results.errors.push(e);
if let Some(pb) = pb {
pb.finish_with_message(format!("{} DNS", style("❌").red()));
}
}
}
}
async fn run_ssl_lookup(&mut self) {
let pb = spinner(!self.cli.json && !self.cli.no_ssl, "🔒 SSL");
match steps::fetch_ssl_step(&self.cli.target, &self.cli).await {
Ok(Some(info)) => {
self.results.ssl_info = Some(info);
if let Some(pb) = pb {
pb.finish_with_message(format!("{} SSL", style("✅").green()));
}
}
Ok(None) => {
if self.cli.no_ssl {
self
.results
.skipped_steps
.push("SSL (skipped by --no-ssl flag)".into());
}
if let Some(pb) = pb {
pb.finish_with_message(format!("{} SSL", style("⚠️").yellow()));
}
}
Err(e) => {
self.results.errors.push(e);
if let Some(pb) = pb {
pb.finish_with_message(format!("{} SSL", style("❌").red()));
}
}
}
}
async fn run_vt_lookup(&mut self) {
let pb = spinner(!self.cli.json && self.cli.vt, "🕵️ VirusTotal");
match steps::fetch_vt_step(
&self.cli.target,
&self.cli,
&self.client,
self.vt_api_key.as_deref(),
)
.await
{
Ok(Some(info)) => {
self.results.vt_info = Some(info);
if let Some(pb) = pb {
pb.finish_with_message(format!("{} VirusTotal", style("✅").green()));
}
}
Ok(None) => {
}
Err(e) => {
self.results.errors.push(e);
if let Some(pb) = pb {
pb.finish_with_message(format!("{} VirusTotal", style("❌").red()));
}
}
}
}
fn print_results(&self) -> Result<()> {
if self.cli.json {
results::print_json(&self.results)
} else {
results::print_human_readable(&self.results);
Ok(())
}
}
}