use std::collections::HashSet;
use gossan_core::{Config, DiscoverySource, DomainTarget, Target};
use governor::DefaultDirectRateLimiter;
pub fn get_api_key(config: &Config, source_name: &str, env_name: &str) -> Option<String> {
config
.api_keys
.get(source_name)
.cloned()
.or_else(|| std::env::var(env_name).ok())
}
pub async fn ct_get_entries(
domain: &str,
url: &str,
config: &Config,
client: &reqwest::Client,
limiter: &DefaultDirectRateLimiter,
) -> anyhow::Result<Vec<Target>> {
limiter.until_ready().await;
let resp = client.get(url).send().await?.error_for_status()?;
let bytes = gossan_core::read_response_limited(resp, config.max_response_size).await?;
let mut seen = HashSet::new();
let domain_lower = domain.to_lowercase();
if let Ok(arr) = serde_json::from_slice::<Vec<serde_json::Value>>(&bytes) {
for item in arr {
if let Some(nv) = item.get("name_value").and_then(|v| v.as_str()) {
collect_subdomains(nv, &domain_lower, &mut seen);
} else if let Some(subs) = item.get("subjects").and_then(|v| v.as_array()) {
for s in subs.iter().filter_map(|v| v.as_str()) {
collect_subdomains(s, &domain_lower, &mut seen);
}
}
}
} else if let Ok(obj) = serde_json::from_slice::<serde_json::Value>(&bytes) {
if let Some(data) = obj.get("data").and_then(|v| v.as_array()) {
for item in data {
if let Some(nv) = item.get("name_value").and_then(|v| v.as_str()) {
collect_subdomains(nv, &domain_lower, &mut seen);
}
}
}
} else {
tracing::warn!(
url = %url,
bytes = bytes.len(),
"CT JSON body was neither array nor object; returning no subdomains"
);
}
Ok(seen
.into_iter()
.map(|d| {
Target::Domain(DomainTarget {
domain: d,
source: DiscoverySource::CertificateTransparency,
})
})
.collect())
}
fn collect_subdomains(raw: &str, domain_lower: &str, seen: &mut HashSet<String>) {
for line in raw.lines() {
let candidate = line.trim().trim_start_matches("*.").to_lowercase();
if !candidate.contains('*') && crate::is_subdomain_of(&candidate, domain_lower) {
seen.insert(candidate);
}
}
}