Some checks failed
CI / Format (push) Failing after 4s
CI / Format (pull_request) Failing after 4s
CI / Clippy (pull_request) Failing after 1m41s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
CI / Clippy (push) Failing after 1m46s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Detect Changes (push) Has been skipped
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (push) Has been skipped
CI / Deploy Dashboard (push) Has been skipped
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) 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
Split large files into focused modules across all crates while maintaining API compatibility via re-exports. Add comprehensive unit tests covering core models, pipeline parsers, LLM triage, DAST security tools, graph algorithms, and MCP parameter validation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
3.7 KiB
Rust
132 lines
3.7 KiB
Rust
use std::sync::Arc;
|
|
|
|
use axum::extract::{Extension, Path};
|
|
use axum::http::StatusCode;
|
|
use axum::response::IntoResponse;
|
|
use axum::Json;
|
|
use mongodb::bson::doc;
|
|
use serde::Deserialize;
|
|
|
|
use compliance_core::models::dast::DastFinding;
|
|
use compliance_core::models::pentest::*;
|
|
|
|
use crate::agent::ComplianceAgent;
|
|
|
|
use super::super::dto::collect_cursor_async;
|
|
|
|
type AgentExt = Extension<Arc<ComplianceAgent>>;
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct ExportBody {
|
|
pub password: String,
|
|
/// Requester display name (from auth)
|
|
#[serde(default)]
|
|
pub requester_name: String,
|
|
/// Requester email (from auth)
|
|
#[serde(default)]
|
|
pub requester_email: String,
|
|
}
|
|
|
|
/// POST /api/v1/pentest/sessions/:id/export — Export an encrypted pentest report archive
|
|
#[tracing::instrument(skip_all, fields(session_id = %id))]
|
|
pub async fn export_session_report(
|
|
Extension(agent): AgentExt,
|
|
Path(id): Path<String>,
|
|
Json(body): Json<ExportBody>,
|
|
) -> Result<axum::response::Response, (StatusCode, String)> {
|
|
let oid = mongodb::bson::oid::ObjectId::parse_str(&id)
|
|
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid session ID".to_string()))?;
|
|
|
|
if body.password.len() < 8 {
|
|
return Err((
|
|
StatusCode::BAD_REQUEST,
|
|
"Password must be at least 8 characters".to_string(),
|
|
));
|
|
}
|
|
|
|
// Fetch session
|
|
let session = agent
|
|
.db
|
|
.pentest_sessions()
|
|
.find_one(doc! { "_id": oid })
|
|
.await
|
|
.map_err(|e| {
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
format!("Database error: {e}"),
|
|
)
|
|
})?
|
|
.ok_or_else(|| (StatusCode::NOT_FOUND, "Session not found".to_string()))?;
|
|
|
|
// Resolve target name
|
|
let target = if let Ok(tid) = mongodb::bson::oid::ObjectId::parse_str(&session.target_id) {
|
|
agent
|
|
.db
|
|
.dast_targets()
|
|
.find_one(doc! { "_id": tid })
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
} else {
|
|
None
|
|
};
|
|
let target_name = target
|
|
.as_ref()
|
|
.map(|t| t.name.clone())
|
|
.unwrap_or_else(|| "Unknown Target".to_string());
|
|
let target_url = target
|
|
.as_ref()
|
|
.map(|t| t.base_url.clone())
|
|
.unwrap_or_default();
|
|
|
|
// Fetch attack chain nodes
|
|
let nodes: Vec<AttackChainNode> = match agent
|
|
.db
|
|
.attack_chain_nodes()
|
|
.find(doc! { "session_id": &id })
|
|
.sort(doc! { "started_at": 1 })
|
|
.await
|
|
{
|
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
|
Err(_) => Vec::new(),
|
|
};
|
|
|
|
// Fetch DAST findings for this session
|
|
let findings: Vec<DastFinding> = match agent
|
|
.db
|
|
.dast_findings()
|
|
.find(doc! { "session_id": &id })
|
|
.sort(doc! { "severity": -1, "created_at": -1 })
|
|
.await
|
|
{
|
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
|
Err(_) => Vec::new(),
|
|
};
|
|
|
|
let ctx = crate::pentest::report::ReportContext {
|
|
session,
|
|
target_name,
|
|
target_url,
|
|
findings,
|
|
attack_chain: nodes,
|
|
requester_name: if body.requester_name.is_empty() {
|
|
"Unknown".to_string()
|
|
} else {
|
|
body.requester_name
|
|
},
|
|
requester_email: body.requester_email,
|
|
};
|
|
|
|
let report = crate::pentest::generate_encrypted_report(&ctx, &body.password)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?;
|
|
|
|
let response = serde_json::json!({
|
|
"archive_base64": base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &report.archive),
|
|
"sha256": report.sha256,
|
|
"filename": format!("pentest-report-{id}.zip"),
|
|
});
|
|
|
|
Ok(Json(response).into_response())
|
|
}
|