use compliance_core::error::CoreError; use compliance_core::traits::dast_agent::{DastAgent, DastContext, DiscoveredEndpoint, EndpointParameter}; use compliance_core::traits::pentest_tool::{PentestTool, PentestToolContext, PentestToolResult}; use serde_json::json; use crate::agents::api_fuzzer::ApiFuzzerAgent; /// PentestTool wrapper around the existing ApiFuzzerAgent. pub struct ApiFuzzerTool { http: reqwest::Client, agent: ApiFuzzerAgent, } impl ApiFuzzerTool { pub fn new(http: reqwest::Client) -> Self { let agent = ApiFuzzerAgent::new(http.clone()); Self { http, agent } } fn parse_endpoints(input: &serde_json::Value) -> Vec { let mut endpoints = Vec::new(); if let Some(arr) = input.get("endpoints").and_then(|v| v.as_array()) { for ep in arr { let url = ep.get("url").and_then(|v| v.as_str()).unwrap_or_default().to_string(); let method = ep.get("method").and_then(|v| v.as_str()).unwrap_or("GET").to_string(); let mut parameters = Vec::new(); if let Some(params) = ep.get("parameters").and_then(|v| v.as_array()) { for p in params { parameters.push(EndpointParameter { name: p.get("name").and_then(|v| v.as_str()).unwrap_or_default().to_string(), location: p.get("location").and_then(|v| v.as_str()).unwrap_or("query").to_string(), param_type: p.get("param_type").and_then(|v| v.as_str()).map(String::from), example_value: p.get("example_value").and_then(|v| v.as_str()).map(String::from), }); } } endpoints.push(DiscoveredEndpoint { url, method, parameters, content_type: ep.get("content_type").and_then(|v| v.as_str()).map(String::from), requires_auth: ep.get("requires_auth").and_then(|v| v.as_bool()).unwrap_or(false), }); } } endpoints } } impl PentestTool for ApiFuzzerTool { fn name(&self) -> &str { "api_fuzzer" } fn description(&self) -> &str { "Fuzzes API endpoints to discover misconfigurations, information disclosure, and hidden \ endpoints. Probes common sensitive paths and tests for verbose error messages." } fn input_schema(&self) -> serde_json::Value { json!({ "type": "object", "properties": { "endpoints": { "type": "array", "description": "Known endpoints to fuzz", "items": { "type": "object", "properties": { "url": { "type": "string" }, "method": { "type": "string", "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"] }, "parameters": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "location": { "type": "string" }, "param_type": { "type": "string" }, "example_value": { "type": "string" } }, "required": ["name"] } } }, "required": ["url"] } }, "base_url": { "type": "string", "description": "Base URL to probe for common sensitive paths (used if no endpoints provided)" } } }) } fn execute<'a>( &'a self, input: serde_json::Value, context: &'a PentestToolContext, ) -> std::pin::Pin> + Send + 'a>> { Box::pin(async move { let mut endpoints = Self::parse_endpoints(&input); // If a base_url is provided but no endpoints, create a default endpoint if endpoints.is_empty() { if let Some(base) = input.get("base_url").and_then(|v| v.as_str()) { endpoints.push(DiscoveredEndpoint { url: base.to_string(), method: "GET".to_string(), parameters: Vec::new(), content_type: None, requires_auth: false, }); } } if endpoints.is_empty() { return Ok(PentestToolResult { summary: "No endpoints or base_url provided to fuzz.".to_string(), findings: Vec::new(), data: json!({}), }); } let dast_context = DastContext { endpoints, technologies: Vec::new(), sast_hints: Vec::new(), }; let findings = self.agent.run(&context.target, &dast_context).await?; let count = findings.len(); Ok(PentestToolResult { summary: if count > 0 { format!("Found {count} API misconfigurations or information disclosures.") } else { "No API misconfigurations detected.".to_string() }, findings, data: json!({ "endpoints_tested": dast_context.endpoints.len() }), }) }) } }