From 1ab443457e18ea0920e812f16da7cf73e83569bc Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:35:55 +0200 Subject: [PATCH] feat(pipeline): run ingest+classify (tramiton) and provision DAST in run_target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the built-but-unused classification + DAST paths into the unified scan so onboarded targets are actually detected and dynamically scanned: - classify_and_store: ingests the target's artifacts and runs classify_target (tramiton-core in-process for firmware/RTOS/Yocto + MCU/board; heuristics for the rest), storing the Classification on the target. Best-effort — never fails a scan. This is what makes firmware detection real during a scan. - ensure_dast_target: when DAST is planned and the target has a LiveUrl artifact, provisions a DastTarget (repo_id = target id) so the existing DAST trigger fires for wizard-created targets, not just migrated ones. Idempotent. Both run inside run_target (behind UNIFIED_PIPELINE), before the code pipeline. Note: classification ingest currently clones the git repo separately from the scan pipeline's clone (double-clone) — a perf follow-up, not a correctness issue. Refs #133, #135, #118. Co-Authored-By: Claude Fable 5 --- compliance-agent/src/pipeline/orchestrator.rs | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/compliance-agent/src/pipeline/orchestrator.rs b/compliance-agent/src/pipeline/orchestrator.rs index d3c6da0..4da7a3e 100644 --- a/compliance-agent/src/pipeline/orchestrator.rs +++ b/compliance-agent/src/pipeline/orchestrator.rs @@ -498,6 +498,13 @@ impl PipelineOrchestrator { "Unified pipeline: scan plan built" ); + // Ingest + classify (tramiton for firmware) and store the detected type. + self.classify_and_store(target, &target_id, scan_run_id) + .await; + // Provision a DAST target from a LiveUrl artifact so DAST fires for + // wizard-created targets, not just migrated ones. + self.ensure_dast_target(target, &plan).await; + match target.code_artifact() { Some(code) if code.kind == ArtifactKind::GitRepo => { let repo = repo_view_from_target(target, code); @@ -527,6 +534,110 @@ impl PipelineOrchestrator { } } + /// Ingest the target's artifacts, classify (tramiton for firmware/RTOS/Yocto, + /// heuristics otherwise), and store the detected classification on the target. + /// Best-effort — never fails the scan. + async fn classify_and_store( + &self, + target: &OnboardedTarget, + target_id: &str, + scan_run_id: &str, + ) { + self.update_phase(scan_run_id, "classification").await; + let ctx = crate::ingest::IngestContext::from_config(&self.config, target_id); + let ingest_set = match crate::ingest::ingest_all(target, &ctx) { + Ok(set) => set, + Err(e) => { + tracing::warn!(target_id, error = %e, "Unified pipeline: ingest for classification failed"); + return; + } + }; + let working_paths = ingest_set.working_paths(); + match crate::classify::classify_target( + target, + &working_paths, + &crate::classify::TramitonNative, + ) + .await + { + Ok(classification) => { + tracing::info!( + target_id, + suggested = %classification.suggested, + "Unified pipeline: classified target" + ); + if let (Some(oid), Ok(bson)) = (target.id, mongodb::bson::to_bson(&classification)) + { + let _ = self + .db + .onboarded_targets() + .update_one( + doc! { "_id": oid }, + doc! { "$set": { "classification": bson } }, + ) + .await; + } + } + Err(e) => { + tracing::warn!(target_id, error = %e, "Unified pipeline: classification failed") + } + } + } + + /// If the target has a `LiveUrl` artifact and DAST is planned, provision a + /// `DastTarget` (keyed by `repo_id` = target id) so the existing DAST trigger + /// fires for wizard-created targets. Idempotent. + async fn ensure_dast_target( + &self, + target: &OnboardedTarget, + plan: &crate::pipeline::plan::ScanPlan, + ) { + if !plan.has(ScanType::Dast) { + return; + } + let (Some(url), Some(oid)) = (target.live_url(), target.id) else { + return; + }; + let target_id = oid.to_hex(); + if self + .db + .dast_targets() + .find_one(doc! { "repo_id": &target_id }) + .await + .ok() + .flatten() + .is_some() + { + return; // already provisioned + } + let kind = url + .web + .as_ref() + .map(|w| w.target_kind.clone()) + .unwrap_or(DastTargetType::WebApp); + let mut dast = DastTarget::new(target.name.clone(), url.source_ref.clone(), kind); + dast.repo_id = Some(target_id); + if let Some(web) = &url.web { + dast.excluded_paths = web.excluded_paths.clone(); + dast.max_crawl_depth = web.max_crawl_depth; + dast.rate_limit = web.rate_limit; + dast.allow_destructive = web.allow_destructive; + } + if let Some(auth) = &url.auth { + dast.auth_config = Some(DastAuthConfig { + method: auth.method.clone(), + login_url: auth.login_url.clone(), + username: auth.username.clone(), + password: None, + token: auth.secret.clone(), + headers: auth.headers.clone(), + }); + } + if let Err(e) = self.db.dast_targets().insert_one(&dast).await { + tracing::warn!(error = %e, "Unified pipeline: failed to provision DAST target"); + } + } + /// Sync the onboarded-target document after a scan: bump `findings_count` /// and advance the git artifact's `last_scanned_commit` watermark. async fn finalize_target( -- 2.54.0