Skip to main content

gossan_techstack/
lib.rs

1#![forbid(unsafe_code)]
2// pedantic moved to workspace [lints.clippy] in root Cargo.toml
3#![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
19//! Panoram tech stack scanner — thin integration layer.
20//!
21//! All fingerprinting, security header auditing, and favicon hashing logic
22//! lives in the standalone [`truestack`] crate. This module adapts
23//! `truestack` results into the panoram scanner pipeline.
24
25pub mod bridge;
26
27use async_trait::async_trait;
28use futures::StreamExt;
29use gossan_core::{Config, ScanClient, ScanInput, Scanner, ServiceTarget, Target, WebAssetTarget};
30use secfinding::Finding;
31use std::sync::Arc;
32/// Technology fingerprinting scanner — HTTP headers, HTML patterns, and JS frameworks.
33pub struct TechStackScanner;
34
35#[async_trait]
36impl Scanner for TechStackScanner {
37    fn name(&self) -> &'static str {
38        "techstack"
39    }
40    fn tags(&self) -> &[&'static str] {
41        &["active", "web", "fingerprint"]
42    }
43    fn accepts(&self, target: &Target) -> bool {
44        matches!(target, Target::Service(s) if s.is_web())
45    }
46
47    async fn run(&self, input: ScanInput, config: &Config) -> anyhow::Result<()> {
48        let client = ScanClient::from_config(config, Arc::clone(&input.resolver))?;
49
50        // Drain the streaming target receiver. Techstack fingerprinting is
51        // batch-shaped (per-asset HTTP probes via `buffer_unordered`), so
52        // the receiver is fully drained up front rather than processed
53        // incrementally.
54        let web_targets: Vec<ServiceTarget> = {
55            let mut rx = input.target_rx.lock().await;
56            let mut buf = Vec::new();
57            while let Ok(t) = rx.try_recv() {
58                if let Target::Service(s) = t {
59                    if s.is_web() {
60                        buf.push(s);
61                    }
62                }
63            }
64            buf
65        };
66
67        let results: Vec<Option<(WebAssetTarget, Vec<Finding>)>> =
68            futures::stream::iter(web_targets)
69                .map(|svc| {
70                    let client = client.clone();
71                    async move { bridge::probe(&client, svc).await.ok() }
72                })
73                .buffer_unordered(config.concurrency)
74                .collect()
75                .await;
76
77        for item in results.into_iter().flatten() {
78            let (asset, header_findings) = item;
79            tracing::debug!(
80                url = %asset.url,
81                tech = ?asset.tech.iter().map(|t| &t.name).collect::<Vec<_>>(),
82                "web asset"
83            );
84            for f in header_findings {
85                input.emit(f);
86            }
87            input.emit_target(Target::Web(Box::new(asset)));
88        }
89
90        Ok(())
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use gossan_core::{HostTarget, Protocol, Scanner};
98
99    fn service(port: u16, banner: Option<&str>) -> Target {
100        Target::Service(ServiceTarget {
101            host: HostTarget {
102                ip: "127.0.0.1".parse().unwrap(),
103                domain: Some("example.com".into()),
104            },
105            port,
106            protocol: Protocol::Tcp,
107            banner: banner.map(str::to_string),
108            tls: port == 443,
109        })
110    }
111
112    #[test]
113    fn scanner_accepts_only_web_services() {
114        let scanner = TechStackScanner;
115        assert!(scanner.accepts(&service(443, None)));
116        assert!(scanner.accepts(&service(1234, Some("HTTP/1.1 200 OK"))));
117        assert!(!scanner.accepts(&service(22, Some("SSH-2.0"))));
118    }
119
120    #[test]
121    fn scanner_metadata_is_stable() {
122        let scanner = TechStackScanner;
123        assert_eq!(scanner.name(), "techstack");
124        assert_eq!(scanner.tags(), &["active", "web", "fingerprint"]);
125    }
126}