#![forbid(unsafe_code)]
#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::todo,
clippy::unimplemented,
clippy::panic
)
)]
#![allow(
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::missing_errors_doc
)]
pub mod bridge;
use async_trait::async_trait;
use futures::StreamExt;
use gossan_core::{Config, ScanClient, ScanInput, Scanner, ServiceTarget, Target, WebAssetTarget};
use secfinding::Finding;
use std::sync::Arc;
pub struct TechStackScanner;
#[async_trait]
impl Scanner for TechStackScanner {
fn name(&self) -> &'static str {
"techstack"
}
fn tags(&self) -> &[&'static str] {
&["active", "web", "fingerprint"]
}
fn accepts(&self, target: &Target) -> bool {
matches!(target, Target::Service(s) if s.is_web())
}
async fn run(&self, input: ScanInput, config: &Config) -> anyhow::Result<()> {
let client = ScanClient::from_config(config, Arc::clone(&input.resolver))?;
let web_targets: Vec<ServiceTarget> = {
let mut rx = input.target_rx.lock().await;
let mut buf = Vec::new();
while let Ok(t) = rx.try_recv() {
if let Target::Service(s) = t {
if s.is_web() {
buf.push(s);
}
}
}
buf
};
let results: Vec<Option<(WebAssetTarget, Vec<Finding>)>> =
futures::stream::iter(web_targets)
.map(|svc| {
let client = client.clone();
async move { bridge::probe(&client, svc).await.ok() }
})
.buffer_unordered(config.concurrency)
.collect()
.await;
for item in results.into_iter().flatten() {
let (asset, header_findings) = item;
tracing::debug!(
url = %asset.url,
tech = ?asset.tech.iter().map(|t| &t.name).collect::<Vec<_>>(),
"web asset"
);
for f in header_findings {
input.emit(f);
}
input.emit_target(Target::Web(Box::new(asset)));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use gossan_core::{HostTarget, Protocol, Scanner};
fn service(port: u16, banner: Option<&str>) -> Target {
Target::Service(ServiceTarget {
host: HostTarget {
ip: "127.0.0.1".parse().unwrap(),
domain: Some("example.com".into()),
},
port,
protocol: Protocol::Tcp,
banner: banner.map(str::to_string),
tls: port == 443,
})
}
#[test]
fn scanner_accepts_only_web_services() {
let scanner = TechStackScanner;
assert!(scanner.accepts(&service(443, None)));
assert!(scanner.accepts(&service(1234, Some("HTTP/1.1 200 OK"))));
assert!(!scanner.accepts(&service(22, Some("SSH-2.0"))));
}
#[test]
fn scanner_metadata_is_stable() {
let scanner = TechStackScanner;
assert_eq!(scanner.name(), "techstack");
assert_eq!(scanner.tags(), &["active", "web", "fingerprint"]);
}
}