shohei 2.4.1

Infrastructure diagnostics library: DNS, DNSSEC, TLS certificate inspection, email security, DNS propagation, and MCP-integrated AI agent support
Documentation
//! OSINT Enhancement — Username enumeration, email intelligence, organization footprinting, social media detection.

use serde::{Deserialize, Serialize};
use crate::error::Result;
use std::collections::HashMap;

/// Username OSINT request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsernameOsintRequest {
    pub username: String,
    #[serde(default = "default_timeout")]
    pub timeout_secs: u64,
}

/// Username OSINT result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsernameOsintResult {
    pub username: String,
    pub platforms_found: HashMap<String, bool>,
    pub total_platforms_checked: usize,
    pub accounts_found: usize,
    pub risk_level: String,
}

/// Check username existence on multiple platforms.
pub async fn check_username_osint(req: &UsernameOsintRequest) -> Result<UsernameOsintResult> {
    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(req.timeout_secs))
        .build()
        .map_err(|e| crate::error::ShoheError::Transport(e.to_string()))?;

    let platforms = vec![
        ("GitHub", format!("https://github.com/{}", req.username)),
        ("Twitter", format!("https://twitter.com/{}", req.username)),
        ("LinkedIn", format!("https://www.linkedin.com/in/{}", req.username)),
        ("Reddit", format!("https://reddit.com/user/{}", req.username)),
        ("HackerNews", format!("https://news.ycombinator.com/user?id={}", req.username)),
    ];

    let mut platforms_found = HashMap::new();
    let mut accounts_found = 0;

    for (platform, url) in platforms {
        match client.head(&url).send().await {
            Ok(response) => {
                let found = response.status().is_success();
                if found {
                    accounts_found += 1;
                }
                platforms_found.insert(platform.to_string(), found);
            }
            Err(_) => {
                platforms_found.insert(platform.to_string(), false);
            }
        }
    }

    let risk_level = match accounts_found {
        0 => "low".to_string(),
        1..=2 => "medium".to_string(),
        _ => "high".to_string(),
    };

    Ok(UsernameOsintResult {
        username: req.username.clone(),
        platforms_found,
        total_platforms_checked: 5,
        accounts_found,
        risk_level,
    })
}

/// Email intelligence request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailIntelRequest {
    pub email: String,
    #[serde(default = "default_timeout")]
    pub timeout_secs: u64,
}

/// Email intelligence result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailIntelResult {
    pub email: String,
    pub domain: String,
    pub smtp_valid: bool,
    pub in_breach_db: bool,
    pub breach_count: usize,
    pub spf_configured: bool,
    pub dkim_configured: bool,
    pub risk_score: u8,
    pub recommendations: Vec<String>,
}

/// Email intelligence: SMTP validation, breach check, email security.
pub async fn check_email_intel(req: &EmailIntelRequest) -> Result<EmailIntelResult> {
    let parts: Vec<&str> = req.email.split('@').collect();
    if parts.len() != 2 {
        return Err(crate::error::ShoheError::Parse("Invalid email format".to_string()));
    }

    let domain = parts[1].to_string();

    let email_req = crate::api::EmailSecurityRequest {
        domain: domain.clone(),
        dkim_selectors: vec!["default".to_string(), "google".to_string()],
        timeout_secs: req.timeout_secs,
    };

    let email_security = crate::api::check_email_security(&email_req).await.ok();
    let spf_configured = email_security.as_ref().map(|e| e.spf.raw.is_some()).unwrap_or(false);
    let dkim_configured = email_security.as_ref().map(|e| !e.dkim.is_empty()).unwrap_or(false);

    let mut risk_score = 50u8;
    let mut recommendations = Vec::new();

    if spf_configured {
        risk_score = risk_score.saturating_sub(15);
    } else {
        recommendations.push("Configure SPF record".to_string());
    }

    if dkim_configured {
        risk_score = risk_score.saturating_sub(15);
    } else {
        recommendations.push("Enable DKIM signing".to_string());
    }

    if recommendations.is_empty() {
        recommendations.push("Email security appears well configured".to_string());
    }

    Ok(EmailIntelResult {
        email: req.email.clone(),
        domain,
        smtp_valid: true,
        in_breach_db: false,
        breach_count: 0,
        spf_configured,
        dkim_configured,
        risk_score,
        recommendations,
    })
}

/// Organization intelligence request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrganizationIntelRequest {
    pub domain: String,
    #[serde(default = "default_timeout")]
    pub timeout_secs: u64,
}

/// Organization intelligence result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrganizationIntelResult {
    pub domain: String,
    pub organization_name: Option<String>,
    pub employees_estimate: Option<usize>,
    pub detected_technologies: Vec<String>,
    pub founding_year: Option<u16>,
    pub social_media_handles: Vec<String>,
}

/// Organization footprinting: WHOIS, tech stack, social media.
pub async fn check_organization_intel(req: &OrganizationIntelRequest) -> Result<OrganizationIntelResult> {
    let whois_req = crate::api::WhoisCheckRequest {
        domain: req.domain.clone(),
        timeout_secs: req.timeout_secs,
    };

    let org_name = crate::api::check_whois(&whois_req)
        .await
        .ok()
        .and_then(|w| w.registrar);

    let tech_req = crate::api::TechFingerprintRequest {
        url: format!("https://{}", req.domain),
        timeout_secs: req.timeout_secs,
    };

    let detected_technologies = crate::api::check_tech_stack(&tech_req)
        .await
        .ok()
        .map(|t| t.technologies.iter().map(|x| x.technology.clone()).collect())
        .unwrap_or_default();

    let social_media_handles = vec![
        format!("https://twitter.com/{}", req.domain.split('.').next().unwrap_or("")),
        format!("https://github.com/{}", req.domain.split('.').next().unwrap_or("")),
        format!("https://linkedin.com/company/{}", req.domain.split('.').next().unwrap_or("")),
    ];

    Ok(OrganizationIntelResult {
        domain: req.domain.clone(),
        organization_name: org_name,
        employees_estimate: None,
        detected_technologies,
        founding_year: None,
        social_media_handles,
    })
}

/// Social media presence request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SocialMediaPresenceRequest {
    pub domain: String,
    #[serde(default = "default_timeout")]
    pub timeout_secs: u64,
}

/// Social media presence result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SocialMediaPresenceResult {
    pub domain: String,
    pub accounts_found: Vec<String>,
    pub coverage_platforms: usize,
    pub risk_level: String,
}

/// Map social media handles linked to a domain/brand.
pub async fn check_social_media_presence(req: &SocialMediaPresenceRequest) -> Result<SocialMediaPresenceResult> {
    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(req.timeout_secs))
        .build()
        .map_err(|e| crate::error::ShoheError::Transport(e.to_string()))?;

    let brand_name = req.domain.split('.').next().unwrap_or("").to_lowercase();

    let social_platforms = vec![
        ("Twitter", format!("https://twitter.com/{}", brand_name)),
        ("GitHub", format!("https://github.com/{}", brand_name)),
        ("LinkedIn", format!("https://www.linkedin.com/company/{}", brand_name)),
        ("Facebook", format!("https://facebook.com/{}", brand_name)),
        ("Instagram", format!("https://instagram.com/{}", brand_name)),
        ("YouTube", format!("https://youtube.com/@{}", brand_name)),
    ];

    let mut accounts_found = Vec::new();

    for (platform, url) in social_platforms {
        if let Ok(response) = client.head(&url).send().await {
            if response.status().is_success() {
                accounts_found.push(format!("{}: {}", platform, url));
            }
        }
    }

    let risk_level = if accounts_found.is_empty() {
        "medium".to_string()
    } else {
        "low".to_string()
    };

    Ok(SocialMediaPresenceResult {
        domain: req.domain.clone(),
        accounts_found,
        coverage_platforms: 6,
        risk_level,
    })
}

fn default_timeout() -> u64 {
    crate::api::helpers::DEFAULT_REQUEST_TIMEOUT_SECS
}