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>; #[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, Json(body): Json, ) -> Result { 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 = 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 = 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()) }