use async_trait::async_trait;
use serde::Deserialize;
use crate::enrichment::{AsnInfo, IntelEnrichment, ServiceInfo};
use crate::sources::IntelSource;
const BASE_URL: &str = "https://search.censys.io/api/v2/hosts";
const MAX_CENSYS_JSON_BYTES: usize = 4 * 1024 * 1024;
pub struct CensysSource {
client: reqwest::Client,
api_id: Option<String>,
api_secret: Option<String>,
}
impl CensysSource {
pub fn new(
client: reqwest::Client,
api_id: Option<String>,
api_secret: Option<String>,
) -> Self {
Self {
client,
api_id,
api_secret,
}
}
}
#[async_trait]
impl IntelSource for CensysSource {
fn name(&self) -> &'static str {
"censys"
}
async fn query_ip(&self, ip: &str) -> anyhow::Result<IntelEnrichment> {
let api_id = self
.api_id
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Censys requires API ID"))?;
let api_secret = self
.api_secret
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Censys requires API secret"))?;
let mut url = url::Url::parse(BASE_URL)?;
{
let mut segs = url.path_segments_mut().map_err(|_| anyhow::anyhow!("invalid base URL"))?;
segs.push(ip);
}
let resp = self
.client
.get(url)
.basic_auth(api_id, Some(api_secret))
.send()
.await?
.error_for_status()?;
let body: CensysResp = gossan_core::net::bounded_json(resp, MAX_CENSYS_JSON_BYTES).await?;
let mut enrichment = IntelEnrichment::new("censys", "ip", ip);
if let Some(services) = body.result.services {
for svc in services {
enrichment.services.push(ServiceInfo {
port: svc.port,
protocol: svc.transport_protocol.unwrap_or_else(|| "tcp".to_string()),
banner: svc.banner,
product: svc.service_name.clone(),
});
if let Some(software) = svc.software {
for sw in software {
if let Some(name) = sw.product {
enrichment.technologies.push(name);
}
}
}
}
}
if let Some(ref asn) = body.result.asn {
enrichment.asn = Some(AsnInfo {
asn: asn.asn.clone(),
org: asn.name.clone(),
domain: None,
});
}
enrichment
.raw
.insert("censys_code".to_string(), serde_json::to_value(body.code)?);
Ok(enrichment)
}
async fn query_domain(&self, _domain: &str) -> anyhow::Result<IntelEnrichment> {
anyhow::bail!("Censys host context v2 does not support domain enrichment")
}
}
#[derive(Debug, Deserialize)]
struct CensysResp {
code: i32,
result: CensysResult,
}
#[derive(Debug, Deserialize)]
struct CensysResult {
#[serde(default)]
services: Option<Vec<CensysService>>,
#[serde(default)]
asn: Option<CensysAsn>,
}
#[derive(Debug, Deserialize)]
struct CensysService {
port: u16,
#[serde(default)]
transport_protocol: Option<String>,
#[serde(default)]
banner: Option<String>,
#[serde(default)]
service_name: Option<String>,
#[serde(default)]
software: Option<Vec<CensysSoftware>>,
}
#[derive(Debug, Deserialize)]
struct CensysSoftware {
#[serde(default)]
product: Option<String>,
}
#[derive(Debug, Deserialize)]
struct CensysAsn {
asn: String,
name: Option<String>,
}