fix(plc): scan every PLC-source artifact, not just the first
CI / Check (pull_request) Successful in 5m22s
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

run_plc_scan analyzed only the first PlcProject (or single code artifact), so a
target with several PLC artifacts (e.g. one PLCopen XML export per POU) had the
rest silently ignored. Found live: a target with pump_station.st + pump_fbd.xml
only produced findings for the first.

Iterate over every PlcProject / git repo / source-archive artifact on the target,
merge the control-logic findings, and union the control-app SBOM (deduped by
name+version) across all of them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-07-16 21:54:05 +02:00
co-authored by Claude Opus 4.8
parent 98357cae10
commit 34d6569d78
+44 -33
View File
@@ -459,32 +459,57 @@ impl PipelineOrchestrator {
let ctx = crate::ingest::IngestContext::from_config(&self.config, target_id);
let ingest_set = crate::ingest::ingest_all(target, &ctx)?;
// The PLC source: a dedicated PlcProject artifact, else a code artifact
// (git repo / source archive) holding PLCopen XML / ST exports.
let Some(artifact) = target
.first_of(ArtifactKind::PlcProject)
.or_else(|| target.code_artifact())
else {
// Every PLC-source artifact on the target: dedicated PLC projects plus any
// code artifacts (git repo / source archive) holding PLCopen XML / ST
// exports. A target can carry several (e.g. one POU export per file).
let sources: Vec<&Artifact> = target
.artifacts
.iter()
.filter(|a| {
matches!(
a.kind,
ArtifactKind::PlcProject | ArtifactKind::GitRepo | ArtifactKind::SourceArchive
)
})
.collect();
if sources.is_empty() {
tracing::warn!(target_id, "PLC scan: no PLC source artifact");
return Ok(0);
};
let Some(path) = ingest_set
.get(&artifact.id)
.and_then(|ia| ia.working_path.clone())
else {
tracing::warn!(target_id, "PLC scan: no ingested PLC source path");
return Ok(0);
};
}
let findings = crate::pipeline::plc::analyze_tree(&path, target_id);
let mut all_findings = Vec::new();
let mut all_sbom: Vec<SbomEntry> = Vec::new();
let mut sbom_seen = std::collections::BTreeSet::new();
for a in &sources {
let Some(path) = ingest_set.get(&a.id).and_then(|ia| ia.working_path.clone()) else {
continue;
};
all_findings.extend(crate::pipeline::plc::analyze_tree(&path, target_id));
// Control-application SBOM: CODESYS libraries + runtime from a
// `.projectarchive` (uploaded, or committed in the working tree).
let archive = a
.stored_path
.clone()
.unwrap_or_else(|| a.source_ref.clone());
for e in crate::pipeline::plc::sbom::collect_sbom(
std::path::Path::new(&archive),
&path,
target_id,
) {
if sbom_seen.insert((e.name.clone(), e.version.clone())) {
all_sbom.push(e);
}
}
}
tracing::info!(
target_id,
found = findings.len(),
artifacts = sources.len(),
found = all_findings.len(),
"PLC control-logic analysis complete"
);
let mut new_count = 0u32;
for mut finding in findings {
for mut finding in all_findings {
finding.scan_run_id = Some(scan_run_id.to_string());
if self
.db
@@ -498,23 +523,9 @@ impl PipelineOrchestrator {
}
}
// Control-application dependency SBOM: the CODESYS libraries + runtime
// bundled in a `.projectarchive`, matched against known CVEs. Sourced from
// the uploaded archive *and* any `.projectarchive` committed in the working
// tree (e.g. a git repo). Empty for a bare `.st`/`.xml` or a repo of only
// PLCopen XML exports (which carry no library manifest).
let archive = artifact
.stored_path
.clone()
.unwrap_or_else(|| artifact.source_ref.clone());
let sbom = crate::pipeline::plc::sbom::collect_sbom(
std::path::Path::new(&archive),
&path,
target_id,
);
if !sbom.is_empty() {
if !all_sbom.is_empty() {
if let Err(e) = self
.persist_control_app_sbom(target_id, &target.name, sbom)
.persist_control_app_sbom(target_id, &target.name, all_sbom)
.await
{
tracing::warn!(target_id, error = %e, "control-app SBOM persist failed");