web-analyzer 0.1.10

Enterprise domain security & intelligence platform — WHOIS, DNS, SEO, tech detection, subdomain takeover, API security scanning, and more
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DnsCheckResult {
    pub a_records: Vec<String>,
    pub aaaa_records: Vec<String>,
    pub cname_records: Vec<String>,
    pub mx_records: Vec<String>,
    pub txt_records: Vec<String>,
    pub ns_records: Vec<String>,
    pub has_valid_dns: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TakeoverVulnerability {
    pub subdomain: String,
    pub service: String,
    pub vulnerability_type: String,
    pub cname: Option<String>,
    pub confidence: String,
    pub description: String,
    pub exploitation_difficulty: String,
    pub mitigation: String,
    pub dns_info: DnsCheckResult,
    pub http_status: Option<u16>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanStatistics {
    pub subdomains_scanned: usize,
    pub vulnerable_count: usize,
    pub high_confidence: usize,
    pub medium_confidence: usize,
    pub low_confidence: usize,
    pub scan_time_secs: f64,
    pub services_checked: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TakeoverResult {
    pub domain: String,
    pub statistics: ScanStatistics,
    pub vulnerable: Vec<TakeoverVulnerability>,
}

use hickory_resolver::config::*;
use hickory_resolver::AsyncResolver;
use std::time::Instant;
use tokio::task::JoinSet;

pub async fn check_subdomain_takeover(
    domain: &str,
    subdomains: &[String],
    progress_tx: Option<tokio::sync::mpsc::Sender<crate::ScanProgress>>,
) -> Result<TakeoverResult, Box<dyn std::error::Error + Send + Sync>> {
    let start_time = Instant::now();
    let resolver = AsyncResolver::tokio(ResolverConfig::cloudflare(), ResolverOpts::default());

    if let Some(t) = &progress_tx {
        let _ = t.send(crate::ScanProgress {
            module: "Subdomain Takeover".into(),
            percentage: 10.0,
            message: format!("Checking {} subdomains for dangling CNAMEs natively", subdomains.len()),
            status: "Info".into(),
        }).await;
    }

    let mut vulnerable_list = Vec::new();
    let mut set = JoinSet::new();
    let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(10));

    for d in subdomains {
        let subdomain = d.clone();
        let res = resolver.clone();
        let permit = semaphore.clone().acquire_owned().await.unwrap();
        set.spawn(async move {
            use hickory_resolver::proto::rr::RecordType;
            let mut cname_records = vec![];
            let mut a_records = vec![];

            if let Ok(response) = res.lookup(subdomain.as_str(), RecordType::CNAME).await {
                for record in response.iter() {
                    if let Some(cname) = record.as_cname() {
                        cname_records.push(cname.to_string());
                    }
                }
            }
            
            if let Ok(response) = res.ipv4_lookup(subdomain.as_str()).await {
                for ip in response.iter() {
                    a_records.push(ip.to_string());
                }
            }

            let result = if !cname_records.is_empty() && a_records.is_empty() {
                Some(TakeoverVulnerability {
                    subdomain: subdomain.clone(),
                    service: "Unknown".into(),
                    vulnerability_type: "Dangling CNAME".into(),
                    cname: Some(cname_records[0].clone()),
                    confidence: "High".into(),
                    description: format!("CNAME points to {} which doesn't resolve to an IP.", cname_records[0]),
                    exploitation_difficulty: "Medium".into(),
                    mitigation: "Remove the DNS record or claim the external resource.".into(),
                    dns_info: DnsCheckResult {
                        a_records,
                        aaaa_records: vec![],
                        cname_records,
                        mx_records: vec![],
                        txt_records: vec![],
                        ns_records: vec![],
                        has_valid_dns: true,
                    },
                    http_status: None,
                })
            } else {
                None
            };
            drop(permit);
            result
        });
    }

    let mut scanned = 0;
    while let Some(vuln_opt) = set.join_next().await {
        scanned += 1;
        if let Ok(Some(v)) = vuln_opt {
            vulnerable_list.push(v);
        }
    }

    if let Some(t) = &progress_tx {
        let _ = t.send(crate::ScanProgress {
            module: "Subdomain Takeover".into(),
            percentage: 100.0,
            message: "Finished native CNAME checking".into(),
            status: "Info".into(),
        }).await;
    }

    Ok(TakeoverResult {
        domain: domain.to_string(),
        statistics: ScanStatistics {
            subdomains_scanned: scanned,
            vulnerable_count: vulnerable_list.len(),
            high_confidence: vulnerable_list.len(),
            medium_confidence: 0,
            low_confidence: 0,
            scan_time_secs: start_time.elapsed().as_secs_f64(),
            services_checked: 0,
        },
        vulnerable: vulnerable_list,
    })
}