From f8eb4ea84d8a8f80f03cecce84c17558e4f6bf9c Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:28:26 +0200 Subject: [PATCH] fix: cascade-delete DAST targets, pentests, and all downstream data when repo is deleted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, deleting a repository only cleaned up SAST findings, SBOM, scan runs, CVEs, tracker issues, graph data, and embeddings — but left orphaned DAST targets, scan runs, DAST findings, pentest sessions, attack chain nodes, and pentest messages in the database. Now the delete handler follows the full cascade chain: repo → dast_targets → dast_scan_runs → dast_findings repo → dast_targets → pentest_sessions → attack_chain_nodes repo → dast_targets → pentest_sessions → pentest_messages repo → pentest_sessions (direct repo_id link) → downstream Co-Authored-By: Claude Opus 4.6 (1M context) --- compliance-agent/src/api/handlers/repos.rs | 87 ++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/compliance-agent/src/api/handlers/repos.rs b/compliance-agent/src/api/handlers/repos.rs index 7dfd77b..891bcd5 100644 --- a/compliance-agent/src/api/handlers/repos.rs +++ b/compliance-agent/src/api/handlers/repos.rs @@ -237,5 +237,92 @@ pub async fn delete_repository( .delete_many(doc! { "repo_id": &id }) .await; + // Cascade delete DAST targets linked to this repo, and all their downstream data + // (scan runs, findings, pentest sessions, attack chains, messages) + if let Ok(mut cursor) = db.dast_targets().find(doc! { "repo_id": &id }).await { + use futures_util::StreamExt; + while let Some(Ok(target)) = cursor.next().await { + let target_id = target.id.map(|oid| oid.to_hex()).unwrap_or_default(); + if !target_id.is_empty() { + cascade_delete_dast_target(db, &target_id).await; + } + } + } + + // Also delete pentest sessions linked directly to this repo (not via target) + if let Ok(mut cursor) = db.pentest_sessions().find(doc! { "repo_id": &id }).await { + use futures_util::StreamExt; + while let Some(Ok(session)) = cursor.next().await { + let session_id = session.id.map(|oid| oid.to_hex()).unwrap_or_default(); + if !session_id.is_empty() { + let _ = db + .attack_chain_nodes() + .delete_many(doc! { "session_id": &session_id }) + .await; + let _ = db + .pentest_messages() + .delete_many(doc! { "session_id": &session_id }) + .await; + // Delete DAST findings produced by this session + let _ = db + .dast_findings() + .delete_many(doc! { "session_id": &session_id }) + .await; + } + } + } + let _ = db + .pentest_sessions() + .delete_many(doc! { "repo_id": &id }) + .await; + Ok(Json(serde_json::json!({ "status": "deleted" }))) } + +/// Cascade-delete a DAST target and all its downstream data. +async fn cascade_delete_dast_target(db: &crate::database::Database, target_id: &str) { + // Delete pentest sessions for this target (and their attack chains + messages) + if let Ok(mut cursor) = db + .pentest_sessions() + .find(doc! { "target_id": target_id }) + .await + { + use futures_util::StreamExt; + while let Some(Ok(session)) = cursor.next().await { + let session_id = session.id.map(|oid| oid.to_hex()).unwrap_or_default(); + if !session_id.is_empty() { + let _ = db + .attack_chain_nodes() + .delete_many(doc! { "session_id": &session_id }) + .await; + let _ = db + .pentest_messages() + .delete_many(doc! { "session_id": &session_id }) + .await; + let _ = db + .dast_findings() + .delete_many(doc! { "session_id": &session_id }) + .await; + } + } + } + let _ = db + .pentest_sessions() + .delete_many(doc! { "target_id": target_id }) + .await; + + // Delete DAST scan runs and their findings + let _ = db + .dast_findings() + .delete_many(doc! { "target_id": target_id }) + .await; + let _ = db + .dast_scan_runs() + .delete_many(doc! { "target_id": target_id }) + .await; + + // Delete the target itself + if let Ok(oid) = mongodb::bson::oid::ObjectId::parse_str(target_id) { + let _ = db.dast_targets().delete_one(doc! { "_id": oid }).await; + } +}