feat: add pentest MCP tools, session timeout, and error recovery
Add 5 MCP tools for querying pentest sessions, attack chains, messages, and stats. Add session timeout (30min) and automatic failure marking with run_session_guarded wrapper. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -102,18 +102,9 @@ pub async fn create_session(
|
||||
let target_clone = target.clone();
|
||||
tokio::spawn(async move {
|
||||
let orchestrator = PentestOrchestrator::new(llm, db);
|
||||
if let Err(e) = orchestrator
|
||||
.run_session(&session_clone, &target_clone, &initial_message)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Pentest orchestrator failed for session {}: {e}",
|
||||
session_clone
|
||||
.id
|
||||
.map(|oid| oid.to_hex())
|
||||
.unwrap_or_default()
|
||||
);
|
||||
}
|
||||
orchestrator
|
||||
.run_session_guarded(&session_clone, &target_clone, &initial_message)
|
||||
.await;
|
||||
});
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
@@ -254,9 +245,9 @@ pub async fn send_message(
|
||||
let message = req.message.clone();
|
||||
tokio::spawn(async move {
|
||||
let orchestrator = PentestOrchestrator::new(llm, db);
|
||||
if let Err(e) = orchestrator.run_session(&session, &target, &message).await {
|
||||
tracing::error!("Pentest orchestrator failed for session {session_id}: {e}");
|
||||
}
|
||||
orchestrator
|
||||
.run_session_guarded(&session, &target, &message)
|
||||
.await;
|
||||
});
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
@@ -474,7 +465,6 @@ pub async fn pentest_stats(
|
||||
};
|
||||
|
||||
// Severity distribution from pentest-related DAST findings
|
||||
let pentest_filter = doc! { "session_id": { "$exists": true, "$ne": null } };
|
||||
let critical = db
|
||||
.dast_findings()
|
||||
.count_documents(doc! { "session_id": { "$exists": true, "$ne": null }, "severity": "critical" })
|
||||
@@ -501,8 +491,6 @@ pub async fn pentest_stats(
|
||||
.await
|
||||
.unwrap_or(0) as u32;
|
||||
|
||||
let _ = pentest_filter; // used above inline
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
data: PentestStats {
|
||||
running_sessions,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use mongodb::bson::doc;
|
||||
@@ -17,6 +18,9 @@ use crate::llm::client::{
|
||||
};
|
||||
use crate::llm::LlmClient;
|
||||
|
||||
/// Maximum duration for a single pentest session before timeout
|
||||
const SESSION_TIMEOUT: Duration = Duration::from_secs(30 * 60); // 30 minutes
|
||||
|
||||
pub struct PentestOrchestrator {
|
||||
tool_registry: ToolRegistry,
|
||||
llm: Arc<LlmClient>,
|
||||
@@ -43,7 +47,65 @@ impl PentestOrchestrator {
|
||||
self.event_tx.clone()
|
||||
}
|
||||
|
||||
pub async fn run_session(
|
||||
/// Run a pentest session with timeout and automatic failure marking on errors.
|
||||
pub async fn run_session_guarded(
|
||||
&self,
|
||||
session: &PentestSession,
|
||||
target: &DastTarget,
|
||||
initial_message: &str,
|
||||
) {
|
||||
let session_id = session.id;
|
||||
|
||||
match tokio::time::timeout(
|
||||
SESSION_TIMEOUT,
|
||||
self.run_session(session, target, initial_message),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => {
|
||||
tracing::info!(?session_id, "Pentest session completed successfully");
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!(?session_id, error = %e, "Pentest session failed");
|
||||
self.mark_session_failed(session_id, &format!("Error: {e}"))
|
||||
.await;
|
||||
let _ = self.event_tx.send(PentestEvent::Error {
|
||||
message: format!("Session failed: {e}"),
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!(?session_id, "Pentest session timed out after 30 minutes");
|
||||
self.mark_session_failed(session_id, "Session timed out after 30 minutes")
|
||||
.await;
|
||||
let _ = self.event_tx.send(PentestEvent::Error {
|
||||
message: "Session timed out after 30 minutes".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn mark_session_failed(
|
||||
&self,
|
||||
session_id: Option<mongodb::bson::oid::ObjectId>,
|
||||
reason: &str,
|
||||
) {
|
||||
if let Some(sid) = session_id {
|
||||
let _ = self
|
||||
.db
|
||||
.pentest_sessions()
|
||||
.update_one(
|
||||
doc! { "_id": sid },
|
||||
doc! { "$set": {
|
||||
"status": "failed",
|
||||
"completed_at": mongodb::bson::DateTime::now(),
|
||||
"error_message": reason,
|
||||
}},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_session(
|
||||
&self,
|
||||
session: &PentestSession,
|
||||
target: &DastTarget,
|
||||
|
||||
Reference in New Issue
Block a user