CI / Check (pull_request) Successful in 5m29s
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
Onboarded targets are now the sole persisted entity. The legacy `TrackedRepository` model, the `repositories` collection, the `/repositories` API, the Repositories dashboard page, the one-shot migration, and the `UNIFIED_PIPELINE` transition flag are all removed. Net −1.7k LOC. Agent - New internal `pipeline::repo_view::RepoView` (non-persisted) replaces the `TrackedRepository` model; it's projected from an `OnboardedTarget` + its code artifact by `RepoView::from_target` (the old `repo_view_from_target`), so the scan/PR-review pipeline is byte-for-byte the same behaviour it already ran on the unified path — only the type's origin changed. - `run_scan` always runs the unified `run_target`; the legacy `orchestrator::run` and the `unified_pipeline` flag are gone. `run_pr_review` resolves the target from `onboarded_targets`. - Webhooks (github/gitea/gitlab), the CVE monitor, graph build, chat embeddings, health stats, and the pentest repo lookup all read `onboarded_targets`. - `delete_target` now cascades the full downstream set (findings, sbom, scans, cve, tracker issues, graph, embeddings, DAST targets + pentest sessions and their children) — matching the old repository delete. - `get_ssh_public_key` moved to the health handler; `repositories()` accessor, `repos.rs`, and `migrate/` deleted. Core - `TrackedRepository` removed; `ScanTrigger` stays. `unified_pipeline` config field removed. Dashboard - Repositories page + route deleted; overview / graph / chat / pentest-wizard read onboarded targets; `infrastructure/repositories.rs` trimmed to just the SSH-key fetch. Tests - Legacy repositories-API and migration integration tests removed; tenant isolation, cascade-delete, and stats tests repointed to `/targets` / `onboarded_targets`. `git.rs` gains a `sanitize_repo_dir` unit test. Local: fmt clean; agent/mcp clippy clean; dashboard server+web compile; core + agent lib tests (32) pass; integration tests compile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
220 lines
8.0 KiB
Rust
220 lines
8.0 KiB
Rust
use crate::common::TestServer;
|
|
use serde_json::json;
|
|
|
|
/// Insert a DAST target directly into MongoDB linked to a repo.
|
|
async fn insert_dast_target(server: &TestServer, repo_id: &str, name: &str) -> String {
|
|
let mongodb_uri = std::env::var("TEST_MONGODB_URI")
|
|
.unwrap_or_else(|_| "mongodb://root:example@localhost:27017/?authSource=admin".into());
|
|
let client = mongodb::Client::with_uri_str(&mongodb_uri).await.unwrap();
|
|
let db = client.database(&server.db_name());
|
|
|
|
let result = db
|
|
.collection::<mongodb::bson::Document>("dast_targets")
|
|
.insert_one(mongodb::bson::doc! {
|
|
"name": name,
|
|
"base_url": format!("https://{name}.example.com"),
|
|
"target_type": "webapp",
|
|
"repo_id": repo_id,
|
|
"rate_limit": 10,
|
|
"allow_destructive": false,
|
|
"created_at": mongodb::bson::DateTime::now(),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
result.inserted_id.as_object_id().unwrap().to_hex()
|
|
}
|
|
|
|
/// Insert a pentest session linked to a target.
|
|
async fn insert_pentest_session(server: &TestServer, target_id: &str, repo_id: &str) -> String {
|
|
let mongodb_uri = std::env::var("TEST_MONGODB_URI")
|
|
.unwrap_or_else(|_| "mongodb://root:example@localhost:27017/?authSource=admin".into());
|
|
let client = mongodb::Client::with_uri_str(&mongodb_uri).await.unwrap();
|
|
let db = client.database(&server.db_name());
|
|
|
|
let result = db
|
|
.collection::<mongodb::bson::Document>("pentest_sessions")
|
|
.insert_one(mongodb::bson::doc! {
|
|
"target_id": target_id,
|
|
"repo_id": repo_id,
|
|
"strategy": "comprehensive",
|
|
"status": "completed",
|
|
"findings_count": 1_i32,
|
|
"exploitable_count": 0_i32,
|
|
"created_at": mongodb::bson::DateTime::now(),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
result.inserted_id.as_object_id().unwrap().to_hex()
|
|
}
|
|
|
|
/// Insert an attack chain node linked to a session.
|
|
async fn insert_attack_node(server: &TestServer, session_id: &str) {
|
|
let mongodb_uri = std::env::var("TEST_MONGODB_URI")
|
|
.unwrap_or_else(|_| "mongodb://root:example@localhost:27017/?authSource=admin".into());
|
|
let client = mongodb::Client::with_uri_str(&mongodb_uri).await.unwrap();
|
|
let db = client.database(&server.db_name());
|
|
|
|
db.collection::<mongodb::bson::Document>("attack_chain_nodes")
|
|
.insert_one(mongodb::bson::doc! {
|
|
"session_id": session_id,
|
|
"node_id": "node-1",
|
|
"tool_name": "recon",
|
|
"status": "completed",
|
|
"created_at": mongodb::bson::DateTime::now(),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
/// Insert a DAST finding linked to a target.
|
|
async fn insert_dast_finding(server: &TestServer, target_id: &str, session_id: &str) {
|
|
let mongodb_uri = std::env::var("TEST_MONGODB_URI")
|
|
.unwrap_or_else(|_| "mongodb://root:example@localhost:27017/?authSource=admin".into());
|
|
let client = mongodb::Client::with_uri_str(&mongodb_uri).await.unwrap();
|
|
let db = client.database(&server.db_name());
|
|
|
|
db.collection::<mongodb::bson::Document>("dast_findings")
|
|
.insert_one(mongodb::bson::doc! {
|
|
"scan_run_id": "run-1",
|
|
"target_id": target_id,
|
|
"vuln_type": "xss",
|
|
"title": "Reflected XSS",
|
|
"description": "XSS in search param",
|
|
"severity": "high",
|
|
"endpoint": "https://example.com/search",
|
|
"method": "GET",
|
|
"exploitable": true,
|
|
"evidence": [],
|
|
"session_id": session_id,
|
|
"created_at": mongodb::bson::DateTime::now(),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
/// Helper to count documents in a collection
|
|
async fn count_docs(server: &TestServer, collection: &str) -> u64 {
|
|
let mongodb_uri = std::env::var("TEST_MONGODB_URI")
|
|
.unwrap_or_else(|_| "mongodb://root:example@localhost:27017/?authSource=admin".into());
|
|
let client = mongodb::Client::with_uri_str(&mongodb_uri).await.unwrap();
|
|
let db = client.database(&server.db_name());
|
|
db.collection::<mongodb::bson::Document>(collection)
|
|
.count_documents(mongodb::bson::doc! {})
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn delete_repo_cascades_to_dast_and_pentest_data() {
|
|
let server = TestServer::start().await;
|
|
|
|
// Create a repo
|
|
let resp = server
|
|
.post(
|
|
"/api/v1/targets",
|
|
&json!({
|
|
"name": "cascade-test",
|
|
"target_type": "web_app",
|
|
"artifacts": [{ "kind": "git_repo", "source_ref": "https://github.com/example/cascade-test.git", "branch": "main" }],
|
|
}),
|
|
)
|
|
.await;
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
let repo_id = body["data"]["_id"]["$oid"].as_str().unwrap().to_string();
|
|
|
|
// Insert DAST target linked to repo
|
|
let target_id = insert_dast_target(&server, &repo_id, "cascade-target").await;
|
|
|
|
// Insert pentest session linked to target
|
|
let session_id = insert_pentest_session(&server, &target_id, &repo_id).await;
|
|
|
|
// Insert downstream data
|
|
insert_attack_node(&server, &session_id).await;
|
|
insert_dast_finding(&server, &target_id, &session_id).await;
|
|
|
|
// Verify data exists
|
|
assert_eq!(count_docs(&server, "dast_targets").await, 1);
|
|
assert_eq!(count_docs(&server, "pentest_sessions").await, 1);
|
|
assert_eq!(count_docs(&server, "attack_chain_nodes").await, 1);
|
|
assert_eq!(count_docs(&server, "dast_findings").await, 1);
|
|
|
|
// Delete the repo
|
|
let resp = server.delete(&format!("/api/v1/targets/{repo_id}")).await;
|
|
assert_eq!(resp.status(), 200);
|
|
|
|
// All downstream data should be gone
|
|
assert_eq!(count_docs(&server, "dast_targets").await, 0);
|
|
assert_eq!(count_docs(&server, "pentest_sessions").await, 0);
|
|
assert_eq!(count_docs(&server, "attack_chain_nodes").await, 0);
|
|
assert_eq!(count_docs(&server, "dast_findings").await, 0);
|
|
|
|
server.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn delete_repo_cascades_sast_findings_and_sbom() {
|
|
let server = TestServer::start().await;
|
|
|
|
// Create a repo
|
|
let resp = server
|
|
.post(
|
|
"/api/v1/targets",
|
|
&json!({
|
|
"name": "sast-cascade",
|
|
"target_type": "web_app",
|
|
"artifacts": [{ "kind": "git_repo", "source_ref": "https://github.com/example/sast-cascade.git", "branch": "main" }],
|
|
}),
|
|
)
|
|
.await;
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
let repo_id = body["data"]["_id"]["$oid"].as_str().unwrap().to_string();
|
|
|
|
// Insert SAST finding and SBOM entry
|
|
let mongodb_uri = std::env::var("TEST_MONGODB_URI")
|
|
.unwrap_or_else(|_| "mongodb://root:example@localhost:27017/?authSource=admin".into());
|
|
let client = mongodb::Client::with_uri_str(&mongodb_uri).await.unwrap();
|
|
let db = client.database(&server.db_name());
|
|
let now = mongodb::bson::DateTime::now();
|
|
|
|
db.collection::<mongodb::bson::Document>("findings")
|
|
.insert_one(mongodb::bson::doc! {
|
|
"repo_id": &repo_id,
|
|
"fingerprint": "fp-test-1",
|
|
"scanner": "semgrep",
|
|
"scan_type": "sast",
|
|
"title": "SQL Injection",
|
|
"description": "desc",
|
|
"severity": "critical",
|
|
"status": "open",
|
|
"created_at": now,
|
|
"updated_at": now,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
db.collection::<mongodb::bson::Document>("sbom_entries")
|
|
.insert_one(mongodb::bson::doc! {
|
|
"repo_id": &repo_id,
|
|
"name": "lodash",
|
|
"version": "4.17.20",
|
|
"package_manager": "npm",
|
|
"known_vulnerabilities": [],
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(count_docs(&server, "findings").await, 1);
|
|
assert_eq!(count_docs(&server, "sbom_entries").await, 1);
|
|
|
|
// Delete repo
|
|
server.delete(&format!("/api/v1/targets/{repo_id}")).await;
|
|
|
|
// Both should be gone
|
|
assert_eq!(count_docs(&server, "findings").await, 0);
|
|
assert_eq!(count_docs(&server, "sbom_entries").await, 0);
|
|
|
|
server.cleanup().await;
|
|
}
|