Some checks failed
CI / Check (pull_request) Failing after 6m3s
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped
- cleanup.rs: 8 tests — routing logic, skip conditions, missing config errors - orchestrator.rs: 7 tests — summarize_tool_output (screenshot strip, truncation, recursion) - findings.rs: 6 tests — empty state, severity grouping, SAST correlation, evidence table - tools/mod.rs: 4 tests — registry completeness, schema validation, browser action enum - models.rs: 4 tests — TestUserRecord serde, IdentityProvider variants, BSON roundtrip Total: 326 tests (was 297) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
247 lines
7.3 KiB
Rust
247 lines
7.3 KiB
Rust
pub mod api_fuzzer;
|
|
pub mod auth_bypass;
|
|
pub mod browser;
|
|
pub mod console_log_detector;
|
|
pub mod cookie_analyzer;
|
|
pub mod cors_checker;
|
|
pub mod csp_analyzer;
|
|
pub mod dmarc_checker;
|
|
pub mod dns_checker;
|
|
pub mod openapi_parser;
|
|
pub mod rate_limit_tester;
|
|
pub mod recon;
|
|
pub mod security_headers;
|
|
pub mod sql_injection;
|
|
pub mod ssrf;
|
|
pub mod tls_analyzer;
|
|
pub mod xss;
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use compliance_core::traits::pentest_tool::PentestTool;
|
|
|
|
/// A definition describing a tool for LLM tool_use registration.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ToolDefinition {
|
|
pub name: String,
|
|
pub description: String,
|
|
pub input_schema: serde_json::Value,
|
|
}
|
|
|
|
/// Registry that holds all available pentest tools and provides
|
|
/// look-up by name.
|
|
pub struct ToolRegistry {
|
|
tools: HashMap<String, Box<dyn PentestTool>>,
|
|
}
|
|
|
|
impl Default for ToolRegistry {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl ToolRegistry {
|
|
/// Create a new registry with all built-in tools pre-registered.
|
|
#[allow(clippy::expect_used)]
|
|
pub fn new() -> Self {
|
|
let http = reqwest::Client::builder()
|
|
.danger_accept_invalid_certs(true)
|
|
.timeout(std::time::Duration::from_secs(30))
|
|
.redirect(reqwest::redirect::Policy::limited(5))
|
|
.build()
|
|
.expect("failed to build HTTP client");
|
|
|
|
let mut tools: HashMap<String, Box<dyn PentestTool>> = HashMap::new();
|
|
|
|
// Agent-wrapping tools
|
|
let register = |tools: &mut HashMap<String, Box<dyn PentestTool>>,
|
|
tool: Box<dyn PentestTool>| {
|
|
tools.insert(tool.name().to_string(), tool);
|
|
};
|
|
|
|
register(
|
|
&mut tools,
|
|
Box::new(sql_injection::SqlInjectionTool::new(http.clone())),
|
|
);
|
|
register(&mut tools, Box::new(xss::XssTool::new(http.clone())));
|
|
register(
|
|
&mut tools,
|
|
Box::new(auth_bypass::AuthBypassTool::new(http.clone())),
|
|
);
|
|
register(&mut tools, Box::new(ssrf::SsrfTool::new(http.clone())));
|
|
register(
|
|
&mut tools,
|
|
Box::new(api_fuzzer::ApiFuzzerTool::new(http.clone())),
|
|
);
|
|
|
|
// New infrastructure / analysis tools
|
|
register(&mut tools, Box::<dns_checker::DnsCheckerTool>::default());
|
|
register(
|
|
&mut tools,
|
|
Box::<dmarc_checker::DmarcCheckerTool>::default(),
|
|
);
|
|
register(
|
|
&mut tools,
|
|
Box::new(tls_analyzer::TlsAnalyzerTool::new(http.clone())),
|
|
);
|
|
register(
|
|
&mut tools,
|
|
Box::new(security_headers::SecurityHeadersTool::new(http.clone())),
|
|
);
|
|
register(
|
|
&mut tools,
|
|
Box::new(cookie_analyzer::CookieAnalyzerTool::new(http.clone())),
|
|
);
|
|
register(
|
|
&mut tools,
|
|
Box::new(csp_analyzer::CspAnalyzerTool::new(http.clone())),
|
|
);
|
|
register(
|
|
&mut tools,
|
|
Box::new(rate_limit_tester::RateLimitTesterTool::new(http.clone())),
|
|
);
|
|
register(
|
|
&mut tools,
|
|
Box::new(console_log_detector::ConsoleLogDetectorTool::new(
|
|
http.clone(),
|
|
)),
|
|
);
|
|
register(
|
|
&mut tools,
|
|
Box::new(cors_checker::CorsCheckerTool::new(http.clone())),
|
|
);
|
|
register(
|
|
&mut tools,
|
|
Box::new(openapi_parser::OpenApiParserTool::new(http.clone())),
|
|
);
|
|
register(&mut tools, Box::new(recon::ReconTool::new(http)));
|
|
register(&mut tools, Box::<browser::BrowserTool>::default());
|
|
|
|
Self { tools }
|
|
}
|
|
|
|
/// Look up a tool by name.
|
|
pub fn get(&self, name: &str) -> Option<&dyn PentestTool> {
|
|
self.tools.get(name).map(|b| b.as_ref())
|
|
}
|
|
|
|
/// Return definitions for every registered tool.
|
|
pub fn all_definitions(&self) -> Vec<ToolDefinition> {
|
|
self.tools
|
|
.values()
|
|
.map(|t| ToolDefinition {
|
|
name: t.name().to_string(),
|
|
description: t.description().to_string(),
|
|
input_schema: t.input_schema(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Return the names of all registered tools.
|
|
pub fn list_names(&self) -> Vec<String> {
|
|
self.tools.keys().cloned().collect()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn registry_has_all_expected_tools() {
|
|
let registry = ToolRegistry::new();
|
|
let names = registry.list_names();
|
|
|
|
let expected = [
|
|
"recon",
|
|
"openapi_parser",
|
|
"dns_checker",
|
|
"dmarc_checker",
|
|
"tls_analyzer",
|
|
"security_headers",
|
|
"cookie_analyzer",
|
|
"csp_analyzer",
|
|
"cors_checker",
|
|
"rate_limit_tester",
|
|
"console_log_detector",
|
|
"sql_injection_scanner",
|
|
"xss_scanner",
|
|
"ssrf_scanner",
|
|
"auth_bypass_scanner",
|
|
"api_fuzzer",
|
|
"browser",
|
|
];
|
|
|
|
for name in &expected {
|
|
assert!(
|
|
names.contains(&name.to_string()),
|
|
"Missing tool: {name}. Registered: {names:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn registry_get_returns_tool() {
|
|
let registry = ToolRegistry::new();
|
|
assert!(registry.get("recon").is_some());
|
|
assert!(registry.get("browser").is_some());
|
|
assert!(registry.get("nonexistent").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn all_definitions_have_valid_schemas() {
|
|
let registry = ToolRegistry::new();
|
|
let defs = registry.all_definitions();
|
|
|
|
assert!(!defs.is_empty());
|
|
for def in &defs {
|
|
assert!(!def.name.is_empty(), "Tool has empty name");
|
|
assert!(
|
|
!def.description.is_empty(),
|
|
"Tool {} has empty description",
|
|
def.name
|
|
);
|
|
assert!(
|
|
def.input_schema.is_object(),
|
|
"Tool {} schema is not an object",
|
|
def.name
|
|
);
|
|
// Every schema should have "type": "object"
|
|
assert_eq!(
|
|
def.input_schema.get("type").and_then(|v| v.as_str()),
|
|
Some("object"),
|
|
"Tool {} schema type is not 'object'",
|
|
def.name
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn browser_tool_schema_has_action_enum() {
|
|
let registry = ToolRegistry::new();
|
|
let browser = registry.get("browser");
|
|
assert!(browser.is_some());
|
|
let schema = browser.map(|t| t.input_schema()).unwrap_or_default();
|
|
let action_prop = schema.get("properties").and_then(|p| p.get("action"));
|
|
assert!(
|
|
action_prop.is_some(),
|
|
"Browser tool missing 'action' property"
|
|
);
|
|
let action_enum = action_prop
|
|
.and_then(|a| a.get("enum"))
|
|
.and_then(|e| e.as_array());
|
|
assert!(action_enum.is_some(), "Browser action missing enum");
|
|
let actions: Vec<&str> = action_enum
|
|
.into_iter()
|
|
.flatten()
|
|
.filter_map(|v| v.as_str())
|
|
.collect();
|
|
assert!(actions.contains(&"navigate"));
|
|
assert!(actions.contains(&"screenshot"));
|
|
assert!(actions.contains(&"click"));
|
|
assert!(actions.contains(&"fill"));
|
|
assert!(actions.contains(&"get_content"));
|
|
assert!(actions.contains(&"close"));
|
|
}
|
|
}
|