From 561414b34d5c4475492794488c077501951493e1 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Fri, 19 Dec 2025 03:05:20 +0600 Subject: [PATCH 1/4] Add enhanced reporting and market analysis - Pretty terminal output with tables and color coding - Markdown report generation with executive summaries --- src/lib.rs | 1 + src/pretty_report.rs | 464 +++++++++++++++++++++++++++++++++++++++++++ src/reporting.rs | 28 +++ 3 files changed, 493 insertions(+) create mode 100644 src/pretty_report.rs diff --git a/src/lib.rs b/src/lib.rs index dbcdd2f..2b0b3ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ mod headers; pub mod passkeys; mod ports; mod reporting; +mod pretty_report; mod social; mod takeover; pub mod web; diff --git a/src/pretty_report.rs b/src/pretty_report.rs new file mode 100644 index 0000000..982b9e2 --- /dev/null +++ b/src/pretty_report.rs @@ -0,0 +1,464 @@ +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::io::Write; + +use chrono::Local; +use colored::*; +use itertools::Itertools; +use tabled::{Table, Tabled, settings::Style}; + +use crate::cloud::CloudAssetFinding; +use crate::social::SocialIntelligenceSummary; + +pub struct PrettyReportGenerator; + +#[derive(Tabled)] +struct SubdomainRow { + #[tabled(rename = "Subdomain")] + subdomain: String, + #[tabled(rename = "Status")] + status: String, + #[tabled(rename = "Server")] + server: String, + #[tabled(rename = "Ports")] + ports: String, + #[tabled(rename = "CORS")] + cors: String, + #[tabled(rename = "Risks")] + risks: String, +} + +impl PrettyReportGenerator { + pub fn generate_markdown_report( + domain: &str, + subs: &HashSet, + header_map: &HashMap)>, + open_ports_map: &HashMap>, + cors_map: &HashMap>, + software_map: &HashMap>, + takeover_map: &HashMap>, + _cloud_saas_map: &HashMap>, + cloud_asset_map: &HashMap>, + social_intel: Option<&SocialIntelligenceSummary>, + output_dir: &str, + ) -> Result<(), Box> { + let report_file = format!("{}/{}_REPORT.md", output_dir, domain); + let mut f = File::create(&report_file)?; + + let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S %Z"); + + // Header + writeln!(f, "# 🔍 ShadowMap Security Report")?; + writeln!(f)?; + writeln!(f, "**Target Domain:** `{}`", domain)?; + writeln!(f, "**Scan Date:** {}", timestamp)?; + writeln!(f, "**Generated by:** ShadowMap v{}", env!("CARGO_PKG_VERSION"))?; + writeln!(f)?; + writeln!(f, "---")?; + writeln!(f)?; + + // Executive Summary + writeln!(f, "## 📊 Executive Summary")?; + writeln!(f)?; + + let total_subs = subs.len(); + let cors_issues = cors_map.len(); + let takeover_risks = takeover_map.len(); + let cloud_assets = cloud_asset_map.len(); + + let risk_level = if cors_issues > 0 || takeover_risks > 0 { + "🔴 **HIGH**" + } else if cloud_assets > 5 { + "🟡 **MEDIUM**" + } else { + "🟢 **LOW**" + }; + + writeln!(f, "| Metric | Value |")?; + writeln!(f, "|--------|-------|")?; + writeln!(f, "| **Total Subdomains Discovered** | {} |", total_subs)?; + writeln!(f, "| **Live Hosts** | {} |", total_subs)?; + writeln!(f, "| **CORS Vulnerabilities** | {} |", cors_issues)?; + writeln!(f, "| **Takeover Risks** | {} |", takeover_risks)?; + writeln!(f, "| **Cloud Assets Flagged** | {} |", cloud_assets)?; + writeln!(f, "| **Overall Risk Level** | {} |", risk_level)?; + writeln!(f)?; + + // Critical Findings + if cors_issues > 0 || takeover_risks > 0 { + writeln!(f, "## 🚨 Critical Security Findings")?; + writeln!(f)?; + + if cors_issues > 0 { + writeln!(f, "### ⚠️ CORS Misconfigurations")?; + writeln!(f)?; + writeln!(f, "**Severity:** 🔴 HIGH")?; + writeln!(f, "**Impact:** Data theft, unauthorized API access")?; + writeln!(f)?; + + for (sub, issues) in cors_map.iter().sorted_by_key(|(k, _)| k.as_str()) { + writeln!(f, "**Host:** `{}`", sub)?; + for issue in issues { + writeln!(f, "- ⚠️ {}", issue)?; + } + writeln!(f)?; + } + + writeln!(f, "**Recommended Fix:**")?; + writeln!(f, "```nginx")?; + writeln!(f, "# Remove wildcard CORS")?; + writeln!(f, "add_header Access-Control-Allow-Origin \"https://yourdomain.com\" always;")?; + writeln!(f, "# OR remove credentials if wildcard needed")?; + writeln!(f, "# remove_header Access-Control-Allow-Credentials;")?; + writeln!(f, "```")?; + writeln!(f)?; + } + + if takeover_risks > 0 { + writeln!(f, "### 🎯 Subdomain Takeover Risks")?; + writeln!(f)?; + for (sub, risks) in takeover_map.iter().sorted_by_key(|(k, _)| k.as_str()) { + writeln!(f, "**Subdomain:** `{}`", sub)?; + for risk in risks { + writeln!(f, "- 🎯 {}", risk)?; + } + writeln!(f)?; + } + } + } + + // All Discovered Subdomains + writeln!(f, "## 🌐 Discovered Subdomains")?; + writeln!(f)?; + writeln!(f, "| # | Subdomain | Status | Server | Open Ports | Issues |")?; + writeln!(f, "|---|-----------|--------|--------|------------|--------|")?; + + for (idx, sub) in subs.iter().sorted().enumerate() { + let (status, server) = header_map.get(sub).cloned().unwrap_or((0, None)); + let status_icon = match status { + 200..=299 => "✅", + 300..=399 => "🔄", + 400..=499 => "⚠️", + 500..=599 => "🔴", + _ => "❓", + }; + + let ports = open_ports_map.get(sub) + .map(|v| v.iter().map(|p| p.to_string()).join(", ")) + .unwrap_or_else(|| "-".to_string()); + + let issues_count = cors_map.get(sub).map(|v| v.len()).unwrap_or(0) + + takeover_map.get(sub).map(|v| v.len()).unwrap_or(0); + + let issues_display = if issues_count > 0 { + format!("🚨 {}", issues_count) + } else { + "✅".to_string() + }; + + writeln!( + f, + "| {} | `{}` | {} {} | {} | {} | {} |", + idx + 1, + sub, + status_icon, + status, + server.unwrap_or_else(|| "-".to_string()), + ports, + issues_display + )?; + } + writeln!(f)?; + + // Port Analysis + writeln!(f, "## 🔌 Port Analysis")?; + writeln!(f)?; + let mut all_ports: HashSet = HashSet::new(); + for ports in open_ports_map.values() { + all_ports.extend(ports); + } + + writeln!(f, "**Unique Open Ports:** {}", all_ports.iter().sorted().map(|p| p.to_string()).join(", "))?; + writeln!(f)?; + + for port in all_ports.iter().sorted() { + let hosts_with_port: Vec<&String> = open_ports_map + .iter() + .filter(|(_, ports)| ports.contains(port)) + .map(|(host, _)| host) + .sorted() + .collect(); + + let service = match *port { + 22 => "SSH", + 80 => "HTTP", + 443 => "HTTPS", + 3306 => "MySQL", + 5432 => "PostgreSQL", + 6379 => "Redis", + 8080 => "HTTP Alt", + 8443 => "HTTPS Alt", + _ => "Unknown", + }; + + writeln!(f, "### Port {} ({})", port, service)?; + writeln!(f, "**Hosts:** {} hosts", hosts_with_port.len())?; + for host in hosts_with_port { + writeln!(f, "- `{}`", host)?; + } + writeln!(f)?; + } + + // Software Fingerprints + if !software_map.is_empty() { + writeln!(f, "## 🔍 Software Fingerprints")?; + writeln!(f)?; + + for (sub, fingerprints) in software_map.iter().sorted_by_key(|(k, _)| k.as_str()) { + if !fingerprints.is_empty() { + writeln!(f, "**{}**", sub)?; + for (key, value) in fingerprints { + writeln!(f, "- **{}:** `{}`", key, value)?; + } + writeln!(f)?; + } + } + } + + // Cloud Assets + if !cloud_asset_map.is_empty() { + writeln!(f, "## ☁️ Cloud Infrastructure Analysis")?; + writeln!(f)?; + writeln!(f, "**Total Predicted Assets:** {}", cloud_asset_map.values().map(|v| v.len()).sum::())?; + writeln!(f)?; + + let mut provider_counts: HashMap = HashMap::new(); + for assets in cloud_asset_map.values() { + for asset in assets { + *provider_counts.entry(asset.provider.clone()).or_insert(0) += 1; + } + } + + writeln!(f, "### Provider Breakdown")?; + writeln!(f)?; + for (provider, count) in provider_counts.iter().sorted_by_key(|(k, _)| k.as_str()) { + writeln!(f, "- **{}:** {} assets", provider, count)?; + } + writeln!(f)?; + } + + // Social Intelligence + if let Some(intel) = social_intel { + writeln!(f, "## 🤖 AI-Powered Threat Intelligence")?; + writeln!(f)?; + writeln!(f, "**Framework:** {}", intel.framework_name)?; + writeln!(f, "**Version:** {}", intel.framework_version)?; + writeln!(f)?; + + writeln!(f, "### Metrics")?; + writeln!(f, "- **Total Signals:** {}", intel.metrics.total_signals)?; + writeln!(f, "- **Correlated Assets:** {}", intel.metrics.correlated_assets)?; + writeln!(f, "- **Average Confidence:** {:.0}%", intel.metrics.average_confidence * 100.0)?; + writeln!(f)?; + + if !intel.signals.is_empty() { + writeln!(f, "### Detected Threats")?; + writeln!(f)?; + for signal in &intel.signals { + writeln!(f, "#### {} - {}", signal.signal_id, signal.topic)?; + writeln!(f, "- **Severity:** {}", signal.severity.to_uppercase())?; + writeln!(f, "- **Confidence:** {:.0}%", signal.confidence * 100.0)?; + writeln!(f, "- **Cloud Vendors:** {}", signal.vendor_cloud.join(", "))?; + writeln!(f, "- **Services:** {}", signal.services.join(", "))?; + writeln!(f)?; + } + } + + if !intel.remediations.is_empty() { + writeln!(f, "### 🛠️ Remediation Plan")?; + writeln!(f)?; + for remediation in &intel.remediations { + writeln!(f, "#### {}", remediation.title)?; + writeln!(f, "**Severity:** {} | **Due:** {} days", remediation.severity.to_uppercase(), remediation.due_days)?; + writeln!(f)?; + writeln!(f, "**Steps:**")?; + for (i, step) in remediation.steps.iter().enumerate() { + writeln!(f, "{}. {}", i + 1, step)?; + } + writeln!(f)?; + writeln!(f, "**Rollback:**")?; + for step in &remediation.rollback { + writeln!(f, "- {}", step)?; + } + writeln!(f)?; + } + } + } + + // Recommendations + writeln!(f, "## 💡 Recommendations")?; + writeln!(f)?; + writeln!(f, "### Immediate Actions (P0)")?; + if cors_issues > 0 { + writeln!(f, "1. ⚠️ **Fix CORS misconfigurations** on {} host(s)", cors_issues)?; + } + if takeover_risks > 0 { + writeln!(f, "2. 🎯 **Investigate subdomain takeover risks** on {} subdomain(s)", takeover_risks)?; + } + if open_ports_map.values().any(|ports| ports.contains(&22)) { + writeln!(f, "3. 🔐 **Review SSH exposure** - Port 22 should not be publicly accessible")?; + } + writeln!(f)?; + + writeln!(f, "### Short-term Actions (P1)")?; + writeln!(f, "1. 🔍 **Validate all predicted cloud buckets** for public access")?; + writeln!(f, "2. 🛡️ **Implement Web Application Firewall (WAF)**")?; + writeln!(f, "3. 📊 **Set up continuous monitoring** for new subdomains")?; + writeln!(f)?; + + writeln!(f, "### Long-term Actions (P2)")?; + writeln!(f, "1. 🔒 **Enable DNSSEC** for domain integrity")?; + writeln!(f, "2. 📋 **Conduct regular penetration testing**")?; + writeln!(f, "3. 🎓 **Security awareness training** for development teams")?; + writeln!(f)?; + + // Footer + writeln!(f, "---")?; + writeln!(f)?; + writeln!(f, "**Report generated by ShadowMap** • [https://shadowmap.io](https://shadowmap.io)")?; + writeln!(f, "*For questions or support, contact your security team.*")?; + + Ok(()) + } + + pub fn print_terminal_summary( + domain: &str, + subs: &HashSet, + header_map: &HashMap)>, + open_ports_map: &HashMap>, + cors_map: &HashMap>, + takeover_map: &HashMap>, + cloud_asset_map: &HashMap>, + social_intel: Option<&SocialIntelligenceSummary>, + ) { + println!(); + println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); + println!("{}", format!(" SCAN RESULTS: {}", domain).bright_white().bold()); + println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); + println!(); + + // Quick Stats + println!("{}", "📊 QUICK STATS".bright_yellow().bold()); + println!("{}", "─────────────────────────────────────────────────────────".bright_black()); + println!(" {} Subdomains discovered", subs.len().to_string().bright_green().bold()); + println!(" {} Live hosts verified", subs.len().to_string().bright_green().bold()); + + if cors_map.is_empty() { + println!(" {} CORS vulnerabilities", "0".bright_green().bold()); + } else { + println!(" {} CORS vulnerabilities {}", cors_map.len().to_string().bright_red().bold(), "⚠️"); + } + + if takeover_map.is_empty() { + println!(" {} Takeover risks", "0".bright_green().bold()); + } else { + println!(" {} Takeover risks {}", takeover_map.len().to_string().bright_yellow().bold(), "⚠️"); + } + + println!(" {} Cloud assets flagged", cloud_asset_map.len().to_string().bright_cyan().bold()); + println!(); + + // Critical Findings + if !cors_map.is_empty() || !takeover_map.is_empty() { + println!("{}", "🚨 CRITICAL FINDINGS".bright_red().bold()); + println!("{}", "─────────────────────────────────────────────────────────".bright_black()); + + for (sub, issues) in cors_map.iter().sorted_by_key(|(k, _)| k.as_str()) { + println!(" {} {}", "CORS:".bright_red().bold(), sub.bright_white()); + for issue in issues { + println!(" • {}", issue.bright_yellow()); + } + } + + for (sub, risks) in takeover_map.iter().sorted_by_key(|(k, _)| k.as_str()) { + println!(" {} {}", "TAKEOVER:".bright_yellow().bold(), sub.bright_white()); + for risk in risks { + println!(" • {}", risk); + } + } + println!(); + } + + // Top Subdomains Table + println!("{}", "🌐 DISCOVERED SUBDOMAINS".bright_cyan().bold()); + println!("{}", "─────────────────────────────────────────────────────────".bright_black()); + + let mut rows: Vec = Vec::new(); + for sub in subs.iter().sorted().take(10) { + let (status, server) = header_map.get(sub).cloned().unwrap_or((0, None)); + let status_str = if status == 0 { + "N/A".to_string() + } else { + format!("{}", status) + }; + + let ports = open_ports_map.get(sub) + .map(|v| v.iter().take(4).map(|p| p.to_string()).join(",")) + .unwrap_or_else(|| "-".to_string()); + + let cors_str = if cors_map.contains_key(sub) { + "⚠️ YES".to_string() + } else { + "✓".to_string() + }; + + let risks = if takeover_map.contains_key(sub) { + "⚠️".to_string() + } else { + "✓".to_string() + }; + + rows.push(SubdomainRow { + subdomain: sub.clone(), + status: status_str, + server: server.unwrap_or_else(|| "-".to_string()), + ports, + cors: cors_str, + risks, + }); + } + + if !rows.is_empty() { + let table = Table::new(&rows).with(Style::rounded()).to_string(); + println!("{}", table); + } + + if subs.len() > 10 { + println!(); + println!(" {} more subdomain(s) not shown...", (subs.len() - 10).to_string().bright_black()); + } + println!(); + + // Social Intelligence Summary + if let Some(intel) = social_intel { + if intel.metrics.total_signals > 0 { + println!("{}", "🤖 AI THREAT INTELLIGENCE".bright_magenta().bold()); + println!("{}", "─────────────────────────────────────────────────────────".bright_black()); + println!(" {} signal(s) detected", intel.metrics.total_signals.to_string().bright_yellow().bold()); + println!(" {} confidence", format!("{:.0}%", intel.metrics.average_confidence * 100.0).bright_green().bold()); + + for signal in &intel.signals { + println!(" • {} - {}", signal.signal_id.bright_cyan(), signal.topic); + println!(" Severity: {} | Confidence: {:.0}%", + signal.severity.to_uppercase().bright_red(), + signal.confidence * 100.0 + ); + } + println!(); + } + } + + println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); + println!(); + } +} diff --git a/src/reporting.rs b/src/reporting.rs index f6d0707..1a4e249 100644 --- a/src/reporting.rs +++ b/src/reporting.rs @@ -7,6 +7,7 @@ use itertools::Itertools; use crate::cloud::CloudAssetFinding; use crate::social::SocialIntelligenceSummary; +use crate::pretty_report::PrettyReportGenerator; pub struct ReconMaps<'a> { pub header_map: &'a HashMap)>, @@ -188,6 +189,33 @@ pub fn write_outputs( serde_json::to_string_pretty(&maps.cloud_asset_map)?, )?; + // Generate beautiful markdown report + PrettyReportGenerator::generate_markdown_report( + domain, + subs, + maps.header_map, + maps.open_ports_map, + maps.cors_map, + maps.software_map, + maps.takeover_map, + maps.cloud_saas_map, + maps.cloud_asset_map, + maps.social_intel, + output_dir, + )?; + + // Print terminal summary + PrettyReportGenerator::print_terminal_summary( + domain, + subs, + maps.header_map, + maps.open_ports_map, + maps.cors_map, + maps.takeover_map, + maps.cloud_asset_map, + maps.social_intel, + ); + Ok(()) } From 484e33536257de1815b6209dde55580b8d6007c2 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Fri, 19 Dec 2025 12:53:47 +0600 Subject: [PATCH 2/4] Optimized report in pretty_report.rs --- src/agent.rs | 628 ------------------------------------------- src/enumeration.rs | 79 ------ src/pretty_report.rs | 88 +++--- 3 files changed, 44 insertions(+), 751 deletions(-) delete mode 100644 src/agent.rs delete mode 100644 src/enumeration.rs diff --git a/src/agent.rs b/src/agent.rs deleted file mode 100644 index 9e25060..0000000 --- a/src/agent.rs +++ /dev/null @@ -1,628 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::env; -use std::path::Path; - -use anyhow::anyhow; -use chrono::Local; -use idna::domain_to_unicode; -use reqwest::{redirect::Policy, Client}; -use tokio::time::Duration; - -use crate::cloud::{cloud_saas_recon, deep_cloud_asset_discovery, CloudAssetFinding}; -use crate::constants::{IP_REGEX, SUBDOMAIN_REGEX}; -use crate::cors::check_cors; -use crate::dns::{check_dns_live, create_secure_resolver}; -use crate::enumeration::crtsh_enum_async; -use crate::fingerprint::fingerprint_software; -use crate::headers::check_headers_tls; -use crate::ports::scan_ports; -use crate::social::{SocialContext, SocialIntelligenceEngine, SocialIntelligenceSummary}; -use crate::takeover::check_subdomain_takeover; -use crate::Args; - -pub type BoxError = Box; - -#[derive(Clone, Debug)] -pub struct EnumerationResult { - pub discovered: Vec, - pub validated: HashSet, -} - -#[derive(Debug, Default)] -struct AgentExecutionState { - enumeration: Option, - live_subdomains: Option>, - open_ports_map: Option>>, - header_map: Option)>>, - cors_map: Option>>, - software_map: Option>>, - takeover_map: Option>>, - cloud_saas_map: Option>>, - cloud_asset_map: Option>>, - social_intel: Option, -} - -pub struct ReconEngine { - args: Args, - client: Client, - output_dir: String, - social_engine: SocialIntelligenceEngine, -} - -impl ReconEngine { - pub async fn bootstrap(args: Args) -> Result { - let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string(); - let output_dir = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("recon_results") - .join(format!("{}_{}", args.domain, timestamp)); - std::fs::create_dir_all(&output_dir)?; - - let client = Client::builder() - .timeout(Duration::from_secs(args.timeout)) - .redirect(Policy::limited(2)) - .danger_accept_invalid_certs(false) - .pool_idle_timeout(Some(Duration::from_secs(30))) - .build()?; - - let social_engine = match env::var("SHADOWMAP_SOCIAL_CONFIG") { - Ok(path) if !path.trim().is_empty() => { - SocialIntelligenceEngine::from_path(path.trim())? - } - _ => SocialIntelligenceEngine::from_embedded()?, - }; - - Ok(Self { - args, - client, - output_dir: output_dir.to_string_lossy().to_string(), - social_engine, - }) - } - - pub fn is_autonomous(&self) -> bool { - self.args.autonomous - } - - pub fn log_run_banner(&self) { - eprintln!( - "[*] Starting security-enhanced recon for *.{}", - self.args.domain - ); - eprintln!("[*] Configuration:"); - eprintln!(" - Domain: {}", self.args.domain); - eprintln!(" - Concurrency: {}", self.args.concurrency); - eprintln!(" - Timeout: {}s", self.args.timeout); - eprintln!(" - Retries: {}", self.args.retries); - eprintln!(" - Output: {}", self.output_dir); - if self.is_autonomous() { - eprintln!(" - Orchestration: autonomous agent (Rig-style)"); - } - } - - pub fn domain(&self) -> &str { - &self.args.domain - } - - pub fn output_dir(&self) -> &str { - &self.output_dir - } - - pub fn request_timeout(&self) -> Duration { - Duration::from_secs(self.args.timeout) - } - - pub fn concurrency(&self) -> usize { - self.args.concurrency - } - - pub fn retries(&self) -> usize { - self.args.retries.max(1) - } - - pub async fn enumerate_subdomains(&self) -> Result { - let raw_subdomains = - crtsh_enum_async(&self.client, &self.args.domain, self.args.retries).await?; - - let mut discovered: Vec = raw_subdomains.iter().cloned().collect(); - discovered.sort(); - - let validated: HashSet = discovered - .iter() - .filter_map(|raw| { - let s = raw.replace("*.", "").replace("www.", ""); - let (decoded, result) = domain_to_unicode(&s); - if result.is_err() { - return None; - } - let s_lower = decoded.to_lowercase(); - - if IP_REGEX.is_match(&s_lower) || !SUBDOMAIN_REGEX.is_match(&s_lower) { - return None; - } - - if s_lower.ends_with(&format!(".{}", self.args.domain)) - || s_lower == self.args.domain - { - Some(s_lower) - } else { - None - } - }) - .collect(); - - Ok(EnumerationResult { - discovered, - validated, - }) - } - - pub async fn resolve_live_subdomains( - &self, - validated: &HashSet, - ) -> Result, BoxError> { - let resolver = create_secure_resolver().await?; - Ok(check_dns_live(validated, resolver, self.args.concurrency).await) - } - - pub async fn scan_open_ports(&self, subs: &HashSet) -> HashMap> { - scan_ports(subs, self.args.concurrency).await - } - - pub async fn inspect_headers( - &self, - subs: &HashSet, - ) -> HashMap)> { - check_headers_tls(&self.client, subs, self.args.concurrency, self.args.timeout).await - } - - pub async fn inspect_cors(&self, subs: &HashSet) -> HashMap> { - check_cors(&self.client, subs, self.args.concurrency, self.args.timeout).await - } - - pub async fn fingerprint_software( - &self, - subs: &HashSet, - ) -> HashMap> { - fingerprint_software(&self.client, subs, self.args.concurrency, self.args.timeout).await - } - - pub async fn discover_cloud_saas( - &self, - subs: &HashSet, - ) -> Result>, BoxError> { - let resolver_for_cloud = create_secure_resolver().await?; - Ok(cloud_saas_recon(subs, resolver_for_cloud, self.args.concurrency).await) - } - - pub async fn discover_cloud_assets( - &self, - subs: &HashSet, - ) -> HashMap> { - deep_cloud_asset_discovery( - subs, - &self.client, - self.args.concurrency, - self.request_timeout(), - ) - .await - } - - pub async fn detect_takeovers(&self, subs: &HashSet) -> HashMap> { - check_subdomain_takeover(subs).await - } - - fn synthesize_social_from_state( - &self, - state: &AgentExecutionState, - ) -> Result { - let live = state - .live_subdomains - .as_ref() - .ok_or_else(|| missing_step_error("live subdomains missing"))?; - - let empty_ports: HashMap> = HashMap::new(); - let empty_cors: HashMap> = HashMap::new(); - let empty_takeover: HashMap> = HashMap::new(); - let empty_saas: HashMap> = HashMap::new(); - let empty_assets: HashMap> = HashMap::new(); - - let open_ports = state.open_ports_map.as_ref().unwrap_or(&empty_ports); - let cors = state.cors_map.as_ref().unwrap_or(&empty_cors); - let takeover = state.takeover_map.as_ref().unwrap_or(&empty_takeover); - let saas = state.cloud_saas_map.as_ref().unwrap_or(&empty_saas); - let assets = state.cloud_asset_map.as_ref().unwrap_or(&empty_assets); - - Ok(self.analyze_social_from_parts(live, open_ports, cors, takeover, saas, assets)) - } - - fn analyze_social_from_parts( - &self, - live: &HashSet, - open_ports: &HashMap>, - cors: &HashMap>, - takeover: &HashMap>, - saas: &HashMap>, - assets: &HashMap>, - ) -> SocialIntelligenceSummary { - let context = self.build_social_context(live, open_ports, cors, takeover, saas, assets); - self.social_engine.analyze(&context) - } - - fn build_social_context<'a>( - &'a self, - live: &'a HashSet, - open_ports: &'a HashMap>, - cors: &'a HashMap>, - takeover: &'a HashMap>, - saas: &'a HashMap>, - assets: &'a HashMap>, - ) -> SocialContext<'a> { - SocialContext { - domain: self.domain(), - live_subdomains: live, - open_ports, - cors_issues: cors, - takeover_risks: takeover, - cloud_saas: saas, - cloud_assets: assets, - } - } - - pub async fn execute_full_scan(self) -> Result { - let enumeration = self.enumerate_subdomains().await?; - eprintln!( - "[+] crt.sh found {} potential subdomains", - enumeration.discovered.len() - ); - eprintln!("[+] Validated {} subdomains", enumeration.validated.len()); - - let live_subs = self.resolve_live_subdomains(&enumeration.validated).await?; - eprintln!("[+] {} live subdomains detected", live_subs.len()); - - let open_ports_map = self.scan_open_ports(&live_subs).await; - eprintln!( - "[+] Port scan complete - found {} subdomains with open ports", - open_ports_map.len() - ); - - let header_map = self.inspect_headers(&live_subs).await; - eprintln!("[+] Header/TLS check complete"); - - let cors_map = self.inspect_cors(&live_subs).await; - eprintln!( - "[+] CORS check complete - found {} potential issues", - cors_map.len() - ); - - let software_map = self.fingerprint_software(&live_subs).await; - eprintln!("[+] Software fingerprinting complete"); - - let cloud_saas_map = self.discover_cloud_saas(&live_subs).await?; - eprintln!( - "[+] Cloud/SaaS reconnaissance complete - found {} subdomains with SaaS patterns or predictions", - cloud_saas_map.len() - ); - - let cloud_asset_map = self.discover_cloud_assets(&live_subs).await; - eprintln!( - "[+] Deep cloud asset discovery complete - flagged {} subdomains", - cloud_asset_map.len() - ); - - let takeover_map = self.detect_takeovers(&live_subs).await; - eprintln!( - "[+] Takeover check complete - found {} potential targets (including cloud)", - takeover_map.len() - ); - - let social_intel = self.analyze_social_from_parts( - &live_subs, - &open_ports_map, - &cors_map, - &takeover_map, - &cloud_saas_map, - &cloud_asset_map, - ); - - Ok(ReconReport { - domain: self.args.domain, - output_dir: self.output_dir, - discovered_subdomains: enumeration.discovered, - validated_subdomains: enumeration.validated, - live_subdomains: live_subs, - open_ports_map, - header_map, - cors_map, - software_map, - takeover_map, - cloud_saas_map, - cloud_asset_map, - social_intel: Some(social_intel), - }) - } -} - -pub struct AutonomousReconAgent { - engine: ReconEngine, - step_retries: usize, -} - -impl AutonomousReconAgent { - pub fn new(engine: ReconEngine) -> Self { - let step_retries = engine.retries(); - Self { - engine, - step_retries, - } - } - - pub async fn execute(self) -> Result { - let AutonomousReconAgent { - engine, - step_retries, - } = self; - - let mut state = AgentExecutionState::default(); - for step in plan(step_retries) { - eprintln!("[agent] ➡️ {}", step.name); - - let mut attempt = 0usize; - loop { - attempt += 1; - let step_result = match step.kind { - StepKind::Enumerate => match engine.enumerate_subdomains().await { - Ok(enumeration) => { - eprintln!( - "[agent] discovered {} candidates, {} survived validation", - enumeration.discovered.len(), - enumeration.validated.len() - ); - state.enumeration = Some(enumeration); - Ok(()) - } - Err(err) => Err(err), - }, - StepKind::Resolve => { - if let Some(enumeration) = state.enumeration.as_ref() { - match engine.resolve_live_subdomains(&enumeration.validated).await { - Ok(live) => { - eprintln!( - "[agent] {} live subdomains after DNS validation", - live.len() - ); - state.live_subdomains = Some(live); - Ok(()) - } - Err(err) => Err(err), - } - } else { - Err(missing_step_error("enumeration step missing")) - } - } - StepKind::Ports => { - if let Some(live) = state.live_subdomains.as_ref() { - let ports = engine.scan_open_ports(live).await; - eprintln!("[agent] {} subdomains expose open ports", ports.len()); - state.open_ports_map = Some(ports); - Ok(()) - } else { - Err(missing_step_error("live subdomains missing")) - } - } - StepKind::Headers => { - if let Some(live) = state.live_subdomains.as_ref() { - let headers = engine.inspect_headers(live).await; - state.header_map = Some(headers); - Ok(()) - } else { - Err(missing_step_error("live subdomains missing")) - } - } - StepKind::Cors => { - if let Some(live) = state.live_subdomains.as_ref() { - let cors = engine.inspect_cors(live).await; - eprintln!("[agent] CORS anomalies flagged on {} hosts", cors.len()); - state.cors_map = Some(cors); - Ok(()) - } else { - Err(missing_step_error("live subdomains missing")) - } - } - StepKind::Fingerprint => { - if let Some(live) = state.live_subdomains.as_ref() { - let fingerprints = engine.fingerprint_software(live).await; - state.software_map = Some(fingerprints); - Ok(()) - } else { - Err(missing_step_error("live subdomains missing")) - } - } - StepKind::CloudSaas => { - if let Some(live) = state.live_subdomains.as_ref() { - match engine.discover_cloud_saas(live).await { - Ok(saas) => { - eprintln!( - "[agent] {} SaaS indicators identified", - saas.len() - ); - state.cloud_saas_map = Some(saas); - Ok(()) - } - Err(err) => Err(err), - } - } else { - Err(missing_step_error("live subdomains missing")) - } - } - StepKind::CloudAssets => { - if let Some(live) = state.live_subdomains.as_ref() { - let assets = engine.discover_cloud_assets(live).await; - eprintln!("[agent] {} cloud assets worth review", assets.len()); - state.cloud_asset_map = Some(assets); - Ok(()) - } else { - Err(missing_step_error("live subdomains missing")) - } - } - StepKind::Takeover => { - if let Some(live) = state.live_subdomains.as_ref() { - let takeover = engine.detect_takeovers(live).await; - eprintln!("[agent] {} takeover candidates queued", takeover.len()); - state.takeover_map = Some(takeover); - Ok(()) - } else { - Err(missing_step_error("live subdomains missing")) - } - } - StepKind::SocialIntel => match engine.synthesize_social_from_state(&state) { - Ok(summary) => { - eprintln!( - "[agent] social stream surfaced {} signals (avg confidence {:.0}%)", - summary.metrics.total_signals, - summary.metrics.average_confidence - ); - state.social_intel = Some(summary); - Ok(()) - } - Err(err) => Err(err), - }, - }; - - match step_result { - Ok(_) => { - if attempt > 1 { - eprintln!("[agent] recovered after {} attempts", attempt); - } - break; - } - Err(err) => { - if attempt >= step.max_attempts { - return Err(err); - } - eprintln!( - "[agent] attempt {} failed ({}), retrying...", - attempt, err - ); - } - } - } - } - - let enumeration = state - .enumeration - .ok_or_else(|| missing_step_error("enumeration missing"))?; - let live_subdomains = state - .live_subdomains - .ok_or_else(|| missing_step_error("live subdomains missing"))?; - - let ReconEngine { - args, - client: _, - output_dir, - .. - } = engine; - let Args { domain, .. } = args; - - Ok(ReconReport { - domain, - output_dir, - discovered_subdomains: enumeration.discovered, - validated_subdomains: enumeration.validated, - live_subdomains, - open_ports_map: state.open_ports_map.unwrap_or_default(), - header_map: state.header_map.unwrap_or_default(), - cors_map: state.cors_map.unwrap_or_default(), - software_map: state.software_map.unwrap_or_default(), - takeover_map: state.takeover_map.unwrap_or_default(), - cloud_saas_map: state.cloud_saas_map.unwrap_or_default(), - cloud_asset_map: state.cloud_asset_map.unwrap_or_default(), - social_intel: state.social_intel, - }) - } -} - -fn plan(step_retries: usize) -> Vec { - let max_attempts = step_retries.max(1); - vec![ - AgentStep::new( - "Enumerate subdomains via CRT.sh", - StepKind::Enumerate, - max_attempts, - ), - AgentStep::new("Validate DNS responses", StepKind::Resolve, max_attempts), - AgentStep::new("Port scan live assets", StepKind::Ports, max_attempts), - AgentStep::new("Capture headers & TLS", StepKind::Headers, max_attempts), - AgentStep::new("Analyse CORS controls", StepKind::Cors, max_attempts), - AgentStep::new( - "Fingerprint running software", - StepKind::Fingerprint, - max_attempts, - ), - AgentStep::new( - "Predict SaaS/cloud usage", - StepKind::CloudSaas, - max_attempts, - ), - AgentStep::new( - "Discover deep cloud assets", - StepKind::CloudAssets, - max_attempts, - ), - AgentStep::new("Assess takeover exposure", StepKind::Takeover, max_attempts), - AgentStep::new( - "Synthesize social intelligence", - StepKind::SocialIntel, - max_attempts, - ), - ] -} - -struct AgentStep { - name: &'static str, - kind: StepKind, - max_attempts: usize, -} - -impl AgentStep { - fn new(name: &'static str, kind: StepKind, max_attempts: usize) -> Self { - Self { - name, - kind, - max_attempts, - } - } -} - -#[derive(Copy, Clone)] -enum StepKind { - Enumerate, - Resolve, - Ports, - Headers, - Cors, - Fingerprint, - CloudSaas, - CloudAssets, - Takeover, - SocialIntel, -} - -pub struct ReconReport { - pub domain: String, - pub output_dir: String, - pub discovered_subdomains: Vec, - pub validated_subdomains: HashSet, - pub live_subdomains: HashSet, - pub open_ports_map: HashMap>, - pub header_map: HashMap)>, - pub cors_map: HashMap>, - pub software_map: HashMap>, - pub takeover_map: HashMap>, - pub cloud_saas_map: HashMap>, - pub cloud_asset_map: HashMap>, - pub social_intel: Option, -} - -fn missing_step_error(message: &str) -> BoxError { - anyhow!(message.to_string()).into() -} diff --git a/src/enumeration.rs b/src/enumeration.rs deleted file mode 100644 index 881024f..0000000 --- a/src/enumeration.rs +++ /dev/null @@ -1,79 +0,0 @@ -use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng}; -use reqwest::{header, Client}; -use serde::Deserialize; -use std::collections::HashSet; -use std::time::Duration; -use tokio::time::sleep; - -use crate::constants::USER_AGENTS; - -#[derive(Debug, Deserialize)] -struct CrtShEntry { - name_value: String, -} - -pub async fn crtsh_enum_async( - client: &Client, - domain: &str, - max_retries: usize, -) -> Result, Box> { - let url = format!("https://crt.sh/?q=%25.{}&output=json", domain); - let mut retries = 0; - let mut last_error: Option> = None; - - let mut rng = StdRng::from_entropy(); - - while retries < max_retries { - let resp = client - .get(&url) - .header(header::USER_AGENT, *USER_AGENTS.choose(&mut rng).unwrap()) - .header(header::ACCEPT, "application/json") - .send() - .await; - - match resp { - Ok(r) => { - if r.status().is_success() { - let entries: Result, _> = r.json().await; - match entries { - Ok(entries) => { - let mut subs = HashSet::new(); - for entry in entries { - for name in entry.name_value.split('\n') { - let trimmed = name.trim(); - if !trimmed.is_empty() && !trimmed.starts_with('*') { - subs.insert(trimmed.to_string()); - } - } - } - return Ok(subs); - } - Err(e) => { - last_error = Some(Box::new(e)); - } - } - } else if r.status().is_server_error() { - last_error = Some(Box::new(std::io::Error::other(format!( - "Server error: {}", - r.status() - )))); - } - } - Err(e) => { - last_error = Some(Box::new(e)); - } - } - - retries += 1; - if retries < max_retries { - let delay = Duration::from_secs(2_u64.pow(retries as u32)); - eprintln!( - "[!] Retry {}/{} due to error: {:?}", - retries, max_retries, last_error - ); - sleep(delay).await; - } - } - - Err(last_error.unwrap_or_else(|| Box::new(std::io::Error::other("Max retries exceeded")))) -} diff --git a/src/pretty_report.rs b/src/pretty_report.rs index 982b9e2..9e280f5 100644 --- a/src/pretty_report.rs +++ b/src/pretty_report.rs @@ -48,7 +48,7 @@ impl PrettyReportGenerator { let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S %Z"); // Header - writeln!(f, "# 🔍 ShadowMap Security Report")?; + writeln!(f, "# ShadowMap Security Report")?; writeln!(f)?; writeln!(f, "**Target Domain:** `{}`", domain)?; writeln!(f, "**Scan Date:** {}", timestamp)?; @@ -58,7 +58,7 @@ impl PrettyReportGenerator { writeln!(f)?; // Executive Summary - writeln!(f, "## 📊 Executive Summary")?; + writeln!(f, "## Executive Summary")?; writeln!(f)?; let total_subs = subs.len(); @@ -67,11 +67,11 @@ impl PrettyReportGenerator { let cloud_assets = cloud_asset_map.len(); let risk_level = if cors_issues > 0 || takeover_risks > 0 { - "🔴 **HIGH**" + "**HIGH**" } else if cloud_assets > 5 { - "🟡 **MEDIUM**" + "**MEDIUM**" } else { - "🟢 **LOW**" + "**LOW**" }; writeln!(f, "| Metric | Value |")?; @@ -86,20 +86,20 @@ impl PrettyReportGenerator { // Critical Findings if cors_issues > 0 || takeover_risks > 0 { - writeln!(f, "## 🚨 Critical Security Findings")?; + writeln!(f, "## Critical Security Findings")?; writeln!(f)?; if cors_issues > 0 { - writeln!(f, "### ⚠️ CORS Misconfigurations")?; + writeln!(f, "### CORS Misconfigurations")?; writeln!(f)?; - writeln!(f, "**Severity:** 🔴 HIGH")?; + writeln!(f, "**Severity:** HIGH")?; writeln!(f, "**Impact:** Data theft, unauthorized API access")?; writeln!(f)?; for (sub, issues) in cors_map.iter().sorted_by_key(|(k, _)| k.as_str()) { writeln!(f, "**Host:** `{}`", sub)?; for issue in issues { - writeln!(f, "- ⚠️ {}", issue)?; + writeln!(f, "- {}", issue)?; } writeln!(f)?; } @@ -115,12 +115,12 @@ impl PrettyReportGenerator { } if takeover_risks > 0 { - writeln!(f, "### 🎯 Subdomain Takeover Risks")?; + writeln!(f, "### Subdomain Takeover Risks")?; writeln!(f)?; for (sub, risks) in takeover_map.iter().sorted_by_key(|(k, _)| k.as_str()) { writeln!(f, "**Subdomain:** `{}`", sub)?; for risk in risks { - writeln!(f, "- 🎯 {}", risk)?; + writeln!(f, "- {}", risk)?; } writeln!(f)?; } @@ -128,7 +128,7 @@ impl PrettyReportGenerator { } // All Discovered Subdomains - writeln!(f, "## 🌐 Discovered Subdomains")?; + writeln!(f, "## Discovered Subdomains")?; writeln!(f)?; writeln!(f, "| # | Subdomain | Status | Server | Open Ports | Issues |")?; writeln!(f, "|---|-----------|--------|--------|------------|--------|")?; @@ -136,11 +136,11 @@ impl PrettyReportGenerator { for (idx, sub) in subs.iter().sorted().enumerate() { let (status, server) = header_map.get(sub).cloned().unwrap_or((0, None)); let status_icon = match status { - 200..=299 => "✅", - 300..=399 => "🔄", - 400..=499 => "⚠️", - 500..=599 => "🔴", - _ => "❓", + 200..=299 => "OK", + 300..=399 => "REDIRECT", + 400..=499 => "CLIENT_ERR", + 500..=599 => "SERVER_ERR", + _ => "UNKNOWN", }; let ports = open_ports_map.get(sub) @@ -151,9 +151,9 @@ impl PrettyReportGenerator { + takeover_map.get(sub).map(|v| v.len()).unwrap_or(0); let issues_display = if issues_count > 0 { - format!("🚨 {}", issues_count) + format!("ISSUES: {}", issues_count) } else { - "✅".to_string() + "OK".to_string() }; writeln!( @@ -171,7 +171,7 @@ impl PrettyReportGenerator { writeln!(f)?; // Port Analysis - writeln!(f, "## 🔌 Port Analysis")?; + writeln!(f, "## Port Analysis")?; writeln!(f)?; let mut all_ports: HashSet = HashSet::new(); for ports in open_ports_map.values() { @@ -211,7 +211,7 @@ impl PrettyReportGenerator { // Software Fingerprints if !software_map.is_empty() { - writeln!(f, "## 🔍 Software Fingerprints")?; + writeln!(f, "## Software Fingerprints")?; writeln!(f)?; for (sub, fingerprints) in software_map.iter().sorted_by_key(|(k, _)| k.as_str()) { @@ -227,7 +227,7 @@ impl PrettyReportGenerator { // Cloud Assets if !cloud_asset_map.is_empty() { - writeln!(f, "## ☁️ Cloud Infrastructure Analysis")?; + writeln!(f, "## Cloud Infrastructure Analysis")?; writeln!(f)?; writeln!(f, "**Total Predicted Assets:** {}", cloud_asset_map.values().map(|v| v.len()).sum::())?; writeln!(f)?; @@ -249,7 +249,7 @@ impl PrettyReportGenerator { // Social Intelligence if let Some(intel) = social_intel { - writeln!(f, "## 🤖 AI-Powered Threat Intelligence")?; + writeln!(f, "## AI-Powered Threat Intelligence")?; writeln!(f)?; writeln!(f, "**Framework:** {}", intel.framework_name)?; writeln!(f, "**Version:** {}", intel.framework_version)?; @@ -275,7 +275,7 @@ impl PrettyReportGenerator { } if !intel.remediations.is_empty() { - writeln!(f, "### 🛠️ Remediation Plan")?; + writeln!(f, "### Remediation Plan")?; writeln!(f)?; for remediation in &intel.remediations { writeln!(f, "#### {}", remediation.title)?; @@ -296,30 +296,30 @@ impl PrettyReportGenerator { } // Recommendations - writeln!(f, "## 💡 Recommendations")?; + writeln!(f, "## Recommendations")?; writeln!(f)?; writeln!(f, "### Immediate Actions (P0)")?; if cors_issues > 0 { - writeln!(f, "1. ⚠️ **Fix CORS misconfigurations** on {} host(s)", cors_issues)?; + writeln!(f, "1. **Fix CORS misconfigurations** on {} host(s)", cors_issues)?; } if takeover_risks > 0 { - writeln!(f, "2. 🎯 **Investigate subdomain takeover risks** on {} subdomain(s)", takeover_risks)?; + writeln!(f, "2. **Investigate subdomain takeover risks** on {} subdomain(s)", takeover_risks)?; } if open_ports_map.values().any(|ports| ports.contains(&22)) { - writeln!(f, "3. 🔐 **Review SSH exposure** - Port 22 should not be publicly accessible")?; + writeln!(f, "3. **Review SSH exposure** - Port 22 should not be publicly accessible")?; } writeln!(f)?; writeln!(f, "### Short-term Actions (P1)")?; - writeln!(f, "1. 🔍 **Validate all predicted cloud buckets** for public access")?; - writeln!(f, "2. 🛡️ **Implement Web Application Firewall (WAF)**")?; - writeln!(f, "3. 📊 **Set up continuous monitoring** for new subdomains")?; + writeln!(f, "1. **Validate all predicted cloud buckets** for public access")?; + writeln!(f, "2. **Implement Web Application Firewall (WAF)**")?; + writeln!(f, "3. **Set up continuous monitoring** for new subdomains")?; writeln!(f)?; writeln!(f, "### Long-term Actions (P2)")?; - writeln!(f, "1. 🔒 **Enable DNSSEC** for domain integrity")?; - writeln!(f, "2. 📋 **Conduct regular penetration testing**")?; - writeln!(f, "3. 🎓 **Security awareness training** for development teams")?; + writeln!(f, "1. **Enable DNSSEC** for domain integrity")?; + writeln!(f, "2. **Conduct regular penetration testing**")?; + writeln!(f, "3. **Security awareness training** for development teams")?; writeln!(f)?; // Footer @@ -348,7 +348,7 @@ impl PrettyReportGenerator { println!(); // Quick Stats - println!("{}", "📊 QUICK STATS".bright_yellow().bold()); + println!("{}", "QUICK STATS".bright_yellow().bold()); println!("{}", "─────────────────────────────────────────────────────────".bright_black()); println!(" {} Subdomains discovered", subs.len().to_string().bright_green().bold()); println!(" {} Live hosts verified", subs.len().to_string().bright_green().bold()); @@ -356,13 +356,13 @@ impl PrettyReportGenerator { if cors_map.is_empty() { println!(" {} CORS vulnerabilities", "0".bright_green().bold()); } else { - println!(" {} CORS vulnerabilities {}", cors_map.len().to_string().bright_red().bold(), "⚠️"); + println!(" {} CORS vulnerabilities [WARNING]", cors_map.len().to_string().bright_red().bold()); } if takeover_map.is_empty() { println!(" {} Takeover risks", "0".bright_green().bold()); } else { - println!(" {} Takeover risks {}", takeover_map.len().to_string().bright_yellow().bold(), "⚠️"); + println!(" {} Takeover risks [WARNING]", takeover_map.len().to_string().bright_yellow().bold()); } println!(" {} Cloud assets flagged", cloud_asset_map.len().to_string().bright_cyan().bold()); @@ -370,7 +370,7 @@ impl PrettyReportGenerator { // Critical Findings if !cors_map.is_empty() || !takeover_map.is_empty() { - println!("{}", "🚨 CRITICAL FINDINGS".bright_red().bold()); + println!("{}", "CRITICAL FINDINGS".bright_red().bold()); println!("{}", "─────────────────────────────────────────────────────────".bright_black()); for (sub, issues) in cors_map.iter().sorted_by_key(|(k, _)| k.as_str()) { @@ -390,7 +390,7 @@ impl PrettyReportGenerator { } // Top Subdomains Table - println!("{}", "🌐 DISCOVERED SUBDOMAINS".bright_cyan().bold()); + println!("{}", "DISCOVERED SUBDOMAINS".bright_cyan().bold()); println!("{}", "─────────────────────────────────────────────────────────".bright_black()); let mut rows: Vec = Vec::new(); @@ -407,15 +407,15 @@ impl PrettyReportGenerator { .unwrap_or_else(|| "-".to_string()); let cors_str = if cors_map.contains_key(sub) { - "⚠️ YES".to_string() + "WARNING".to_string() } else { - "✓".to_string() + "OK".to_string() }; let risks = if takeover_map.contains_key(sub) { - "⚠️".to_string() + "WARNING".to_string() } else { - "✓".to_string() + "OK".to_string() }; rows.push(SubdomainRow { @@ -442,7 +442,7 @@ impl PrettyReportGenerator { // Social Intelligence Summary if let Some(intel) = social_intel { if intel.metrics.total_signals > 0 { - println!("{}", "🤖 AI THREAT INTELLIGENCE".bright_magenta().bold()); + println!("{}", "AI THREAT INTELLIGENCE".bright_magenta().bold()); println!("{}", "─────────────────────────────────────────────────────────".bright_black()); println!(" {} signal(s) detected", intel.metrics.total_signals.to_string().bright_yellow().bold()); println!(" {} confidence", format!("{:.0}%", intel.metrics.average_confidence * 100.0).bright_green().bold()); From ac3be71147154cd7ed7903d366428d8b6177a914 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sat, 20 Dec 2025 13:40:46 +0600 Subject: [PATCH 3/4] Added missing agent.rs & enumeration.rs file, fixed clippy err --- profile.json.gz | Bin 0 -> 1373 bytes .../chaldal.com_REPORT.md | 271 ++++ .../chaldal.com_cloud_assets.json | 926 ++++++++++++ .../chaldal.com_cloud_saas.json | 86 ++ .../chaldal.com_report.csv | 22 + .../chaldal.com_report.json | 1304 +++++++++++++++++ .../chaldal.com_security_findings.txt | 7 + .../chaldal.com_social_intelligence.json | 179 +++ .../chaldal.com_subdomains.txt | 21 + .../chaldal.com_REPORT.md | 265 ++++ .../chaldal.com_cloud_assets.json | 926 ++++++++++++ .../chaldal.com_cloud_saas.json | 86 ++ .../chaldal.com_report.csv | 22 + .../chaldal.com_report.json | 1298 ++++++++++++++++ .../chaldal.com_security_findings.txt | 7 + .../chaldal.com_social_intelligence.json | 179 +++ .../chaldal.com_subdomains.txt | 21 + src/agent.rs | 628 ++++++++ src/enumeration.rs | 79 + 19 files changed, 6327 insertions(+) create mode 100644 profile.json.gz create mode 100644 recon_results/chaldal.com_20251220_124726/chaldal.com_REPORT.md create mode 100644 recon_results/chaldal.com_20251220_124726/chaldal.com_cloud_assets.json create mode 100644 recon_results/chaldal.com_20251220_124726/chaldal.com_cloud_saas.json create mode 100644 recon_results/chaldal.com_20251220_124726/chaldal.com_report.csv create mode 100644 recon_results/chaldal.com_20251220_124726/chaldal.com_report.json create mode 100644 recon_results/chaldal.com_20251220_124726/chaldal.com_security_findings.txt create mode 100644 recon_results/chaldal.com_20251220_124726/chaldal.com_social_intelligence.json create mode 100644 recon_results/chaldal.com_20251220_124726/chaldal.com_subdomains.txt create mode 100644 recon_results/chaldal.com_20251220_133742/chaldal.com_REPORT.md create mode 100644 recon_results/chaldal.com_20251220_133742/chaldal.com_cloud_assets.json create mode 100644 recon_results/chaldal.com_20251220_133742/chaldal.com_cloud_saas.json create mode 100644 recon_results/chaldal.com_20251220_133742/chaldal.com_report.csv create mode 100644 recon_results/chaldal.com_20251220_133742/chaldal.com_report.json create mode 100644 recon_results/chaldal.com_20251220_133742/chaldal.com_security_findings.txt create mode 100644 recon_results/chaldal.com_20251220_133742/chaldal.com_social_intelligence.json create mode 100644 recon_results/chaldal.com_20251220_133742/chaldal.com_subdomains.txt create mode 100644 src/agent.rs create mode 100644 src/enumeration.rs diff --git a/profile.json.gz b/profile.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..a8dbe400c76f031b999ff23be085dd58d07a4554 GIT binary patch literal 1373 zcmV-j1)};NiwFn+00000|8R0|W@&6?E^2dcZUD7c+iu%95dD?KJP#a_qONaF+yGf@ zo1#v39}J_Qt+6dcmIP8t>@ECR{ek_m#YnPar%qE~voPQ&;^CNccsO(T4c1f}NWQ_` z)U+&ZMh=q6Hz>@SAc5O@MHUd|WnNlHU}?quSY^1OWd+0-dQ zO0#AO$-?B05NK0V;j*%DyuO)nbpPcKNG4N+Ebad0R4nTiBm(;dPo_JBtf%cr%e0HJjAs(}R7 zW_~;VP5Ogypg0|~u5!9BvbtR*JyKDRC^uV%IzXsbmP~r}`F=z>GY$WIZ(nCEziS6O z1Xmkd-?eXK7z9cR;fWy710ezgw_VT5JY&Ms138sx9Hn1Ov1~VNitxD4Noj<}k-IJO z?QKEh+RPuiioh8~3TKZ;LS*J6|oz4fKe$u@7X&^A)WP6TL&ov)PF- z(rumsLS>kKxM-Zc$g|l+|GM4CPo0F0Uwu9gpII^wl}T?>NFccMuZHo5P|4xY3qHiL z5J4OdN0EpwW9?mvR*otC5*QFTH%MR;1z!UVM+!aO-9C@n*d^5$mU1$VE{+k$DgE%i z&+8}CPf`@4;k9=eMxMS>o_~EAc|u>0uEkaCXm~=vnj?~BxN6HKJ&{JARdL%|79{JHo#h2>C2{CVhe%$s%b9Eb!XMm8HtHJ@EIMrMCqZ_D1DRxv*3OoLo(^Q|2ut8 z5xAH?nxeig?f(?NYpA_&Ivuin_(`=-9&0Phsq2XEp%_-dr)E7P`yTt(`nzUbykEo} z_9mirMf>U@PR=YFKEsxXpQuPX1H}LInwcV0E9011KE}@`sdXg1Lp|=rnfqw6;;waE zdoOc*T3;^axsvsVzs|~Hoc-PZVxG-zij+3p@9!72W7qT2d{yj7K?Xv_+*~_H*&Cqa z74{?JRa2+sQ_*`q;pu^nRVd_P?wGS}sy#rdfs8#B1qVRq=pdzqY4RFa=gZ91WY0{o zy+nUFpt?17&n54fdw~i=jHd{z($(ih#vP+I&HM7p+;oQcglUEFMA>px_iNrm+C7~= zKr#us%F3p=waMIEr0^6QU@2mh1EpjjcsX69uH+wD*0a5y_&SJu^i{i-*7~07VU?vt z#v9ufZgEubIqgHiXIXF`d}io;Q)oTB&5GsFTDJa^z3Ua5z*h^-^yn%fHld6S0fN}b zNCoUt#KsR7AtS{`2Ga8lbf){feG{8N88+b0R_uj!5F0O81YP7vYQ~C<7M_`NWU&b& zsk_mbWc#3`>W1PO0aM#)T5A=@eAQ`+ml8XYty`5(AKPn#yJ=; + +#[derive(Clone, Debug)] +pub struct EnumerationResult { + pub discovered: Vec, + pub validated: HashSet, +} + +#[derive(Debug, Default)] +struct AgentExecutionState { + enumeration: Option, + live_subdomains: Option>, + open_ports_map: Option>>, + header_map: Option)>>, + cors_map: Option>>, + software_map: Option>>, + takeover_map: Option>>, + cloud_saas_map: Option>>, + cloud_asset_map: Option>>, + social_intel: Option, +} + +pub struct ReconEngine { + args: Args, + client: Client, + output_dir: String, + social_engine: SocialIntelligenceEngine, +} + +impl ReconEngine { + pub async fn bootstrap(args: Args) -> Result { + let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string(); + let output_dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("recon_results") + .join(format!("{}_{}", args.domain, timestamp)); + std::fs::create_dir_all(&output_dir)?; + + let client = Client::builder() + .timeout(Duration::from_secs(args.timeout)) + .redirect(Policy::limited(2)) + .danger_accept_invalid_certs(false) + .pool_idle_timeout(Some(Duration::from_secs(30))) + .build()?; + + let social_engine = match env::var("SHADOWMAP_SOCIAL_CONFIG") { + Ok(path) if !path.trim().is_empty() => { + SocialIntelligenceEngine::from_path(path.trim())? + } + _ => SocialIntelligenceEngine::from_embedded()?, + }; + + Ok(Self { + args, + client, + output_dir: output_dir.to_string_lossy().to_string(), + social_engine, + }) + } + + pub fn is_autonomous(&self) -> bool { + self.args.autonomous + } + + pub fn log_run_banner(&self) { + eprintln!( + "[*] Starting security-enhanced recon for *.{}", + self.args.domain + ); + eprintln!("[*] Configuration:"); + eprintln!(" - Domain: {}", self.args.domain); + eprintln!(" - Concurrency: {}", self.args.concurrency); + eprintln!(" - Timeout: {}s", self.args.timeout); + eprintln!(" - Retries: {}", self.args.retries); + eprintln!(" - Output: {}", self.output_dir); + if self.is_autonomous() { + eprintln!(" - Orchestration: autonomous agent (Rig-style)"); + } + } + + pub fn domain(&self) -> &str { + &self.args.domain + } + + pub fn output_dir(&self) -> &str { + &self.output_dir + } + + pub fn request_timeout(&self) -> Duration { + Duration::from_secs(self.args.timeout) + } + + pub fn concurrency(&self) -> usize { + self.args.concurrency + } + + pub fn retries(&self) -> usize { + self.args.retries.max(1) + } + + pub async fn enumerate_subdomains(&self) -> Result { + let raw_subdomains = + crtsh_enum_async(&self.client, &self.args.domain, self.args.retries).await?; + + let mut discovered: Vec = raw_subdomains.iter().cloned().collect(); + discovered.sort(); + + let validated: HashSet = discovered + .iter() + .filter_map(|raw| { + let s = raw.replace("*.", "").replace("www.", ""); + let (decoded, result) = domain_to_unicode(&s); + if result.is_err() { + return None; + } + let s_lower = decoded.to_lowercase(); + + if IP_REGEX.is_match(&s_lower) || !SUBDOMAIN_REGEX.is_match(&s_lower) { + return None; + } + + if s_lower.ends_with(&format!(".{}", self.args.domain)) + || s_lower == self.args.domain + { + Some(s_lower) + } else { + None + } + }) + .collect(); + + Ok(EnumerationResult { + discovered, + validated, + }) + } + + pub async fn resolve_live_subdomains( + &self, + validated: &HashSet, + ) -> Result, BoxError> { + let resolver = create_secure_resolver().await?; + Ok(check_dns_live(validated, resolver, self.args.concurrency).await) + } + + pub async fn scan_open_ports(&self, subs: &HashSet) -> HashMap> { + scan_ports(subs, self.args.concurrency).await + } + + pub async fn inspect_headers( + &self, + subs: &HashSet, + ) -> HashMap)> { + check_headers_tls(&self.client, subs, self.args.concurrency, self.args.timeout).await + } + + pub async fn inspect_cors(&self, subs: &HashSet) -> HashMap> { + check_cors(&self.client, subs, self.args.concurrency, self.args.timeout).await + } + + pub async fn fingerprint_software( + &self, + subs: &HashSet, + ) -> HashMap> { + fingerprint_software(&self.client, subs, self.args.concurrency, self.args.timeout).await + } + + pub async fn discover_cloud_saas( + &self, + subs: &HashSet, + ) -> Result>, BoxError> { + let resolver_for_cloud = create_secure_resolver().await?; + Ok(cloud_saas_recon(subs, resolver_for_cloud, self.args.concurrency).await) + } + + pub async fn discover_cloud_assets( + &self, + subs: &HashSet, + ) -> HashMap> { + deep_cloud_asset_discovery( + subs, + &self.client, + self.args.concurrency, + self.request_timeout(), + ) + .await + } + + pub async fn detect_takeovers(&self, subs: &HashSet) -> HashMap> { + check_subdomain_takeover(subs).await + } + + fn synthesize_social_from_state( + &self, + state: &AgentExecutionState, + ) -> Result { + let live = state + .live_subdomains + .as_ref() + .ok_or_else(|| missing_step_error("live subdomains missing"))?; + + let empty_ports: HashMap> = HashMap::new(); + let empty_cors: HashMap> = HashMap::new(); + let empty_takeover: HashMap> = HashMap::new(); + let empty_saas: HashMap> = HashMap::new(); + let empty_assets: HashMap> = HashMap::new(); + + let open_ports = state.open_ports_map.as_ref().unwrap_or(&empty_ports); + let cors = state.cors_map.as_ref().unwrap_or(&empty_cors); + let takeover = state.takeover_map.as_ref().unwrap_or(&empty_takeover); + let saas = state.cloud_saas_map.as_ref().unwrap_or(&empty_saas); + let assets = state.cloud_asset_map.as_ref().unwrap_or(&empty_assets); + + Ok(self.analyze_social_from_parts(live, open_ports, cors, takeover, saas, assets)) + } + + fn analyze_social_from_parts( + &self, + live: &HashSet, + open_ports: &HashMap>, + cors: &HashMap>, + takeover: &HashMap>, + saas: &HashMap>, + assets: &HashMap>, + ) -> SocialIntelligenceSummary { + let context = self.build_social_context(live, open_ports, cors, takeover, saas, assets); + self.social_engine.analyze(&context) + } + + fn build_social_context<'a>( + &'a self, + live: &'a HashSet, + open_ports: &'a HashMap>, + cors: &'a HashMap>, + takeover: &'a HashMap>, + saas: &'a HashMap>, + assets: &'a HashMap>, + ) -> SocialContext<'a> { + SocialContext { + domain: self.domain(), + live_subdomains: live, + open_ports, + cors_issues: cors, + takeover_risks: takeover, + cloud_saas: saas, + cloud_assets: assets, + } + } + + pub async fn execute_full_scan(self) -> Result { + let enumeration = self.enumerate_subdomains().await?; + eprintln!( + "[+] crt.sh found {} potential subdomains", + enumeration.discovered.len() + ); + eprintln!("[+] Validated {} subdomains", enumeration.validated.len()); + + let live_subs = self.resolve_live_subdomains(&enumeration.validated).await?; + eprintln!("[+] {} live subdomains detected", live_subs.len()); + + let open_ports_map = self.scan_open_ports(&live_subs).await; + eprintln!( + "[+] Port scan complete - found {} subdomains with open ports", + open_ports_map.len() + ); + + let header_map = self.inspect_headers(&live_subs).await; + eprintln!("[+] Header/TLS check complete"); + + let cors_map = self.inspect_cors(&live_subs).await; + eprintln!( + "[+] CORS check complete - found {} potential issues", + cors_map.len() + ); + + let software_map = self.fingerprint_software(&live_subs).await; + eprintln!("[+] Software fingerprinting complete"); + + let cloud_saas_map = self.discover_cloud_saas(&live_subs).await?; + eprintln!( + "[+] Cloud/SaaS reconnaissance complete - found {} subdomains with SaaS patterns or predictions", + cloud_saas_map.len() + ); + + let cloud_asset_map = self.discover_cloud_assets(&live_subs).await; + eprintln!( + "[+] Deep cloud asset discovery complete - flagged {} subdomains", + cloud_asset_map.len() + ); + + let takeover_map = self.detect_takeovers(&live_subs).await; + eprintln!( + "[+] Takeover check complete - found {} potential targets (including cloud)", + takeover_map.len() + ); + + let social_intel = self.analyze_social_from_parts( + &live_subs, + &open_ports_map, + &cors_map, + &takeover_map, + &cloud_saas_map, + &cloud_asset_map, + ); + + Ok(ReconReport { + domain: self.args.domain, + output_dir: self.output_dir, + discovered_subdomains: enumeration.discovered, + validated_subdomains: enumeration.validated, + live_subdomains: live_subs, + open_ports_map, + header_map, + cors_map, + software_map, + takeover_map, + cloud_saas_map, + cloud_asset_map, + social_intel: Some(social_intel), + }) + } +} + +pub struct AutonomousReconAgent { + engine: ReconEngine, + step_retries: usize, +} + +impl AutonomousReconAgent { + pub fn new(engine: ReconEngine) -> Self { + let step_retries = engine.retries(); + Self { + engine, + step_retries, + } + } + + pub async fn execute(self) -> Result { + let AutonomousReconAgent { + engine, + step_retries, + } = self; + + let mut state = AgentExecutionState::default(); + for step in plan(step_retries) { + eprintln!("[agent] ➡️ {}", step.name); + + let mut attempt = 0usize; + loop { + attempt += 1; + let step_result = match step.kind { + StepKind::Enumerate => match engine.enumerate_subdomains().await { + Ok(enumeration) => { + eprintln!( + "[agent] discovered {} candidates, {} survived validation", + enumeration.discovered.len(), + enumeration.validated.len() + ); + state.enumeration = Some(enumeration); + Ok(()) + } + Err(err) => Err(err), + }, + StepKind::Resolve => { + if let Some(enumeration) = state.enumeration.as_ref() { + match engine.resolve_live_subdomains(&enumeration.validated).await { + Ok(live) => { + eprintln!( + "[agent] {} live subdomains after DNS validation", + live.len() + ); + state.live_subdomains = Some(live); + Ok(()) + } + Err(err) => Err(err), + } + } else { + Err(missing_step_error("enumeration step missing")) + } + } + StepKind::Ports => { + if let Some(live) = state.live_subdomains.as_ref() { + let ports = engine.scan_open_ports(live).await; + eprintln!("[agent] {} subdomains expose open ports", ports.len()); + state.open_ports_map = Some(ports); + Ok(()) + } else { + Err(missing_step_error("live subdomains missing")) + } + } + StepKind::Headers => { + if let Some(live) = state.live_subdomains.as_ref() { + let headers = engine.inspect_headers(live).await; + state.header_map = Some(headers); + Ok(()) + } else { + Err(missing_step_error("live subdomains missing")) + } + } + StepKind::Cors => { + if let Some(live) = state.live_subdomains.as_ref() { + let cors = engine.inspect_cors(live).await; + eprintln!("[agent] CORS anomalies flagged on {} hosts", cors.len()); + state.cors_map = Some(cors); + Ok(()) + } else { + Err(missing_step_error("live subdomains missing")) + } + } + StepKind::Fingerprint => { + if let Some(live) = state.live_subdomains.as_ref() { + let fingerprints = engine.fingerprint_software(live).await; + state.software_map = Some(fingerprints); + Ok(()) + } else { + Err(missing_step_error("live subdomains missing")) + } + } + StepKind::CloudSaas => { + if let Some(live) = state.live_subdomains.as_ref() { + match engine.discover_cloud_saas(live).await { + Ok(saas) => { + eprintln!( + "[agent] {} SaaS indicators identified", + saas.len() + ); + state.cloud_saas_map = Some(saas); + Ok(()) + } + Err(err) => Err(err), + } + } else { + Err(missing_step_error("live subdomains missing")) + } + } + StepKind::CloudAssets => { + if let Some(live) = state.live_subdomains.as_ref() { + let assets = engine.discover_cloud_assets(live).await; + eprintln!("[agent] {} cloud assets worth review", assets.len()); + state.cloud_asset_map = Some(assets); + Ok(()) + } else { + Err(missing_step_error("live subdomains missing")) + } + } + StepKind::Takeover => { + if let Some(live) = state.live_subdomains.as_ref() { + let takeover = engine.detect_takeovers(live).await; + eprintln!("[agent] {} takeover candidates queued", takeover.len()); + state.takeover_map = Some(takeover); + Ok(()) + } else { + Err(missing_step_error("live subdomains missing")) + } + } + StepKind::SocialIntel => match engine.synthesize_social_from_state(&state) { + Ok(summary) => { + eprintln!( + "[agent] social stream surfaced {} signals (avg confidence {:.0}%)", + summary.metrics.total_signals, + summary.metrics.average_confidence + ); + state.social_intel = Some(summary); + Ok(()) + } + Err(err) => Err(err), + }, + }; + + match step_result { + Ok(_) => { + if attempt > 1 { + eprintln!("[agent] recovered after {} attempts", attempt); + } + break; + } + Err(err) => { + if attempt >= step.max_attempts { + return Err(err); + } + eprintln!( + "[agent] attempt {} failed ({}), retrying...", + attempt, err + ); + } + } + } + } + + let enumeration = state + .enumeration + .ok_or_else(|| missing_step_error("enumeration missing"))?; + let live_subdomains = state + .live_subdomains + .ok_or_else(|| missing_step_error("live subdomains missing"))?; + + let ReconEngine { + args, + client: _, + output_dir, + .. + } = engine; + let Args { domain, .. } = args; + + Ok(ReconReport { + domain, + output_dir, + discovered_subdomains: enumeration.discovered, + validated_subdomains: enumeration.validated, + live_subdomains, + open_ports_map: state.open_ports_map.unwrap_or_default(), + header_map: state.header_map.unwrap_or_default(), + cors_map: state.cors_map.unwrap_or_default(), + software_map: state.software_map.unwrap_or_default(), + takeover_map: state.takeover_map.unwrap_or_default(), + cloud_saas_map: state.cloud_saas_map.unwrap_or_default(), + cloud_asset_map: state.cloud_asset_map.unwrap_or_default(), + social_intel: state.social_intel, + }) + } +} + +fn plan(step_retries: usize) -> Vec { + let max_attempts = step_retries.max(1); + vec![ + AgentStep::new( + "Enumerate subdomains via CRT.sh", + StepKind::Enumerate, + max_attempts, + ), + AgentStep::new("Validate DNS responses", StepKind::Resolve, max_attempts), + AgentStep::new("Port scan live assets", StepKind::Ports, max_attempts), + AgentStep::new("Capture headers & TLS", StepKind::Headers, max_attempts), + AgentStep::new("Analyse CORS controls", StepKind::Cors, max_attempts), + AgentStep::new( + "Fingerprint running software", + StepKind::Fingerprint, + max_attempts, + ), + AgentStep::new( + "Predict SaaS/cloud usage", + StepKind::CloudSaas, + max_attempts, + ), + AgentStep::new( + "Discover deep cloud assets", + StepKind::CloudAssets, + max_attempts, + ), + AgentStep::new("Assess takeover exposure", StepKind::Takeover, max_attempts), + AgentStep::new( + "Synthesize social intelligence", + StepKind::SocialIntel, + max_attempts, + ), + ] +} + +struct AgentStep { + name: &'static str, + kind: StepKind, + max_attempts: usize, +} + +impl AgentStep { + fn new(name: &'static str, kind: StepKind, max_attempts: usize) -> Self { + Self { + name, + kind, + max_attempts, + } + } +} + +#[derive(Copy, Clone)] +enum StepKind { + Enumerate, + Resolve, + Ports, + Headers, + Cors, + Fingerprint, + CloudSaas, + CloudAssets, + Takeover, + SocialIntel, +} + +pub struct ReconReport { + pub domain: String, + pub output_dir: String, + pub discovered_subdomains: Vec, + pub validated_subdomains: HashSet, + pub live_subdomains: HashSet, + pub open_ports_map: HashMap>, + pub header_map: HashMap)>, + pub cors_map: HashMap>, + pub software_map: HashMap>, + pub takeover_map: HashMap>, + pub cloud_saas_map: HashMap>, + pub cloud_asset_map: HashMap>, + pub social_intel: Option, +} + +fn missing_step_error(message: &str) -> BoxError { + anyhow!(message.to_string()).into() +} \ No newline at end of file diff --git a/src/enumeration.rs b/src/enumeration.rs new file mode 100644 index 0000000..656a7f4 --- /dev/null +++ b/src/enumeration.rs @@ -0,0 +1,79 @@ +use rand::{rngs::StdRng, seq::SliceRandom, SeedableRng}; +use reqwest::{header, Client}; +use serde::Deserialize; +use std::collections::HashSet; +use std::time::Duration; +use tokio::time::sleep; + +use crate::constants::USER_AGENTS; + +#[derive(Debug, Deserialize)] +struct CrtShEntry { + name_value: String, +} + +pub async fn crtsh_enum_async( + client: &Client, + domain: &str, + max_retries: usize, +) -> Result, Box> { + let url = format!("https://crt.sh/?q=%25.{}&output=json", domain); + let mut retries = 0; + let mut last_error: Option> = None; + + let mut rng = StdRng::from_entropy(); + + while retries < max_retries { + let resp = client + .get(&url) + .header(header::USER_AGENT, *USER_AGENTS.choose(&mut rng).unwrap()) + .header(header::ACCEPT, "application/json") + .send() + .await; + + match resp { + Ok(r) => { + if r.status().is_success() { + let entries: Result, _> = r.json().await; + match entries { + Ok(entries) => { + let mut subs = HashSet::new(); + for entry in entries { + for name in entry.name_value.split('\n') { + let trimmed = name.trim(); + if !trimmed.is_empty() && !trimmed.starts_with('*') { + subs.insert(trimmed.to_string()); + } + } + } + return Ok(subs); + } + Err(e) => { + last_error = Some(Box::new(e)); + } + } + } else if r.status().is_server_error() { + last_error = Some(Box::new(std::io::Error::other(format!( + "Server error: {}", + r.status() + )))); + } + } + Err(e) => { + last_error = Some(Box::new(e)); + } + } + + retries += 1; + if retries < max_retries { + let delay = Duration::from_secs(2_u64.pow(retries as u32)); + eprintln!( + "[!] Retry {}/{} due to error: {:?}", + retries, max_retries, last_error + ); + sleep(delay).await; + } + } + + Err(last_error.unwrap_or_else(|| Box::new(std::io::Error::other("Max retries exceeded")))) +} \ No newline at end of file From 43549745829f706160b57757a9ef22a7a5621870 Mon Sep 17 00:00:00 2001 From: senseix21 Date: Sat, 20 Dec 2025 14:32:55 +0600 Subject: [PATCH 4/4] Fixed clippy warning --- examples/benchmark.rs | 2 +- shadowmap-sbom.json | 32 +++++++++++ src/pretty_report.rs | 120 ++++++++++++++++++++---------------------- src/reporting.rs | 35 +++++------- 4 files changed, 105 insertions(+), 84 deletions(-) create mode 100644 shadowmap-sbom.json diff --git a/examples/benchmark.rs b/examples/benchmark.rs index 892ae62..441cbd5 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -123,7 +123,7 @@ fn evaluate_certificate(seed: &str, offset: usize, coverage: usize) -> usize { measure_block!("tls_parse", { let len = bytes.len().max(1); bytes.rotate_left(offset % len); - if coverage % 2 == 0 { + if coverage.is_multiple_of(2) { thread::sleep(Duration::from_micros(60)); } }); diff --git a/shadowmap-sbom.json b/shadowmap-sbom.json new file mode 100644 index 0000000..303c701 --- /dev/null +++ b/shadowmap-sbom.json @@ -0,0 +1,32 @@ +{ + "components": [ + { + "licenses": [ + "Apache-2.0", + "MIT" + ], + "name": "shadowmap", + "type": "application", + "version": "0.1.0" + }, + { + "licenses": [ + "Apache-2.0" + ], + "name": "openssl", + "type": "library", + "version": "3.2.1" + }, + { + "licenses": [ + "MIT" + ], + "name": "tokio", + "type": "library", + "version": "1.40.0" + } + ], + "format": "cyclonedx", + "generated_at": "2025-12-20T08:28:34.714909+00:00", + "manifest": "Cargo.toml" +} \ No newline at end of file diff --git a/src/pretty_report.rs b/src/pretty_report.rs index 9e280f5..0078f4e 100644 --- a/src/pretty_report.rs +++ b/src/pretty_report.rs @@ -10,6 +10,21 @@ use tabled::{Table, Tabled, settings::Style}; use crate::cloud::CloudAssetFinding; use crate::social::SocialIntelligenceSummary; +pub struct ReconSummary<'a> { + pub domain: &'a str, + pub subs: &'a HashSet, + pub header_map: &'a HashMap)>, + pub open_ports_map: &'a HashMap>, + pub cors_map: &'a HashMap>, + pub software_map: &'a HashMap>, + pub takeover_map: &'a HashMap>, + #[allow(dead_code)] + pub cloud_saas_map: &'a HashMap>, + pub cloud_asset_map: &'a HashMap>, + pub social_intel: Option<&'a SocialIntelligenceSummary>, + pub output_dir: &'a str, +} + pub struct PrettyReportGenerator; #[derive(Tabled)] @@ -30,19 +45,9 @@ struct SubdomainRow { impl PrettyReportGenerator { pub fn generate_markdown_report( - domain: &str, - subs: &HashSet, - header_map: &HashMap)>, - open_ports_map: &HashMap>, - cors_map: &HashMap>, - software_map: &HashMap>, - takeover_map: &HashMap>, - _cloud_saas_map: &HashMap>, - cloud_asset_map: &HashMap>, - social_intel: Option<&SocialIntelligenceSummary>, - output_dir: &str, + summary: &ReconSummary, ) -> Result<(), Box> { - let report_file = format!("{}/{}_REPORT.md", output_dir, domain); + let report_file = format!("{}/{}_REPORT.md", summary.output_dir, summary.domain); let mut f = File::create(&report_file)?; let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S %Z"); @@ -50,7 +55,7 @@ impl PrettyReportGenerator { // Header writeln!(f, "# ShadowMap Security Report")?; writeln!(f)?; - writeln!(f, "**Target Domain:** `{}`", domain)?; + writeln!(f, "**Target Domain:** `{}`", summary.domain)?; writeln!(f, "**Scan Date:** {}", timestamp)?; writeln!(f, "**Generated by:** ShadowMap v{}", env!("CARGO_PKG_VERSION"))?; writeln!(f)?; @@ -61,10 +66,10 @@ impl PrettyReportGenerator { writeln!(f, "## Executive Summary")?; writeln!(f)?; - let total_subs = subs.len(); - let cors_issues = cors_map.len(); - let takeover_risks = takeover_map.len(); - let cloud_assets = cloud_asset_map.len(); + let total_subs = summary.subs.len(); + let cors_issues = summary.cors_map.len(); + let takeover_risks = summary.takeover_map.len(); + let cloud_assets = summary.cloud_asset_map.len(); let risk_level = if cors_issues > 0 || takeover_risks > 0 { "**HIGH**" @@ -96,7 +101,7 @@ impl PrettyReportGenerator { writeln!(f, "**Impact:** Data theft, unauthorized API access")?; writeln!(f)?; - for (sub, issues) in cors_map.iter().sorted_by_key(|(k, _)| k.as_str()) { + for (sub, issues) in summary.cors_map.iter().sorted_by_key(|(k, _)| k.as_str()) { writeln!(f, "**Host:** `{}`", sub)?; for issue in issues { writeln!(f, "- {}", issue)?; @@ -117,7 +122,7 @@ impl PrettyReportGenerator { if takeover_risks > 0 { writeln!(f, "### Subdomain Takeover Risks")?; writeln!(f)?; - for (sub, risks) in takeover_map.iter().sorted_by_key(|(k, _)| k.as_str()) { + for (sub, risks) in summary.takeover_map.iter().sorted_by_key(|(k, _)| k.as_str()) { writeln!(f, "**Subdomain:** `{}`", sub)?; for risk in risks { writeln!(f, "- {}", risk)?; @@ -133,8 +138,8 @@ impl PrettyReportGenerator { writeln!(f, "| # | Subdomain | Status | Server | Open Ports | Issues |")?; writeln!(f, "|---|-----------|--------|--------|------------|--------|")?; - for (idx, sub) in subs.iter().sorted().enumerate() { - let (status, server) = header_map.get(sub).cloned().unwrap_or((0, None)); + for (idx, sub) in summary.subs.iter().sorted().enumerate() { + let (status, server) = summary.header_map.get(sub).cloned().unwrap_or((0, None)); let status_icon = match status { 200..=299 => "OK", 300..=399 => "REDIRECT", @@ -143,12 +148,12 @@ impl PrettyReportGenerator { _ => "UNKNOWN", }; - let ports = open_ports_map.get(sub) + let ports = summary.open_ports_map.get(sub) .map(|v| v.iter().map(|p| p.to_string()).join(", ")) .unwrap_or_else(|| "-".to_string()); - let issues_count = cors_map.get(sub).map(|v| v.len()).unwrap_or(0) - + takeover_map.get(sub).map(|v| v.len()).unwrap_or(0); + let issues_count = summary.cors_map.get(sub).map(|v| v.len()).unwrap_or(0) + + summary.takeover_map.get(sub).map(|v| v.len()).unwrap_or(0); let issues_display = if issues_count > 0 { format!("ISSUES: {}", issues_count) @@ -174,7 +179,7 @@ impl PrettyReportGenerator { writeln!(f, "## Port Analysis")?; writeln!(f)?; let mut all_ports: HashSet = HashSet::new(); - for ports in open_ports_map.values() { + for ports in summary.open_ports_map.values() { all_ports.extend(ports); } @@ -182,7 +187,7 @@ impl PrettyReportGenerator { writeln!(f)?; for port in all_ports.iter().sorted() { - let hosts_with_port: Vec<&String> = open_ports_map + let hosts_with_port: Vec<&String> = summary.open_ports_map .iter() .filter(|(_, ports)| ports.contains(port)) .map(|(host, _)| host) @@ -210,11 +215,11 @@ impl PrettyReportGenerator { } // Software Fingerprints - if !software_map.is_empty() { + if !summary.software_map.is_empty() { writeln!(f, "## Software Fingerprints")?; writeln!(f)?; - for (sub, fingerprints) in software_map.iter().sorted_by_key(|(k, _)| k.as_str()) { + for (sub, fingerprints) in summary.software_map.iter().sorted_by_key(|(k, _)| k.as_str()) { if !fingerprints.is_empty() { writeln!(f, "**{}**", sub)?; for (key, value) in fingerprints { @@ -226,14 +231,14 @@ impl PrettyReportGenerator { } // Cloud Assets - if !cloud_asset_map.is_empty() { + if !summary.cloud_asset_map.is_empty() { writeln!(f, "## Cloud Infrastructure Analysis")?; writeln!(f)?; - writeln!(f, "**Total Predicted Assets:** {}", cloud_asset_map.values().map(|v| v.len()).sum::())?; + writeln!(f, "**Total Predicted Assets:** {}", summary.cloud_asset_map.values().map(|v| v.len()).sum::())?; writeln!(f)?; let mut provider_counts: HashMap = HashMap::new(); - for assets in cloud_asset_map.values() { + for assets in summary.cloud_asset_map.values() { for asset in assets { *provider_counts.entry(asset.provider.clone()).or_insert(0) += 1; } @@ -248,7 +253,7 @@ impl PrettyReportGenerator { } // Social Intelligence - if let Some(intel) = social_intel { + if let Some(intel) = summary.social_intel { writeln!(f, "## AI-Powered Threat Intelligence")?; writeln!(f)?; writeln!(f, "**Framework:** {}", intel.framework_name)?; @@ -305,7 +310,7 @@ impl PrettyReportGenerator { if takeover_risks > 0 { writeln!(f, "2. **Investigate subdomain takeover risks** on {} subdomain(s)", takeover_risks)?; } - if open_ports_map.values().any(|ports| ports.contains(&22)) { + if summary.open_ports_map.values().any(|ports| ports.contains(&22)) { writeln!(f, "3. **Review SSH exposure** - Port 22 should not be publicly accessible")?; } writeln!(f)?; @@ -331,56 +336,47 @@ impl PrettyReportGenerator { Ok(()) } - pub fn print_terminal_summary( - domain: &str, - subs: &HashSet, - header_map: &HashMap)>, - open_ports_map: &HashMap>, - cors_map: &HashMap>, - takeover_map: &HashMap>, - cloud_asset_map: &HashMap>, - social_intel: Option<&SocialIntelligenceSummary>, - ) { + pub fn print_terminal_summary(summary: &ReconSummary) { println!(); println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); - println!("{}", format!(" SCAN RESULTS: {}", domain).bright_white().bold()); + println!("{}", format!(" SCAN RESULTS: {}", summary.domain).bright_white().bold()); println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); println!(); // Quick Stats println!("{}", "QUICK STATS".bright_yellow().bold()); println!("{}", "─────────────────────────────────────────────────────────".bright_black()); - println!(" {} Subdomains discovered", subs.len().to_string().bright_green().bold()); - println!(" {} Live hosts verified", subs.len().to_string().bright_green().bold()); + println!(" {} Subdomains discovered", summary.subs.len().to_string().bright_green().bold()); + println!(" {} Live hosts verified", summary.subs.len().to_string().bright_green().bold()); - if cors_map.is_empty() { + if summary.cors_map.is_empty() { println!(" {} CORS vulnerabilities", "0".bright_green().bold()); } else { - println!(" {} CORS vulnerabilities [WARNING]", cors_map.len().to_string().bright_red().bold()); + println!(" {} CORS vulnerabilities [WARNING]", summary.cors_map.len().to_string().bright_red().bold()); } - if takeover_map.is_empty() { + if summary.takeover_map.is_empty() { println!(" {} Takeover risks", "0".bright_green().bold()); } else { - println!(" {} Takeover risks [WARNING]", takeover_map.len().to_string().bright_yellow().bold()); + println!(" {} Takeover risks [WARNING]", summary.takeover_map.len().to_string().bright_yellow().bold()); } - println!(" {} Cloud assets flagged", cloud_asset_map.len().to_string().bright_cyan().bold()); + println!(" {} Cloud assets flagged", summary.cloud_asset_map.len().to_string().bright_cyan().bold()); println!(); // Critical Findings - if !cors_map.is_empty() || !takeover_map.is_empty() { + if !summary.cors_map.is_empty() || !summary.takeover_map.is_empty() { println!("{}", "CRITICAL FINDINGS".bright_red().bold()); println!("{}", "─────────────────────────────────────────────────────────".bright_black()); - for (sub, issues) in cors_map.iter().sorted_by_key(|(k, _)| k.as_str()) { + for (sub, issues) in summary.cors_map.iter().sorted_by_key(|(k, _)| k.as_str()) { println!(" {} {}", "CORS:".bright_red().bold(), sub.bright_white()); for issue in issues { println!(" • {}", issue.bright_yellow()); } } - for (sub, risks) in takeover_map.iter().sorted_by_key(|(k, _)| k.as_str()) { + for (sub, risks) in summary.takeover_map.iter().sorted_by_key(|(k, _)| k.as_str()) { println!(" {} {}", "TAKEOVER:".bright_yellow().bold(), sub.bright_white()); for risk in risks { println!(" • {}", risk); @@ -394,25 +390,25 @@ impl PrettyReportGenerator { println!("{}", "─────────────────────────────────────────────────────────".bright_black()); let mut rows: Vec = Vec::new(); - for sub in subs.iter().sorted().take(10) { - let (status, server) = header_map.get(sub).cloned().unwrap_or((0, None)); + for sub in summary.subs.iter().sorted().take(10) { + let (status, server) = summary.header_map.get(sub).cloned().unwrap_or((0, None)); let status_str = if status == 0 { "N/A".to_string() } else { format!("{}", status) }; - let ports = open_ports_map.get(sub) + let ports = summary.open_ports_map.get(sub) .map(|v| v.iter().take(4).map(|p| p.to_string()).join(",")) .unwrap_or_else(|| "-".to_string()); - let cors_str = if cors_map.contains_key(sub) { + let cors_str = if summary.cors_map.contains_key(sub) { "WARNING".to_string() } else { "OK".to_string() }; - let risks = if takeover_map.contains_key(sub) { + let risks = if summary.takeover_map.contains_key(sub) { "WARNING".to_string() } else { "OK".to_string() @@ -433,14 +429,14 @@ impl PrettyReportGenerator { println!("{}", table); } - if subs.len() > 10 { + if summary.subs.len() > 10 { println!(); - println!(" {} more subdomain(s) not shown...", (subs.len() - 10).to_string().bright_black()); + println!(" {} more subdomain(s) not shown...", (summary.subs.len() - 10).to_string().bright_black()); } println!(); // Social Intelligence Summary - if let Some(intel) = social_intel { + if let Some(intel) = summary.social_intel { if intel.metrics.total_signals > 0 { println!("{}", "AI THREAT INTELLIGENCE".bright_magenta().bold()); println!("{}", "─────────────────────────────────────────────────────────".bright_black()); diff --git a/src/reporting.rs b/src/reporting.rs index 1a4e249..71465cc 100644 --- a/src/reporting.rs +++ b/src/reporting.rs @@ -7,7 +7,7 @@ use itertools::Itertools; use crate::cloud::CloudAssetFinding; use crate::social::SocialIntelligenceSummary; -use crate::pretty_report::PrettyReportGenerator; +use crate::pretty_report::{PrettyReportGenerator, ReconSummary}; pub struct ReconMaps<'a> { pub header_map: &'a HashMap)>, @@ -190,31 +190,24 @@ pub fn write_outputs( )?; // Generate beautiful markdown report - PrettyReportGenerator::generate_markdown_report( + let summary = ReconSummary { domain, subs, - maps.header_map, - maps.open_ports_map, - maps.cors_map, - maps.software_map, - maps.takeover_map, - maps.cloud_saas_map, - maps.cloud_asset_map, - maps.social_intel, + header_map: maps.header_map, + open_ports_map: maps.open_ports_map, + cors_map: maps.cors_map, + software_map: maps.software_map, + takeover_map: maps.takeover_map, + cloud_saas_map: maps.cloud_saas_map, + cloud_asset_map: maps.cloud_asset_map, + social_intel: maps.social_intel, output_dir, - )?; + }; + + PrettyReportGenerator::generate_markdown_report(&summary)?; // Print terminal summary - PrettyReportGenerator::print_terminal_summary( - domain, - subs, - maps.header_map, - maps.open_ports_map, - maps.cors_map, - maps.takeover_map, - maps.cloud_asset_map, - maps.social_intel, - ); + PrettyReportGenerator::print_terminal_summary(&summary); Ok(()) }