Compare commits

..
Author SHA1 Message Date
Sharang ParnerkarandClaude Fable 5 1ab443457e feat(pipeline): run ingest+classify (tramiton) and provision DAST in run_target
CI / Check (pull_request) Successful in 5m34s
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
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 <noreply@anthropic.com>
2026-07-12 23:35:55 +02:00
7 changed files with 6 additions and 115 deletions
+2 -8
View File
@@ -217,16 +217,13 @@ jobs:
image: docker:27-cli image: docker:27-cli
steps: steps:
- name: Build, push and trigger orca redeploy - name: Build, push and trigger orca redeploy
env:
TRAMITON_FETCH_TOKEN: ${{ secrets.TRAMITON_FETCH_TOKEN }}
run: | run: |
apk add --no-cache git curl openssl apk add --no-cache git curl openssl
git init && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" git init && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git fetch --depth=1 origin "${GITHUB_SHA}" && git checkout FETCH_HEAD git fetch --depth=1 origin "${GITHUB_SHA}" && git checkout FETCH_HEAD
IMAGE=registry.meghsakha.com/compliance-dashboard IMAGE=registry.meghsakha.com/compliance-dashboard
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.meghsakha.com -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.meghsakha.com -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
DOCKER_BUILDKIT=1 docker build --secret id=tramiton_token,env=TRAMITON_FETCH_TOKEN \ docker build -f Dockerfile.dashboard -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" .
-f Dockerfile.dashboard -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" .
docker push "$IMAGE:latest" && docker push "$IMAGE:${GITHUB_SHA}" docker push "$IMAGE:latest" && docker push "$IMAGE:${GITHUB_SHA}"
PAYLOAD=$(printf '{"ref":"refs/heads/main","repository":{"full_name":"sharang/compliance-scanner-agent"},"head_commit":{"id":"%s","message":"deploy dashboard"}}' "${GITHUB_SHA}") PAYLOAD=$(printf '{"ref":"refs/heads/main","repository":{"full_name":"sharang/compliance-scanner-agent"},"head_commit":{"id":"%s","message":"deploy dashboard"}}' "${GITHUB_SHA}")
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "${{ secrets.ORCA_WEBHOOK_SECRET }}" | awk '{print $2}') SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "${{ secrets.ORCA_WEBHOOK_SECRET }}" | awk '{print $2}')
@@ -262,16 +259,13 @@ jobs:
image: docker:27-cli image: docker:27-cli
steps: steps:
- name: Build, push and trigger orca redeploy - name: Build, push and trigger orca redeploy
env:
TRAMITON_FETCH_TOKEN: ${{ secrets.TRAMITON_FETCH_TOKEN }}
run: | run: |
apk add --no-cache git curl openssl apk add --no-cache git curl openssl
git init && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" git init && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git fetch --depth=1 origin "${GITHUB_SHA}" && git checkout FETCH_HEAD git fetch --depth=1 origin "${GITHUB_SHA}" && git checkout FETCH_HEAD
IMAGE=registry.meghsakha.com/compliance-mcp IMAGE=registry.meghsakha.com/compliance-mcp
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.meghsakha.com -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.meghsakha.com -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
DOCKER_BUILDKIT=1 docker build --secret id=tramiton_token,env=TRAMITON_FETCH_TOKEN \ docker build -f Dockerfile.mcp -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" .
-f Dockerfile.mcp -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" .
docker push "$IMAGE:latest" && docker push "$IMAGE:${GITHUB_SHA}" docker push "$IMAGE:latest" && docker push "$IMAGE:${GITHUB_SHA}"
PAYLOAD=$(printf '{"ref":"refs/heads/main","repository":{"full_name":"sharang/compliance-scanner-agent"},"head_commit":{"id":"%s","message":"deploy mcp"}}' "${GITHUB_SHA}") PAYLOAD=$(printf '{"ref":"refs/heads/main","repository":{"full_name":"sharang/compliance-scanner-agent"},"head_commit":{"id":"%s","message":"deploy mcp"}}' "${GITHUB_SHA}")
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "${{ secrets.ORCA_WEBHOOK_SECRET }}" | awk '{print $2}') SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "${{ secrets.ORCA_WEBHOOK_SECRET }}" | awk '{print $2}')
+1 -10
View File
@@ -7,16 +7,7 @@ ARG DOCS_URL=/docs
WORKDIR /app WORKDIR /app
COPY . . COPY . .
ENV DOCS_URL=${DOCS_URL} ENV DOCS_URL=${DOCS_URL}
# compliance-agent (a workspace member) depends on the private tramiton-core git RUN dx build --release --package compliance-dashboard
# repo, so the workspace resolve needs it even to build the dashboard.
# Authenticate the fetch with a PAT passed as a BuildKit secret.
RUN --mount=type=secret,id=tramiton_token \
if [ -s /run/secrets/tramiton_token ]; then \
git config --global \
url."https://sharang:$(cat /run/secrets/tramiton_token)@gitea.meghsakha.com/".insteadOf \
"ssh://git@gitea.meghsakha.com:22222/"; \
fi && \
CARGO_NET_GIT_FETCH_WITH_CLI=true dx build --release --package compliance-dashboard
FROM debian:bookworm-slim FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
+1 -10
View File
@@ -2,16 +2,7 @@ FROM rust:1.94-bookworm AS builder
WORKDIR /app WORKDIR /app
COPY . . COPY . .
# compliance-agent (a workspace member) depends on the private tramiton-core git RUN cargo build --release -p compliance-mcp
# repo, so the workspace resolve needs it even to build the mcp binary.
# Authenticate the fetch with a PAT passed as a BuildKit secret.
RUN --mount=type=secret,id=tramiton_token \
if [ -s /run/secrets/tramiton_token ]; then \
git config --global \
url."https://sharang:$(cat /run/secrets/tramiton_token)@gitea.meghsakha.com/".insteadOf \
"ssh://git@gitea.meghsakha.com:22222/"; \
fi && \
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release -p compliance-mcp
FROM debian:bookworm-slim FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
@@ -348,43 +348,3 @@ pub async fn detect_target(
page: None, 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<String>,
) -> Result<Json<serde_json::Value>, 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" })))
}
-4
View File
@@ -48,10 +48,6 @@ pub fn build_router() -> Router {
"/api/v1/targets/{id}/detect", "/api/v1/targets/{id}/detect",
post(handlers::onboarding::detect_target), 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", get(handlers::list_findings))
.route("/api/v1/findings/{id}", get(handlers::get_finding)) .route("/api/v1/findings/{id}", get(handlers::get_finding))
.route( .route(
@@ -105,19 +105,3 @@ pub async fn fetch_applicable_scans(id: String) -> Result<ApplicableScansRespons
.await .await
.map_err(|e| ServerFnError::new(e.to_string())) .map_err(|e| ServerFnError::new(e.to_string()))
} }
/// Trigger a scan for a target.
#[server]
pub async fn trigger_target_scan(id: String) -> Result<serde_json::Value, ServerFnError> {
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()))
}
+2 -27
View File
@@ -2,7 +2,7 @@ use dioxus::prelude::*;
use crate::components::page_header::PageHeader; use crate::components::page_header::PageHeader;
use crate::infrastructure::onboarding::{ use crate::infrastructure::onboarding::{
create_target, detect_target, fetch_applicable_scans, trigger_target_scan, ArtifactInputDto, create_target, detect_target, fetch_applicable_scans, ArtifactInputDto,
}; };
/// (value, label, one-line description) for the 9 target families. /// (value, label, one-line description) for the 9 target families.
@@ -113,8 +113,6 @@ pub fn OnboardingPage() -> Element {
let mut error = use_signal(|| Option::<String>::None); let mut error = use_signal(|| Option::<String>::None);
let mut scans = use_signal(Vec::<serde_json::Value>::new); let mut scans = use_signal(Vec::<serde_json::Value>::new);
let mut suggested = use_signal(|| Option::<String>::None); let mut suggested = use_signal(|| Option::<String>::None);
let mut created_id = use_signal(|| Option::<String>::None);
let mut scan_msg = use_signal(|| Option::<String>::None);
let step_now = step(); let step_now = step();
let can_advance_type = !name().trim().is_empty() && !target_type().trim().is_empty(); let can_advance_type = !name().trim().is_empty() && !target_type().trim().is_empty();
@@ -292,27 +290,7 @@ pub fn OnboardingPage() -> Element {
ScanRow { scan: s } ScanRow { scan: s }
} }
} }
if let Some(msg) = scan_msg() { div { style: "margin-top: 16px;",
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 { button {
class: "btn btn-secondary", class: "btn btn-secondary",
onclick: move |_| { onclick: move |_| {
@@ -323,8 +301,6 @@ pub fn OnboardingPage() -> Element {
artifacts.write().clear(); artifacts.write().clear();
scans.write().clear(); scans.write().clear();
suggested.set(None); suggested.set(None);
created_id.set(None);
scan_msg.set(None);
error.set(None); error.set(None);
}, },
"Onboard another" "Onboard another"
@@ -372,7 +348,6 @@ pub fn OnboardingPage() -> Element {
.and_then(|s| s.as_str()) .and_then(|s| s.as_str())
.map(String::from); .map(String::from);
if let Some(id) = id { if let Some(id) = id {
created_id.set(Some(id.clone()));
if let Ok(sc) = fetch_applicable_scans(id.clone()).await { if let Ok(sc) = fetch_applicable_scans(id.clone()).await {
scans.set(sc.data.scans); scans.set(sc.data.scans);
} }