#![forbid(unsafe_code)]
#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::todo,
clippy::unimplemented,
clippy::panic
)
)]
#![allow(
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::missing_errors_doc
)]
use async_trait::async_trait;
use futures::StreamExt;
use gossan_core::{
Config, DiscoverySource, DomainTarget, NetworkTarget, ScanInput, Scanner, Target,
};
use secfinding::{Finding, Severity};
use std::sync::Arc;
pub mod asn;
pub mod conservative;
pub mod ownership;
pub struct HorizontalScanner;
#[async_trait]
impl Scanner for HorizontalScanner {
fn name(&self) -> &'static str {
"horizontal"
}
fn tags(&self) -> &[&'static str] {
&["passive", "network", "intel", "horizontal"]
}
fn accepts(&self, target: &Target) -> bool {
matches!(
target,
Target::Domain(_) | Target::Host(_) | Target::Network(_)
)
}
async fn run(&self, input: ScanInput, config: &Config) -> anyhow::Result<()> {
let client = gossan_core::ScanClient::from_config(config, Arc::clone(&input.resolver))?;
let inbound: Vec<Target> = {
let mut rx = input.target_rx.lock().await;
let mut buf = Vec::new();
while let Ok(t) = rx.try_recv() {
buf.push(t);
}
buf
};
for target in &inbound {
if let Some(ip) = target.ip() {
if let Ok(prefixes) = asn::get_prefixes_for_ip(&client, &ip.to_string()).await {
for prefix in prefixes {
let network = Target::Network(NetworkTarget {
cidr: prefix.clone(),
source: DiscoverySource::AsnLookup,
});
input.emit_target(network);
}
}
}
if let Target::Network(net) = target {
if let Ok(prefix) = net.cidr.parse::<ipnet::IpNet>() {
let hosts: Vec<_> = prefix.hosts().take(16).collect();
let ptr_results: Vec<Option<String>> = futures::stream::iter(hosts)
.map(|ip| {
let resolver = Arc::clone(&input.resolver);
async move {
resolver.reverse_lookup(ip).await.ok().and_then(|r| {
r.iter().next().map(|name| {
name.to_string().trim_end_matches('.').to_string()
})
})
}
})
.buffer_unordered(config.concurrency)
.collect()
.await;
for name in ptr_results.into_iter().flatten() {
let new_domain = Target::Domain(DomainTarget {
domain: name.clone(),
source: DiscoverySource::Crawl, });
input.emit_target(new_domain);
}
}
}
if let Target::Domain(d) = target {
if let Ok(sibling_domains) =
ownership::get_sibling_domains(&client, &d.domain).await
{
for domain in sibling_domains {
let new_domain = Target::Domain(DomainTarget {
domain: domain.clone(),
source: DiscoverySource::Crawl, });
input.emit_target(new_domain);
if let Some(finding) = Finding::builder("horizontal", &d.domain, Severity::Info)
.title("Horizontal discovery: sibling domain found via ownership correlation".to_string())
.detail(format!("Domain {} shares ownership attributes with {}. This reveals a wider attack surface.", domain, d.domain))
.tag("horizontal")
.tag("ownership-pivot")
.kind(secfinding::FindingKind::InfoDisclosure)
.build_or_log()
{
input.emit(finding);
}
}
}
}
}
Ok(())
}
}