1#![forbid(unsafe_code)]
2#![cfg_attr(
4 not(test),
5 deny(
6 clippy::unwrap_used,
7 clippy::expect_used,
8 clippy::todo,
9 clippy::unimplemented,
10 clippy::panic
11 )
12)]
13#![allow(
14 clippy::module_name_repetitions,
15 clippy::must_use_candidate,
16 clippy::missing_errors_doc
17)]
18
19use async_trait::async_trait;
36use gossan_core::{Config, ScanClient, ScanInput, Scanner, Target};
37use std::sync::Arc;
38
39pub mod cache;
40pub mod db;
41pub mod enrichment;
42pub mod ingest;
43pub mod query;
44pub mod ratelimit;
45pub mod sources;
46
47use cache::IntelCache;
48use sources::IntelSource;
49
50pub struct IntelScanner {
52 pub client: ScanClient,
54 pub sources: Vec<Arc<dyn IntelSource>>,
56 pub cache: Option<Arc<IntelCache>>,
58 pub cache_ttl_secs: u64,
60 pub db: Option<Arc<db::IntelDb>>,
62 pub limiter: Option<Arc<ratelimit::ServiceRateLimiter>>,
64}
65
66impl IntelScanner {
67 pub fn from_config(config: &Config) -> anyhow::Result<Self> {
73 let client = ScanClient::default_client();
74 let inner = client.inner().clone();
75
76 let mut sources: Vec<Arc<dyn IntelSource>> = Vec::new();
77
78 let greynoise_key = config.api_keys.get("greynoise").cloned();
79 sources.push(Arc::new(sources::greynoise::GreyNoiseSource::new(
80 inner.clone(),
81 greynoise_key,
82 )));
83
84 let censys_id = config.api_keys.get("censys_id").cloned();
85 let censys_secret = config.api_keys.get("censys_secret").cloned();
86 sources.push(Arc::new(sources::censys::CensysSource::new(
87 inner.clone(),
88 censys_id,
89 censys_secret,
90 )));
91
92 let shodan_key = config.api_keys.get("shodan").cloned();
93 sources.push(Arc::new(sources::shodan::ShodanSource::new(
94 inner.clone(),
95 shodan_key,
96 )));
97
98 let abuseipdb_key = config.api_keys.get("abuseipdb").cloned();
99 sources.push(Arc::new(sources::abuseipdb::AbuseIpdbSource::new(
100 inner.clone(),
101 abuseipdb_key,
102 )));
103
104 let vt_key = config.api_keys.get("virustotal").cloned();
105 sources.push(Arc::new(sources::virustotal::VirusTotalSource::new(
106 inner.clone(),
107 vt_key,
108 )));
109
110 let urlscan_key = config.api_keys.get("urlscan").cloned();
111 sources.push(Arc::new(sources::urlscan::UrlScanSource::new(
112 inner.clone(),
113 urlscan_key,
114 )));
115
116 let asn_token = config.api_keys.get("ipinfo").cloned();
117 sources.push(Arc::new(sources::asn::AsnSource::new(
118 inner.clone(),
119 asn_token,
120 )));
121
122 let pdns_key = config.api_keys.get("passive_dns").cloned();
123 let pdns_endpoint = config
124 .api_keys
125 .get("passive_dns_endpoint")
126 .cloned()
127 .unwrap_or_else(|| "https://api.dnsdb.info".to_string());
128 sources.push(Arc::new(sources::passive_dns::PassiveDnsSource::new(
129 inner.clone(),
130 pdns_key,
131 pdns_endpoint,
132 )));
133
134 let cache = if let Some(ref path) = config.intel_db_path {
135 Some(Arc::new(IntelCache::open(path)?))
136 } else {
137 None
138 };
139
140 let db = if let Some(ref path) = config.intel_db_path {
141 Some(Arc::new(db::IntelDb::open(path)?))
142 } else {
143 None
144 };
145
146 let limiter = Some(ratelimit::build_limiter(config.rate_limit.max(1)));
147
148 Ok(Self {
149 client,
150 sources,
151 cache,
152 cache_ttl_secs: 86_400, db,
154 limiter,
155 })
156 }
157
158 pub fn new(path: &str) -> anyhow::Result<Self> {
160 let db = Arc::new(db::IntelDb::open(path)?);
161 Ok(Self {
162 client: ScanClient::default_client(),
163 sources: Vec::new(),
164 cache: None,
165 cache_ttl_secs: 86_400,
166 db: Some(db),
167 limiter: None,
168 })
169 }
170
171 pub async fn enrich_target(&self, target: &Target, input: &ScanInput) -> anyhow::Result<usize> {
175 let mut emitted = 0usize;
176
177 if let Some(ref db) = self.db {
179 let db = Arc::clone(db);
180 let target = target.clone();
181 let live_tx = input.live_tx.clone();
182 let count = tokio::task::spawn_blocking(move || {
183 let mut count = 0usize;
184 let records_by_ip = if let Some(ip_addr) = target.ip() {
185 db.query_by_ip(&ip_addr.to_string()).unwrap_or_default()
186 } else {
187 vec![]
188 };
189 let records_by_host = if let Some(host) = target.domain() {
190 db.query_by_host(host).unwrap_or_default()
191 } else {
192 vec![]
193 };
194 for r in records_by_ip.iter().chain(records_by_host.iter()) {
195 if let Some(finding) = query::record_to_finding(r) {
196 let _ = live_tx.send(finding);
197 count += 1;
198 }
199 }
200 count
201 })
202 .await?;
203 emitted += count;
204 }
205
206 let ip = target.ip().map(|i| i.to_string());
208 let domain = target.domain().map(|s| s.to_string());
209
210 for source in &self.sources {
211 if let Some(ref limiter) = self.limiter {
212 ratelimit::acquire(limiter, source.name()).await;
213 }
214
215 let enrichment = if let Some(ref ip) = ip {
216 if let Some(ref cache) = self.cache {
217 if let Some(cached) = cache.get(source.name(), "ip", ip, self.cache_ttl_secs)? {
218 Ok(cached)
219 } else {
220 let result = source.query_ip(ip).await;
221 if let Ok(ref e) = result {
222 let _ = cache.put(e);
223 }
224 result
225 }
226 } else {
227 source.query_ip(ip).await
228 }
229 } else if let Some(ref domain) = domain {
230 if let Some(ref cache) = self.cache {
231 if let Some(cached) =
232 cache.get(source.name(), "domain", domain, self.cache_ttl_secs)?
233 {
234 Ok(cached)
235 } else {
236 let result = source.query_domain(domain).await;
237 if let Ok(ref e) = result {
238 let _ = cache.put(e);
239 }
240 result
241 }
242 } else {
243 source.query_domain(domain).await
244 }
245 } else {
246 continue;
247 };
248
249 match enrichment {
250 Ok(e) => {
251 if let Some(finding) = query::enrichment_to_finding(&e) {
252 input.emit(finding);
253 emitted += 1;
254 }
255 }
256 Err(e) => {
257 tracing::warn!(source = source.name(), error = %e, "intel source failed");
258 }
259 }
260 }
261
262 Ok(emitted)
263 }
264}
265
266#[async_trait]
267impl Scanner for IntelScanner {
268 fn name(&self) -> &'static str {
269 "intel"
270 }
271 fn tags(&self) -> &[&'static str] {
272 &["passive", "active", "intel", "enrichment"]
273 }
274 fn accepts(&self, target: &Target) -> bool {
275 matches!(target, Target::Domain(_) | Target::Host(_))
276 }
277
278 async fn run(&self, input: ScanInput, _config: &Config) -> anyhow::Result<()> {
279 let mut rx = input.target_rx.lock().await;
280 while let Some(target) = rx.recv().await {
281 if let Err(e) = self.enrich_target(&target, &input).await {
282 tracing::warn!(error = %e, "intel enrichment failed for target");
283 }
284 }
285 Ok(())
286 }
287}