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::xss::XssAgent; /// PentestTool wrapper around the existing XssAgent. pub struct XssTool { _http: reqwest::Client, agent: XssAgent, } impl XssTool { pub fn new(http: reqwest::Client) -> Self { let agent = XssAgent::new(http.clone()); Self { _http: 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 } } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn parse_endpoints_basic() { let input = json!({ "endpoints": [ { "url": "https://example.com/search", "method": "GET", "parameters": [ { "name": "q", "location": "query" } ] } ] }); let endpoints = XssTool::parse_endpoints(&input); assert_eq!(endpoints.len(), 1); assert_eq!(endpoints[0].url, "https://example.com/search"); assert_eq!(endpoints[0].method, "GET"); assert_eq!(endpoints[0].parameters.len(), 1); assert_eq!(endpoints[0].parameters[0].name, "q"); assert_eq!(endpoints[0].parameters[0].location, "query"); } #[test] fn parse_endpoints_empty() { let input = json!({ "endpoints": [] }); assert!(XssTool::parse_endpoints(&input).is_empty()); } #[test] fn parse_endpoints_missing_key() { let input = json!({}); assert!(XssTool::parse_endpoints(&input).is_empty()); } #[test] fn parse_endpoints_defaults() { let input = json!({ "endpoints": [ { "url": "https://example.com/api", "parameters": [] } ] }); let endpoints = XssTool::parse_endpoints(&input); assert_eq!(endpoints[0].method, "GET"); // default assert!(!endpoints[0].requires_auth); // default false } #[test] fn parse_endpoints_full_params() { let input = json!({ "endpoints": [{ "url": "https://example.com", "method": "POST", "content_type": "application/json", "requires_auth": true, "parameters": [{ "name": "body", "location": "body", "param_type": "string", "example_value": "test" }] }] }); let endpoints = XssTool::parse_endpoints(&input); assert_eq!(endpoints[0].method, "POST"); assert_eq!( endpoints[0].content_type.as_deref(), Some("application/json") ); assert!(endpoints[0].requires_auth); assert_eq!( endpoints[0].parameters[0].param_type.as_deref(), Some("string") ); assert_eq!( endpoints[0].parameters[0].example_value.as_deref(), Some("test") ); } } impl PentestTool for XssTool { fn name(&self) -> &str { "xss_scanner" } fn description(&self) -> &str { "Tests endpoints for Cross-Site Scripting (XSS) vulnerabilities including reflected, \ stored, and DOM-based XSS. Provide endpoints with parameters to test." } fn input_schema(&self) -> serde_json::Value { json!({ "type": "object", "properties": { "endpoints": { "type": "array", "description": "Endpoints to test for XSS", "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", "enum": ["query", "body", "header", "path", "cookie"] }, "param_type": { "type": "string" }, "example_value": { "type": "string" } }, "required": ["name"] } } }, "required": ["url", "method", "parameters"] } }, "custom_payloads": { "type": "array", "description": "Optional additional XSS payloads to test", "items": { "type": "string" } } }, "required": ["endpoints"] }) } fn execute<'a>( &'a self, input: serde_json::Value, context: &'a PentestToolContext, ) -> std::pin::Pin< Box> + Send + 'a>, > { Box::pin(async move { let endpoints = Self::parse_endpoints(&input); if endpoints.is_empty() { return Ok(PentestToolResult { summary: "No endpoints provided to test.".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} XSS vulnerabilities.") } else { "No XSS vulnerabilities detected.".to_string() }, findings, data: json!({ "endpoints_tested": dast_context.endpoints.len() }), }) }) } }