CI / Check (pull_request) Successful in 6m14s
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
REST surface the wizard drives onboarding through:
- GET/POST /api/v1/targets (list paginated / create)
- GET/PATCH/DELETE /api/v1/targets/{id} (fetch / update / delete + cascade
findings/scans by repo_id)
- POST /api/v1/targets/{id}/artifacts (attach an artifact by reference)
- GET /api/v1/targets/{id}/applicable-scans (scan matrix + pentest support)
- POST /api/v1/targets/{id}/detect (kind-based classification — no ingest;
deep post-ingest detection is a follow-up)
Follows the existing axum handler/route/DTO conventions (ApiResponse,
PaginationParams, tenant_db, collect_cursor_async, TenantCtx). Multipart artifact
upload and the scan-trigger endpoint (needs the unified pipeline, #133) are
follow-ups.
Also fixes the TestServer db-name prefix (was 37 chars, over the pool's 30-char
cap) which had silently broken every API integration test locally.
3 HTTP integration tests (create/list/applicable-scans, detect->PLC,
add-artifact/delete) — pass against local Mongo.
Closes #131.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
116 lines
4.0 KiB
Rust
116 lines
4.0 KiB
Rust
use crate::common::TestServer;
|
|
use serde_json::json;
|
|
|
|
#[tokio::test]
|
|
async fn create_list_and_applicable_scans() {
|
|
let server = TestServer::start().await;
|
|
|
|
// Initially empty.
|
|
let resp = server.get("/api/v1/targets").await;
|
|
assert_eq!(resp.status(), 200);
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
assert_eq!(body["data"].as_array().unwrap().len(), 0);
|
|
|
|
// Create a web-app target with a git repo + a live URL.
|
|
let resp = server
|
|
.post(
|
|
"/api/v1/targets",
|
|
&json!({
|
|
"name": "acme-web",
|
|
"target_type": "web_app",
|
|
"artifacts": [
|
|
{ "kind": "git_repo", "source_ref": "https://git/acme.git", "branch": "main" },
|
|
{ "kind": "live_url", "source_ref": "https://acme.example.com" }
|
|
]
|
|
}),
|
|
)
|
|
.await;
|
|
assert_eq!(resp.status(), 200);
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
let id = body["data"]["_id"]["$oid"].as_str().unwrap().to_string();
|
|
assert!(!id.is_empty());
|
|
assert_eq!(body["data"]["artifacts"].as_array().unwrap().len(), 2);
|
|
|
|
// List returns it.
|
|
let resp = server.get("/api/v1/targets").await;
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
assert_eq!(body["data"].as_array().unwrap().len(), 1);
|
|
|
|
// Applicable scans: SAST present + DAST offered (live URL present), pentest supported.
|
|
let resp = server
|
|
.get(&format!("/api/v1/targets/{id}/applicable-scans"))
|
|
.await;
|
|
assert_eq!(resp.status(), 200);
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
let scans = body["data"]["scans"].as_array().unwrap();
|
|
let names: Vec<&str> = scans.iter().filter_map(|s| s["scan"].as_str()).collect();
|
|
assert!(names.contains(&"sast"));
|
|
assert!(names.contains(&"dast"));
|
|
assert_eq!(body["data"]["pentest_supported"], true);
|
|
|
|
server.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn detect_classifies_a_plc_target() {
|
|
let server = TestServer::start().await;
|
|
|
|
// A PLC project artifact is a strong kind-based signal.
|
|
let resp = server
|
|
.post(
|
|
"/api/v1/targets",
|
|
&json!({
|
|
"name": "line-controller",
|
|
"target_type": "backend_service", // deliberately wrong; detect should suggest PLC
|
|
"artifacts": [
|
|
{ "kind": "plc_project", "source_ref": "line.xml", "plc_format": "plcopen_xml" }
|
|
]
|
|
}),
|
|
)
|
|
.await;
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
let id = body["data"]["_id"]["$oid"].as_str().unwrap().to_string();
|
|
|
|
let resp = server
|
|
.post(&format!("/api/v1/targets/{id}/detect"), &json!({}))
|
|
.await;
|
|
assert_eq!(resp.status(), 200);
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
assert_eq!(body["data"]["classification"]["suggested"], "plc_sps");
|
|
|
|
server.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn add_artifact_and_delete_target() {
|
|
let server = TestServer::start().await;
|
|
|
|
let resp = server
|
|
.post(
|
|
"/api/v1/targets",
|
|
&json!({ "name": "svc", "target_type": "backend_service" }),
|
|
)
|
|
.await;
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
let id = body["data"]["_id"]["$oid"].as_str().unwrap().to_string();
|
|
|
|
// Attach a git repo.
|
|
let resp = server
|
|
.post(
|
|
&format!("/api/v1/targets/{id}/artifacts"),
|
|
&json!({ "kind": "git_repo", "source_ref": "https://git/svc.git" }),
|
|
)
|
|
.await;
|
|
assert_eq!(resp.status(), 200);
|
|
let body: serde_json::Value = resp.json().await.unwrap();
|
|
assert_eq!(body["data"]["artifacts"].as_array().unwrap().len(), 1);
|
|
|
|
// Delete it.
|
|
let resp = server.delete(&format!("/api/v1/targets/{id}")).await;
|
|
assert_eq!(resp.status(), 200);
|
|
let resp = server.get(&format!("/api/v1/targets/{id}")).await;
|
|
assert_eq!(resp.status(), 404);
|
|
|
|
server.cleanup().await;
|
|
}
|