diff --git a/compliance-agent/src/api/handlers/onboarding.rs b/compliance-agent/src/api/handlers/onboarding.rs index 860d3ad..6784353 100644 --- a/compliance-agent/src/api/handlers/onboarding.rs +++ b/compliance-agent/src/api/handlers/onboarding.rs @@ -348,3 +348,43 @@ pub async fn detect_target( page: None, })) } + +/// POST /api/v1/targets/{id}/scan — trigger a scan for the target. +/// +/// Dispatches to the unified pipeline when `UNIFIED_PIPELINE` is set (else the +/// legacy path). Runs in the background and returns immediately. +#[tracing::instrument(skip_all, fields(target_id = %id))] +pub async fn trigger_target_scan( + Extension(agent): AgentExt, + tenant: TenantCtx, + Path(id): Path, +) -> Result, StatusCode> { + let oid = parse_oid(&id)?; + let db = tenant_db(&agent, &tenant).await?; + // 404 if the target doesn't exist for this tenant. + if db + .onboarded_targets() + .find_one(doc! { "_id": oid }) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .is_none() + { + return Err(StatusCode::NOT_FOUND); + } + + let agent_clone = (*agent).clone(); + let tenant_id = tenant.0.tenant_id.clone(); + tokio::spawn(async move { + if let Err(e) = agent_clone + .run_scan( + &tenant_id, + &id, + compliance_core::models::ScanTrigger::Manual, + ) + .await + { + tracing::error!("Manual target scan failed for {id}: {e}"); + } + }); + Ok(Json(serde_json::json!({ "status": "scan_triggered" }))) +} diff --git a/compliance-agent/src/api/routes.rs b/compliance-agent/src/api/routes.rs index a067366..3f1cf07 100644 --- a/compliance-agent/src/api/routes.rs +++ b/compliance-agent/src/api/routes.rs @@ -48,6 +48,10 @@ pub fn build_router() -> Router { "/api/v1/targets/{id}/detect", post(handlers::onboarding::detect_target), ) + .route( + "/api/v1/targets/{id}/scan", + post(handlers::onboarding::trigger_target_scan), + ) .route("/api/v1/findings", get(handlers::list_findings)) .route("/api/v1/findings/{id}", get(handlers::get_finding)) .route( diff --git a/compliance-dashboard/src/infrastructure/onboarding.rs b/compliance-dashboard/src/infrastructure/onboarding.rs index 09c2a05..df2380b 100644 --- a/compliance-dashboard/src/infrastructure/onboarding.rs +++ b/compliance-dashboard/src/infrastructure/onboarding.rs @@ -105,3 +105,19 @@ pub async fn fetch_applicable_scans(id: String) -> Result Result { + let resp = super::agent_client::agent_request( + reqwest::Method::POST, + &format!("/api/v1/targets/{id}/scan"), + ) + .await? + .send() + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + resp.json() + .await + .map_err(|e| ServerFnError::new(e.to_string())) +} diff --git a/compliance-dashboard/src/pages/onboarding.rs b/compliance-dashboard/src/pages/onboarding.rs index ce18faa..5e8aaf6 100644 --- a/compliance-dashboard/src/pages/onboarding.rs +++ b/compliance-dashboard/src/pages/onboarding.rs @@ -2,7 +2,7 @@ use dioxus::prelude::*; use crate::components::page_header::PageHeader; use crate::infrastructure::onboarding::{ - create_target, detect_target, fetch_applicable_scans, ArtifactInputDto, + create_target, detect_target, fetch_applicable_scans, trigger_target_scan, ArtifactInputDto, }; /// (value, label, one-line description) for the 9 target families. @@ -113,6 +113,8 @@ pub fn OnboardingPage() -> Element { let mut error = use_signal(|| Option::::None); let mut scans = use_signal(Vec::::new); let mut suggested = use_signal(|| Option::::None); + let mut created_id = use_signal(|| Option::::None); + let mut scan_msg = use_signal(|| Option::::None); let step_now = step(); let can_advance_type = !name().trim().is_empty() && !target_type().trim().is_empty(); @@ -290,7 +292,27 @@ pub fn OnboardingPage() -> Element { ScanRow { scan: s } } } - div { style: "margin-top: 16px;", + if let Some(msg) = scan_msg() { + div { style: "margin-top: 8px; color: var(--success, #2a2);", "{msg}" } + } + div { style: "margin-top: 16px; display: flex; gap: 8px;", + button { + class: "btn btn-primary", + onclick: move |_| { + if let Some(id) = created_id() { + scan_msg.set(Some("Scan triggered...".to_string())); + spawn(async move { + match trigger_target_scan(id).await { + Ok(_) => scan_msg.set(Some( + "Scan started — findings will appear as it runs.".to_string(), + )), + Err(e) => scan_msg.set(Some(format!("Failed to start scan: {e}"))), + } + }); + } + }, + "Run scan" + } button { class: "btn btn-secondary", onclick: move |_| { @@ -301,6 +323,8 @@ pub fn OnboardingPage() -> Element { artifacts.write().clear(); scans.write().clear(); suggested.set(None); + created_id.set(None); + scan_msg.set(None); error.set(None); }, "Onboard another" @@ -348,6 +372,7 @@ pub fn OnboardingPage() -> Element { .and_then(|s| s.as_str()) .map(String::from); if let Some(id) = id { + created_id.set(Some(id.clone())); if let Ok(sc) = fetch_applicable_scans(id.clone()).await { scans.set(sc.data.scans); }