use serde::{Deserialize, Serialize};
use crate::error::Result;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsernameOsintRequest {
pub username: String,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
}
#[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,
}
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,
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailIntelRequest {
pub email: String,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
}
#[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>,
}
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,
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrganizationIntelRequest {
pub domain: String,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
}
#[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>,
}
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,
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SocialMediaPresenceRequest {
pub domain: String,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SocialMediaPresenceResult {
pub domain: String,
pub accounts_found: Vec<String>,
pub coverage_platforms: usize,
pub risk_level: String,
}
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
}