feat(matrix): PlcSps as a composite device target (SBOM/CVE/DAST/pentest)
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

A PLC/SPS device is two layers — the control application and the device it
runs on (firmware/OS + reachable runtime services). Previously PlcSps offered
only PlcControlLogic, so a CODESYS-on-Yocto device's firmware and network
attack surface was invisible.

- scan_matrix: PlcSps now offers FirmwareStatic + Sbom + Cve (require a firmware
  image) and Dast (require a running endpoint, e.g. WebVisu), and
  supports_pentest(PlcSps) is now true. Control-logic stays default-on; the
  device-level scans are offered but opt-in (default-off) — firmware-image
  SBOM/CVE execution is shared with the firmware families and still landing
  (#151/#128), so they must not silently auto-run.
- orchestrator: the PLC branch no longer early-returns, so a PLC device with a
  reachable endpoint also runs DAST (the wired path) after the control-logic scan.

Part of the CODESYS-on-Yocto coverage tracker (#167). Closes #164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-07-16 12:50:24 +02:00
co-authored by Claude Opus 4.8
parent afa5c985ee
commit 78cf3237d5
2 changed files with 118 additions and 22 deletions
+18 -11
View File
@@ -450,37 +450,44 @@ impl PipelineOrchestrator {
self.ensure_dast_target(target, &plan).await;
// PLC control-logic analysis for PLC/SPS targets (a PlcProject artifact).
// A PLC/SPS device is a composite target: after the control-logic scan we
// fall through so a reachable device (WebVisu / exposed services) still
// gets DAST, rather than early-returning on the PLC scan alone.
let mut new_count = 0u32;
if plan.has(ScanType::PlcControlLogic) {
return self.run_plc_scan(target, &target_id, scan_run_id).await;
new_count += self.run_plc_scan(target, &target_id, scan_run_id).await?;
}
match target.code_artifact() {
Some(code) if code.kind == ArtifactKind::GitRepo => {
let repo = RepoView::from_target(target, code);
let new_count = self.run_pipeline(&repo, scan_run_id).await?;
self.finalize_target(target, &repo, new_count).await?;
Ok(new_count)
let n = {
let repo = RepoView::from_target(target, code);
let n = self.run_pipeline(&repo, scan_run_id).await?;
self.finalize_target(target, &repo, n).await?;
n
};
new_count += n;
}
Some(_) => {
tracing::warn!(
target_id = %target_id,
"Unified pipeline: source-archive scanning not yet wired; skipping"
);
Ok(0)
}
None => {
// No code to scan. Firmware/PLC/mobile static scanners land in
// #128/#129/#130; DAST for a running URL still works when a
// DastTarget row exists (migrated targets).
// No code to scan (a PLC device or a migrated DAST target).
// Firmware/mobile static scanners land in #128/#129; DAST for a
// running URL works when a DastTarget row exists (provisioned above
// from a LiveUrl, or from a migrated target).
tracing::info!(
target_id = %target_id,
"Unified pipeline: no code artifact; attempting DAST only"
"Unified pipeline: no code artifact; attempting DAST"
);
self.update_phase(scan_run_id, "dast_scanning").await;
self.maybe_trigger_dast(&target_id, scan_run_id).await;
Ok(0)
}
}
Ok(new_count)
}
/// Analyze a PLC/SPS project (Structured Text / PLCopen XML) for