use std::net::IpAddr;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use crate::client::{Client, Request};
use crate::error::{Error, Result};
use crate::retry::RetryPolicy;
use crate::timestamp::Timestamp;
const MAX_BATCH_SIZE: usize = 1000;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct IpInfo {
pub ip: IpAddr,
pub network: Option<Network>,
pub location: Option<Location>,
pub intelligence: Option<Intelligence>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Network {
pub asn: u32,
pub isp: String,
pub r#type: NetworkType,
pub org: Option<String>,
pub domain: Option<String>,
pub abuse_email: Option<String>,
pub abuse_phone: Option<String>,
}
wire_enum! {
pub enum NetworkType {
Mobile = "MOBILE",
Satellite = "SATELLITE",
InFlightWifi = "IN_FLIGHT_WIFI",
Residential = "RESIDENTIAL",
Corporate = "CORPORATE",
Academic = "ACADEMIC",
Datacenter = "DATACENTER",
Government = "GOVERNMENT",
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Location {
pub country: String,
pub state: String,
pub city: String,
pub timezone: String,
pub latitude: f64,
pub longitude: f64,
pub geo_hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Intelligence {
pub risk_score: u8,
pub categories: Vec<String>,
pub behavior: Vec<String>,
pub providers: Vec<Provider>,
pub devices: Vec<Device>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Provider {
#[serde(rename = "provider")]
pub name: String,
pub r#type: String,
pub last_seen: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Device {
pub os: Os,
pub version: String,
pub last_seen: Timestamp,
}
wire_enum! {
pub enum Os {
Android = "Android",
ChromeOs = "Chrome OS",
FreeBsd = "FreeBSD",
Ios = "iOS",
Linux = "Linux",
MacOsX = "Mac OS X",
Windows = "Windows",
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DomainInfo {
pub domain: String,
pub status: DomainStatus,
pub stats: DomainStats,
pub unique_ips: UniqueIps,
pub time_series: Vec<TimeSeriesPoint>,
pub top_asn: Option<AsnCount>,
pub top_subdomains: Vec<SubdomainCount>,
pub top_ports: Vec<PortCount>,
pub geo_distribution: Vec<CountryCount>,
pub hour_dow_heatmap: Vec<Vec<u64>>,
pub activity_stats: ActivityStats,
pub recent_events: Vec<DomainEvent>,
}
wire_enum! {
pub enum DomainStatus {
Active = "active",
Inactive = "inactive",
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DomainStats {
pub events_24h: u64,
pub total_events_30d: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UniqueIps {
pub value_24h: u64,
pub value_30d: u64,
pub sparkline_24h: Vec<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TimeSeriesPoint {
pub date: Timestamp,
pub events: u64,
pub unique_ips: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AsnCount {
pub asn: u32,
pub events: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SubdomainCount {
pub subdomain: String,
pub count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PortCount {
pub port: u16,
pub count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CountryCount {
pub country_code: String,
pub events: u64,
pub unique_ips: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ActivityStats {
pub cadence: Cadence,
pub median_per_hour: u64,
pub p95_per_hour: u64,
pub peak_hour: u8,
pub quiet_hour: u8,
}
wire_enum! {
pub enum Cadence {
NoActivity = "No activity in the last 30 days.",
EvenlyDistributed = "Activity is evenly distributed across the day.",
BusinessHours = "Activity follows a weekday business-hours pattern.",
Weekend = "Activity follows a weekend pattern.",
BurstDriven = "Activity is burst-driven with significant peaks.",
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DomainEvent {
pub timestamp: Timestamp,
#[serde(rename = "source_ip_masked")]
pub source_ip: String,
pub target_subdomain: String,
pub country_code: String,
pub port: u16,
}
#[derive(Serialize)]
#[cfg_attr(test, derive(Deserialize))]
pub(crate) struct BatchRequest {
pub(crate) ips: Vec<IpAddr>,
}
#[derive(Deserialize)]
#[cfg_attr(test, derive(Serialize))]
pub(crate) struct BatchResponse {
pub(crate) results: Vec<IpInfo>,
}
impl Client {
pub async fn lookup_ip(&self, ip: impl Into<IpAddr>) -> Result<IpInfo> {
let ip = ip.into().to_string();
let url = self.endpoint(["lookup", "ip", ip.as_str()]);
self.request_json(Request::get(url, RetryPolicy::RateLimit))
.await
}
pub async fn lookup_ips(&self, ips: &[IpAddr]) -> Result<Vec<IpInfo>> {
if ips.is_empty() {
return Ok(Vec::new());
}
if ips.len() > MAX_BATCH_SIZE {
return Err(Error::InvalidInput(format!(
"{} addresses exceed the batch limit of {MAX_BATCH_SIZE}",
ips.len()
)));
}
let body = serde_json::to_vec(&BatchRequest { ips: ips.to_vec() })?;
let request = Request {
method: Method::POST,
body: Some(body.into()),
..Request::get(self.endpoint(["lookup", "ips"]), RetryPolicy::RateLimit)
};
let response: BatchResponse = self.request_json(request).await?;
Ok(response.results)
}
pub async fn lookup_domain(&self, domain: &str, limit: Option<u32>) -> Result<DomainInfo> {
if matches!(domain, "" | "." | "..") {
return Err(Error::InvalidInput(format!("invalid domain {domain:?}")));
}
let mut url = self.endpoint(["lookup", "domain", domain]);
if let Some(limit) = limit {
url.query_pairs_mut()
.append_pair("limit", &limit.to_string());
}
self.request_json(Request::get(url, RetryPolicy::RateLimit))
.await
}
}