feat(plc): control-application SBOM from CODESYS .projectarchive (#170)
This commit was merged in pull request #170.
This commit is contained in:
@@ -503,11 +503,14 @@ impl PipelineOrchestrator {
|
||||
|
||||
let ctx = crate::ingest::IngestContext::from_config(&self.config, target_id);
|
||||
let ingest_set = crate::ingest::ingest_all(target, &ctx)?;
|
||||
let path = target
|
||||
.first_of(ArtifactKind::PlcProject)
|
||||
.and_then(|a| ingest_set.get(&a.id))
|
||||
.and_then(|ia| ia.working_path.clone());
|
||||
let Some(path) = path else {
|
||||
let Some(artifact) = target.first_of(ArtifactKind::PlcProject) else {
|
||||
tracing::warn!(target_id, "PLC scan: no PLC project 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 project path");
|
||||
return Ok(0);
|
||||
};
|
||||
@@ -533,9 +536,102 @@ impl PipelineOrchestrator {
|
||||
new_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Control-application dependency SBOM: the CODESYS libraries + runtime
|
||||
// bundled in a `.projectarchive`, matched against known CVEs. Best-effort
|
||||
// and empty for a bare `.st`/`.xml` (which carries no library manifest).
|
||||
let archive = artifact
|
||||
.stored_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| artifact.source_ref.clone());
|
||||
let sbom = crate::pipeline::plc::sbom::projectarchive_sbom(
|
||||
std::path::Path::new(&archive),
|
||||
target_id,
|
||||
);
|
||||
if !sbom.is_empty() {
|
||||
if let Err(e) = self.persist_control_app_sbom(target_id, sbom).await {
|
||||
tracing::warn!(target_id, error = %e, "control-app SBOM persist failed");
|
||||
}
|
||||
}
|
||||
Ok(new_count)
|
||||
}
|
||||
|
||||
/// Store a control-application SBOM (CODESYS libraries + runtime) for a target
|
||||
/// and match it against known CVEs. Scoped to `package_manager = "codesys"` so
|
||||
/// it refreshes on re-scan and coexists with any firmware/source SBOM. The
|
||||
/// runtime `Cmp*` / `3SLicense` components carry real CODESYS advisories, so
|
||||
/// this is where PLC-device CVE coverage comes from.
|
||||
async fn persist_control_app_sbom(
|
||||
&self,
|
||||
target_id: &str,
|
||||
mut entries: Vec<SbomEntry>,
|
||||
) -> Result<(), AgentError> {
|
||||
if entries.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
self.db
|
||||
.sbom_entries()
|
||||
.delete_many(doc! { "repo_id": target_id, "package_manager": "codesys" })
|
||||
.await?;
|
||||
|
||||
let cve_scanner = CveScanner::new(
|
||||
self.http.clone(),
|
||||
self.config.searxng_url.clone(),
|
||||
self.config.nvd_api_key.as_ref().map(|k| {
|
||||
use secrecy::ExposeSecret;
|
||||
k.expose_secret().to_string()
|
||||
}),
|
||||
);
|
||||
let alerts = match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(600),
|
||||
cve_scanner.scan_dependencies(target_id, &mut entries),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(a)) => a,
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!(target_id, error = %e, "control-app CVE scan failed");
|
||||
Vec::new()
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!(target_id, "control-app CVE scan timed out");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
for entry in &entries {
|
||||
let filter = doc! {
|
||||
"repo_id": &entry.repo_id,
|
||||
"name": &entry.name,
|
||||
"version": &entry.version,
|
||||
};
|
||||
if let Ok(d) = mongodb::bson::to_document(entry) {
|
||||
self.db
|
||||
.sbom_entries()
|
||||
.update_one(filter, doc! { "$set": d })
|
||||
.upsert(true)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
for alert in &alerts {
|
||||
let filter = doc! { "cve_id": &alert.cve_id, "repo_id": &alert.repo_id };
|
||||
if let Ok(d) = mongodb::bson::to_document(alert) {
|
||||
self.db
|
||||
.cve_alerts()
|
||||
.update_one(filter, doc! { "$set": d })
|
||||
.upsert(true)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
tracing::info!(
|
||||
target_id,
|
||||
components = entries.len(),
|
||||
alerts = alerts.len(),
|
||||
"control-app SBOM stored"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
Reference in New Issue
Block a user