use std::collections::HashMap; use compliance_core::error::CoreError; use compliance_core::traits::pentest_tool::{PentestTool, PentestToolContext, PentestToolResult}; use serde_json::json; use tracing::info; use crate::recon::ReconAgent; /// PentestTool wrapper around the existing ReconAgent. /// /// Performs HTTP header fingerprinting and technology detection. /// Returns structured recon data for the LLM to use when planning attacks. pub struct ReconTool { http: reqwest::Client, agent: ReconAgent, } impl ReconTool { pub fn new(http: reqwest::Client) -> Self { let agent = ReconAgent::new(http.clone()); Self { http, agent } } } impl PentestTool for ReconTool { fn name(&self) -> &str { "recon" } fn description(&self) -> &str { "Performs reconnaissance on a target URL. Fingerprints HTTP headers, detects server \ technologies and frameworks. Returns structured data about the target's technology stack." } fn input_schema(&self) -> serde_json::Value { json!({ "type": "object", "properties": { "url": { "type": "string", "description": "Base URL to perform reconnaissance on" }, "additional_paths": { "type": "array", "description": "Optional additional paths to probe for technology fingerprinting", "items": { "type": "string" } } }, "required": ["url"] }) } fn execute<'a>( &'a self, input: serde_json::Value, _context: &'a PentestToolContext, ) -> std::pin::Pin< Box> + Send + 'a>, > { Box::pin(async move { let url = input .get("url") .and_then(|v| v.as_str()) .ok_or_else(|| CoreError::Dast("Missing required 'url' parameter".to_string()))?; let additional_paths: Vec = input .get("additional_paths") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(String::from)) .collect() }) .unwrap_or_default(); let result = self.agent.scan(url).await?; // Scan additional paths for more technology signals let mut extra_technologies: Vec = Vec::new(); let mut extra_headers: HashMap = HashMap::new(); let base_url = url.trim_end_matches('/'); for path in &additional_paths { let probe_url = format!("{base_url}/{}", path.trim_start_matches('/')); if let Ok(resp) = self.http.get(&probe_url).send().await { for (key, value) in resp.headers() { let k = key.to_string().to_lowercase(); let v = value.to_str().unwrap_or("").to_string(); // Look for technology indicators if (k == "x-powered-by" || k == "server" || k == "x-generator") && !result.technologies.contains(&v) && !extra_technologies.contains(&v) { extra_technologies.push(v.clone()); } extra_headers.insert(format!("{probe_url} -> {k}"), v); } } } let mut all_technologies = result.technologies.clone(); all_technologies.extend(extra_technologies); all_technologies.dedup(); let tech_count = all_technologies.len(); info!(url, technologies = tech_count, "Recon complete"); Ok(PentestToolResult { summary: format!( "Recon complete for {url}. Detected {} technologies. Server: {}.", tech_count, result.server.as_deref().unwrap_or("unknown") ), findings: Vec::new(), // Recon produces data, not findings data: json!({ "base_url": url, "server": result.server, "technologies": all_technologies, "interesting_headers": result.interesting_headers, "extra_headers": extra_headers, "open_ports": result.open_ports, }), }) }) } }