Add a complete AI pentest system where Claude autonomously drives security testing via tool-calling. The LLM selects from 16 tools, chains results, and builds an attack chain DAG. Core: - PentestTool trait (dyn-compatible) with PentestToolContext/Result - PentestSession, AttackChainNode, PentestMessage, PentestEvent models - 10 new DastVulnType variants (DNS, DMARC, TLS, cookies, CSP, CORS, etc.) - LLM client chat_with_tools() for OpenAI-compatible tool calling Tools (16 total): - 5 agent wrappers: SQL injection, XSS, auth bypass, SSRF, API fuzzer - 11 new infra tools: DNS checker, DMARC checker, TLS analyzer, security headers, cookie analyzer, CSP analyzer, rate limit tester, console log detector, CORS checker, OpenAPI parser, recon - ToolRegistry for tool lookup and LLM definition generation Orchestrator: - PentestOrchestrator with iterative tool-calling loop (max 50 rounds) - Attack chain node recording per tool invocation - SSE event broadcasting for real-time progress - Strategy-aware system prompts (quick/comprehensive/targeted/aggressive/stealth) API (9 endpoints): - POST/GET /pentest/sessions, GET /pentest/sessions/:id - POST /pentest/sessions/:id/chat, GET /pentest/sessions/:id/stream - GET /pentest/sessions/:id/attack-chain, messages, findings - GET /pentest/stats Dashboard: - Pentest dashboard with stat cards, severity distribution, session list - Chat-based session page with split layout (chat + findings/attack chain) - Inline tool execution indicators, auto-polling, new session modal - Sidebar navigation item Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
147 lines
5.7 KiB
Rust
147 lines
5.7 KiB
Rust
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<DiscoveredEndpoint> {
|
|
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<Box<dyn std::future::Future<Output = Result<PentestToolResult, CoreError>> + 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() }),
|
|
})
|
|
})
|
|
}
|
|
}
|