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
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:
co-authored by
Claude Opus 4.8
parent
98357cae10
commit
34d6569d78
@@ -459,32 +459,57 @@ impl PipelineOrchestrator {
|
|||||||
|
|
||||||
let ctx = crate::ingest::IngestContext::from_config(&self.config, target_id);
|
let ctx = crate::ingest::IngestContext::from_config(&self.config, target_id);
|
||||||
let ingest_set = crate::ingest::ingest_all(target, &ctx)?;
|
let ingest_set = crate::ingest::ingest_all(target, &ctx)?;
|
||||||
// The PLC source: a dedicated PlcProject artifact, else a code artifact
|
// Every PLC-source artifact on the target: dedicated PLC projects plus any
|
||||||
// (git repo / source archive) holding PLCopen XML / ST exports.
|
// code artifacts (git repo / source archive) holding PLCopen XML / ST
|
||||||
let Some(artifact) = target
|
// exports. A target can carry several (e.g. one POU export per file).
|
||||||
.first_of(ArtifactKind::PlcProject)
|
let sources: Vec<&Artifact> = target
|
||||||
.or_else(|| target.code_artifact())
|
.artifacts
|
||||||
else {
|
.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");
|
tracing::warn!(target_id, "PLC scan: no PLC source artifact");
|
||||||
return Ok(0);
|
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!(
|
tracing::info!(
|
||||||
target_id,
|
target_id,
|
||||||
found = findings.len(),
|
artifacts = sources.len(),
|
||||||
|
found = all_findings.len(),
|
||||||
"PLC control-logic analysis complete"
|
"PLC control-logic analysis complete"
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut new_count = 0u32;
|
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());
|
finding.scan_run_id = Some(scan_run_id.to_string());
|
||||||
if self
|
if self
|
||||||
.db
|
.db
|
||||||
@@ -498,23 +523,9 @@ impl PipelineOrchestrator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Control-application dependency SBOM: the CODESYS libraries + runtime
|
if !all_sbom.is_empty() {
|
||||||
// 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 let Err(e) = self
|
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
|
.await
|
||||||
{
|
{
|
||||||
tracing::warn!(target_id, error = %e, "control-app SBOM persist failed");
|
tracing::warn!(target_id, error = %e, "control-app SBOM persist failed");
|
||||||
|
|||||||
Reference in New Issue
Block a user