mod api;
mod config;
mod dns;
mod models;
mod output;
mod pivot;
use anyhow::{Context, Result};
use clap::Parser;
use models::DiscoveryStats;
use output::*;
#[tokio::main]
async fn main() -> Result<()> {
let config = config::Config::try_parse().context("Failed to parse CLI arguments")?;
let pivot_config = config.pivot_config();
welcome(&config.domain);
let client = api::WhoxyClient::new(config.api_key(), config.verbose);
progress("Performing preflight check (API + WHOIS lookup)...");
let api_availability = client.preflight_check(&config.domain).await;
if api_availability.history_api {
success("✓ History API is available");
} else {
if config.continue_on_fail {
warning("✗ History API unavailable - will skip");
} else {
error("✗ History API unavailable - API key may be invalid");
eprintln!("\n💡 Tip: Use --continue-on-fail to proceed with available APIs");
return Ok(());
}
}
if api_availability.reverse_whois_api {
success("✓ Reverse WHOIS API is available");
} else {
if config.continue_on_fail {
warning("✗ Reverse WHOIS API unavailable - will skip");
} else {
error("✗ Reverse WHOIS API unavailable - API key may be invalid");
eprintln!("\n💡 Tip: Use --continue-on-fail to proceed with available APIs");
return Ok(());
}
}
let has_whois_attrs = api_availability.whois_attributes.as_ref()
.map(|attrs| !attrs.names.is_empty() || !attrs.emails.is_empty() || !attrs.companies.is_empty())
.unwrap_or(false);
if !api_availability.history_api && !api_availability.reverse_whois_api {
if has_whois_attrs && config.continue_on_fail {
warning("Preflight indicated APIs unavailable, but proceeding with extracted identifiers...");
} else {
error("No APIs are available. Cannot proceed.");
if has_whois_attrs {
eprintln!("\n💡 Tip: Use --continue-on-fail to attempt reverse WHOIS with extracted identifiers");
}
return Ok(());
}
}
if let Some(ref whois_attrs) = api_availability.whois_attributes {
if !whois_attrs.names.is_empty() || !whois_attrs.emails.is_empty() || !whois_attrs.companies.is_empty() {
progress("WHOIS identifiers extracted during preflight:");
let mut display_attrs = models::PivotAttributes::default();
display_attrs.merge(whois_attrs.clone());
display_pivot_attributes(&display_attrs, &pivot_config);
}
}
let history_records = if api_availability.history_api {
progress(&format!("Fetching WHOIS history for {}...", config.domain));
let history_response = client
.get_whois_history(&config.domain)
.await
.context("Failed to fetch WHOIS history")?;
if history_response.status != 1 {
if let Some(ref status_reason) = history_response.status_reason {
if status_reason.to_lowercase().contains("zero account balance") {
error(&format!("API account has zero balance: {}", status_reason));
} else if let Some(ref error_msg) = history_response.error {
error(&format!("Failed to fetch WHOIS history: {}", error_msg));
} else {
error(&format!("Failed to fetch WHOIS history: {}", status_reason));
}
} else if let Some(ref error_msg) = history_response.error {
error(&format!("Failed to fetch WHOIS history: {}", error_msg));
} else {
if history_response.history.is_empty() && history_response.status == 0 {
warning("No historical WHOIS records found for this domain in Whoxy's database.");
warning("This could mean:");
warning(" - The domain has no historical records");
warning(" - The API key is invalid (check your key)");
warning(" - The domain is not in Whoxy's database");
} else {
error(&format!("Failed to fetch WHOIS history - API returned status {}", history_response.status));
}
}
Vec::new()
} else {
let records = history_response.history;
success(&format!(
"Found {} historical WHOIS record(s)",
records.len()
));
records
}
} else {
warning("Skipping WHOIS history fetch (API unavailable)");
Vec::new()
};
let mut attributes = models::PivotAttributes::default();
if !config.search_names.is_empty() || !config.search_emails.is_empty() || !config.search_companies.is_empty() {
progress("Using manual identifiers for reverse WHOIS...");
for name in &config.search_names {
attributes.add_name(name.trim().to_string());
}
for email in &config.search_emails {
attributes.add_email(email.trim().to_lowercase());
}
for company in &config.search_companies {
attributes.add_company(company.trim().to_string());
}
}
if let Some(ref whois_attrs) = api_availability.whois_attributes {
if !whois_attrs.names.is_empty() || !whois_attrs.emails.is_empty() || !whois_attrs.companies.is_empty() {
progress("Using WHOIS identifiers from preflight check...");
attributes.merge(whois_attrs.clone());
}
}
if !history_records.is_empty() {
progress("Extracting pivotable attributes from historical records...");
let history_attrs = pivot::extract_pivot_attributes(&history_records);
attributes.merge(history_attrs);
}
if !attributes.names.is_empty() || !attributes.emails.is_empty() || !attributes.companies.is_empty() {
display_pivot_attributes(&attributes, &pivot_config);
}
let has_attributes = (pivot_config.use_name && !attributes.names.is_empty())
|| (pivot_config.use_email && !attributes.emails.is_empty())
|| (pivot_config.use_company && !attributes.companies.is_empty());
if !has_attributes {
error("No pivotable attributes found. Cannot perform reverse WHOIS lookups.");
eprintln!("\n💡 Options:");
eprintln!(" 1. Use --search-name, --search-email, or --search-company to manually specify identifiers");
eprintln!(" 2. Ensure the domain has WHOIS history or current WHOIS data available");
eprintln!(" 3. Ensure the system 'whois' command is available for current WHOIS lookup");
return Ok(());
}
if config.verbose {
progress("Attributes to use for reverse WHOIS:");
if pivot_config.use_name && !attributes.names.is_empty() {
eprintln!(" Names: {:?}", attributes.names);
}
if pivot_config.use_email && !attributes.emails.is_empty() {
eprintln!(" Emails: {:?}", attributes.emails);
}
if pivot_config.use_company && !attributes.companies.is_empty() {
eprintln!(" Companies: {:?}", attributes.companies);
}
}
if pivot::has_low_fidelity_pivots(&config.domain, &attributes, &pivot_config) {
output::display_fidelity_warning();
}
let discovered_domains = if has_attributes {
progress("Performing reverse WHOIS lookups...");
if !api_availability.reverse_whois_api {
warning("Preflight indicated reverse WHOIS API unavailable, but attempting anyway with extracted identifiers...");
}
match pivot::discover_domains(&client, &attributes, &pivot_config).await {
Ok(domains) => domains,
Err(e) => {
error(&format!("Reverse WHOIS lookup failed: {}", e));
if !api_availability.reverse_whois_api {
eprintln!("\n💡 The API may require account balance or valid API key.");
eprintln!(" Check your Whoxy account balance and API key.");
}
Vec::new()
}
}
} else {
warning("Skipping reverse WHOIS lookups (no attributes available)");
Vec::new()
};
let total_found = discovered_domains.len();
success(&format!("Found {} domain(s) from reverse WHOIS", total_found));
if discovered_domains.is_empty() {
warning("No domains discovered via reverse WHOIS.");
return Ok(());
}
progress("De-duplicating discovered domains...");
let deduplicated = pivot::deduplicate_domains(discovered_domains);
let after_dedup = deduplicated.len();
success(&format!(
"After de-duplication: {} domain(s)",
after_dedup
));
progress("Verifying domains via DNS lookup...");
let verified = dns::verify_domains_concurrent(deduplicated).await;
let after_dns = verified.len();
success(&format!(
"After DNS verification: {} domain(s)",
after_dns
));
display_domains(&verified);
let stats = DiscoveryStats {
total_domains_found: total_found,
after_deduplication: after_dedup,
after_dns_verification: after_dns,
domains_by_attribute: std::collections::HashMap::new(), };
display_stats(&stats);
summary(after_dns);
Ok(())
}