use gossan_core::net::bounded_text;
use gossan_core::{ServiceTarget, Target, TechCategory, Technology, WebAssetTarget};
use secfinding::Finding;
pub async fn probe(
client: &reqwest::Client,
svc: ServiceTarget,
) -> anyhow::Result<(WebAssetTarget, Vec<Finding>)> {
let base = svc
.base_url()
.ok_or_else(|| anyhow::anyhow!("no base url"))?;
let resp = client.get(base.as_str()).send().await?;
let status = resp.status().as_u16();
const MAX_HEADERS: usize = 128;
const MAX_HEADER_VALUE_LEN: usize = 8 * 1024;
let headers: Vec<(String, String)> = resp
.headers()
.iter()
.take(MAX_HEADERS)
.map(|(k, v)| {
let val = String::from_utf8_lossy(v.as_bytes());
let truncated = if val.len() > MAX_HEADER_VALUE_LEN {
&val[..MAX_HEADER_VALUE_LEN]
} else {
&val
};
(k.to_string(), truncated.to_string())
})
.collect();
let body = bounded_text(resp, crate::MAX_TECHSTACK_BODY_BYTES).await?;
let title = truestack::html::extract_title(&body);
let mut ts_techs = truestack::fingerprints::detect(&headers, &body);
if let Err(e) = truestack::behavior::identify(client, base.as_str(), &mut ts_techs).await {
tracing::warn!(error = %e, url = %base, "techstack behavioral identify failed");
}
let rules = &truestack::fingerprints::RuleEngine::embedded().rules;
let mut ts_techs = truestack::postprocess::apply(ts_techs, rules);
truestack::version_intel::assess(&mut ts_techs, &headers);
let tech: Vec<Technology> = ts_techs.into_iter().map(convert_technology).collect();
let body_hash = {
use sha2::{Digest, Sha256};
let hash = Sha256::digest(body.as_bytes());
Some(hex::encode(&hash[..8]))
};
let favicon_hash =
truestack::favicon::fetch_hash_limited(client, base.as_str(), crate::MAX_TECHSTACK_FAVICON_BYTES).await;
let ts_findings = truestack::security_headers::audit(&headers);
let web_target = Target::Service(svc.clone());
let panoram_target = web_target.domain().unwrap_or("?").to_string();
let header_findings: Vec<Finding> = ts_findings
.into_iter()
.filter_map(|f| {
let mut builder = Finding::builder("techstack", panoram_target.clone(), f.severity())
.title(f.title().to_string())
.detail(f.detail().to_string())
.kind(f.kind());
for ev in f.evidence() {
builder = builder.evidence(ev.clone());
}
for tag in f.tags() {
builder = builder.tag(tag.to_string());
}
for cve in f.cve_ids() {
builder = builder.cve(cve.to_string());
}
if let Some(hint) = f.exploit_hint() {
builder = builder.exploit_hint(hint.to_string());
}
match builder.build() {
Ok(finding) => Some(finding),
Err(e) => {
tracing::warn!(error = %e, "techstack security-header finding build failed");
None
}
}
})
.collect();
Ok((
WebAssetTarget {
url: base,
service: svc,
tech,
status,
title,
favicon_hash,
body_hash,
forms: vec![],
params: vec![],
},
header_findings,
))
}
fn convert_technology(t: truestack::Technology) -> Technology {
Technology {
name: t.name,
version: t.version,
category: match t.category {
truestack::TechCategory::Cms => TechCategory::Cms,
truestack::TechCategory::Framework => TechCategory::Framework,
truestack::TechCategory::Language => TechCategory::Language,
truestack::TechCategory::Server => TechCategory::Server,
truestack::TechCategory::Cdn => TechCategory::Cdn,
truestack::TechCategory::Analytics => TechCategory::Analytics,
truestack::TechCategory::Security => TechCategory::Security,
truestack::TechCategory::Database => TechCategory::Database,
truestack::TechCategory::Os => TechCategory::Os,
truestack::TechCategory::Other => TechCategory::Other,
},
confidence: t.confidence,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn convert_technology_preserves_name_version_and_confidence() {
let tech = convert_technology(truestack::Technology {
name: "nginx".into(),
version: Some("1.25".into()),
category: truestack::TechCategory::Server,
confidence: 92,
});
assert_eq!(tech.name, "nginx");
assert_eq!(tech.version.as_deref(), Some("1.25"));
assert!(matches!(tech.category, TechCategory::Server));
assert_eq!(tech.confidence, 92);
}
}