feat(pipeline): PLC/SPS control-logic security scanner (IEC 61131-3) (#162)
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Successful in 3m48s
CI / Deploy Dashboard (push) Successful in 2m51s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2m2s
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Successful in 3m48s
CI / Deploy Dashboard (push) Successful in 2m51s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2m2s
This commit was merged in pull request #162.
This commit is contained in:
@@ -449,6 +449,11 @@ impl PipelineOrchestrator {
|
||||
// wizard-created targets, not just migrated ones.
|
||||
self.ensure_dast_target(target, &plan).await;
|
||||
|
||||
// PLC control-logic analysis for PLC/SPS targets (a PlcProject artifact).
|
||||
if plan.has(ScanType::PlcControlLogic) {
|
||||
return 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);
|
||||
@@ -478,6 +483,52 @@ impl PipelineOrchestrator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Analyze a PLC/SPS project (Structured Text / PLCopen XML) for
|
||||
/// control-logic security issues and persist the new findings.
|
||||
async fn run_plc_scan(
|
||||
&self,
|
||||
target: &OnboardedTarget,
|
||||
target_id: &str,
|
||||
scan_run_id: &str,
|
||||
) -> Result<u32, AgentError> {
|
||||
tracing::info!(target_id, "[{target_id}] PLC control-logic analysis");
|
||||
self.update_phase(scan_run_id, "plc_analysis").await;
|
||||
|
||||
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 {
|
||||
tracing::warn!(target_id, "PLC scan: no ingested PLC project path");
|
||||
return Ok(0);
|
||||
};
|
||||
|
||||
let findings = crate::pipeline::plc::analyze_tree(&path, target_id);
|
||||
tracing::info!(
|
||||
target_id,
|
||||
found = findings.len(),
|
||||
"PLC control-logic analysis complete"
|
||||
);
|
||||
|
||||
let mut new_count = 0u32;
|
||||
for mut finding in findings {
|
||||
finding.scan_run_id = Some(scan_run_id.to_string());
|
||||
if self
|
||||
.db
|
||||
.findings()
|
||||
.find_one(doc! { "fingerprint": &finding.fingerprint })
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
self.db.findings().insert_one(&finding).await?;
|
||||
new_count += 1;
|
||||
}
|
||||
}
|
||||
Ok(new_count)
|
||||
}
|
||||
|
||||
/// 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