gossan_techstack/
bridge.rs1use gossan_core::{ServiceTarget, Target, TechCategory, Technology, WebAssetTarget};
7use secfinding::Finding;
8
9async fn bounded_text(resp: reqwest::Response, limit: usize) -> anyhow::Result<String> {
11 let mut buf = Vec::with_capacity(limit.min(4096));
12 let mut stream = resp.bytes_stream();
13 while let Some(chunk) = futures::StreamExt::next(&mut stream).await {
14 let chunk = chunk?;
15 let remaining = limit.saturating_sub(buf.len());
16 if remaining == 0 {
17 break;
18 }
19 let take = chunk.len().min(remaining);
20 buf.extend_from_slice(&chunk[..take]);
21 }
22 Ok(String::from_utf8_lossy(&buf).to_string())
23}
24
25pub async fn probe(
28 client: &reqwest::Client,
29 svc: ServiceTarget,
30) -> anyhow::Result<(WebAssetTarget, Vec<Finding>)> {
31 let base = svc
32 .base_url()
33 .ok_or_else(|| anyhow::anyhow!("no base url"))?;
34 let resp = client.get(base.as_str()).send().await?;
35
36 let status = resp.status().as_u16();
37 let headers: Vec<(String, String)> = resp
38 .headers()
39 .iter()
40 .map(|(k, v)| {
41 (
42 k.to_string(),
43 String::from_utf8_lossy(v.as_bytes()).to_string(),
44 )
45 })
46 .collect();
47
48 let body = bounded_text(resp, 2 * 1024 * 1024)
49 .await
50 .unwrap_or_default();
51 let title = truestack::html::extract_title(&body);
52
53 let mut ts_techs = truestack::fingerprints::detect(&headers, &body);
55
56 truestack::behavior::identify(client, base.as_str(), &mut ts_techs)
58 .await
59 .ok();
60
61 let rules = &truestack::fingerprints::RuleEngine::embedded().rules;
65 let mut ts_techs = truestack::postprocess::apply(ts_techs, rules);
66
67 truestack::version_intel::assess(&mut ts_techs, &headers);
69
70 let tech: Vec<Technology> = ts_techs.into_iter().map(convert_technology).collect();
71
72 let body_hash = {
74 use sha2::{Digest, Sha256};
75 let hash = Sha256::digest(body.as_bytes());
76 Some(hex::encode(&hash[..8]))
77 };
78
79 let favicon_hash =
81 truestack::favicon::fetch_hash_limited(client, base.as_str(), 5 * 1024 * 1024).await;
82
83 let ts_findings = truestack::security_headers::audit(&headers);
90 let web_target = Target::Service(svc.clone());
91 let panoram_target = web_target.domain().unwrap_or("?").to_string();
92 let header_findings: Vec<Finding> = ts_findings
93 .into_iter()
94 .filter_map(|f| {
95 let mut builder = Finding::builder("techstack", panoram_target.clone(), f.severity())
96 .title(f.title().to_string())
97 .detail(f.detail().to_string())
98 .kind(f.kind());
99 for ev in f.evidence() {
100 builder = builder.evidence(ev.clone());
101 }
102 for tag in f.tags() {
103 builder = builder.tag(tag.to_string());
104 }
105 for cve in f.cve_ids() {
106 builder = builder.cve(cve.to_string());
107 }
108 if let Some(hint) = f.exploit_hint() {
109 builder = builder.exploit_hint(hint.to_string());
110 }
111 builder.build().ok()
112 })
113 .collect();
114
115 Ok((
116 WebAssetTarget {
117 url: base,
118 service: svc,
119 tech,
120 status,
121 title,
122 favicon_hash,
123 body_hash,
124 forms: vec![],
125 params: vec![],
126 },
127 header_findings,
128 ))
129}
130
131fn convert_technology(t: truestack::Technology) -> Technology {
133 Technology {
134 name: t.name,
135 version: t.version,
136 category: match t.category {
137 truestack::TechCategory::Cms => TechCategory::Cms,
138 truestack::TechCategory::Framework => TechCategory::Framework,
139 truestack::TechCategory::Language => TechCategory::Language,
140 truestack::TechCategory::Server => TechCategory::Server,
141 truestack::TechCategory::Cdn => TechCategory::Cdn,
142 truestack::TechCategory::Analytics => TechCategory::Analytics,
143 truestack::TechCategory::Security => TechCategory::Security,
144 truestack::TechCategory::Database => TechCategory::Database,
145 truestack::TechCategory::Os => TechCategory::Os,
146 truestack::TechCategory::Other => TechCategory::Other,
147 },
148 confidence: t.confidence,
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn convert_technology_preserves_name_version_and_confidence() {
158 let tech = convert_technology(truestack::Technology {
159 name: "nginx".into(),
160 version: Some("1.25".into()),
161 category: truestack::TechCategory::Server,
162 confidence: 92,
163 });
164 assert_eq!(tech.name, "nginx");
165 assert_eq!(tech.version.as_deref(), Some("1.25"));
166 assert!(matches!(tech.category, TechCategory::Server));
167 assert_eq!(tech.confidence, 92);
168 }
169}