Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ae5d9a07f | ||
|
|
5bdc35ee92 | ||
|
|
ea516cc054 |
@@ -277,8 +277,10 @@ impl PipelineOrchestrator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dedup against existing findings and insert new ones
|
// Dedup against existing findings: insert first-seen ones, and refresh the
|
||||||
|
// control mappings on ones we've seen before.
|
||||||
let mut new_count = 0u32;
|
let mut new_count = 0u32;
|
||||||
|
let mut refreshed_count = 0u32;
|
||||||
let mut new_findings: Vec<Finding> = Vec::new();
|
let mut new_findings: Vec<Finding> = Vec::new();
|
||||||
for mut finding in all_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());
|
||||||
@@ -293,8 +295,25 @@ impl PipelineOrchestrator {
|
|||||||
finding.id = result.inserted_id.as_object_id();
|
finding.id = result.inserted_id.as_object_id();
|
||||||
new_findings.push(finding);
|
new_findings.push(finding);
|
||||||
new_count += 1;
|
new_count += 1;
|
||||||
|
} else if !finding.control_refs.is_empty() {
|
||||||
|
// Re-scan refresh: a mapping pass (newly enabled or tuned) computed
|
||||||
|
// control_refs for a finding first seen before mapping ran. Persist
|
||||||
|
// them onto the existing row — the insert path alone never would.
|
||||||
|
self.db
|
||||||
|
.findings()
|
||||||
|
.update_one(
|
||||||
|
doc! { "fingerprint": &finding.fingerprint },
|
||||||
|
doc! { "$set": { "control_refs": finding.control_refs.clone() } },
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
refreshed_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if refreshed_count > 0 {
|
||||||
|
tracing::info!(
|
||||||
|
"[{repo_id}] Refreshed control_refs on {refreshed_count} existing findings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Remove stale SBOM entries for this repo before reinserting
|
// Remove stale SBOM entries for this repo before reinserting
|
||||||
if !sbom_entries.is_empty() {
|
if !sbom_entries.is_empty() {
|
||||||
@@ -567,7 +586,21 @@ impl PipelineOrchestrator {
|
|||||||
let Some(path) = ingest_set.get(&a.id).and_then(|ia| ia.working_path.clone()) else {
|
let Some(path) = ingest_set.get(&a.id).and_then(|ia| ia.working_path.clone()) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
all_findings.extend(crate::pipeline::plc::analyze_tree(&path, target_id));
|
let mut source_findings = crate::pipeline::plc::analyze_tree(&path, target_id);
|
||||||
|
// Control mapping for the PLC path (run_plc_scan is separate from
|
||||||
|
// run_pipeline, which does its own mapping). PLC findings carry
|
||||||
|
// file_path/line/cwe, so the semantic pass reads each region under this
|
||||||
|
// source's `path` and stamps master-control refs. The LUT + grounded
|
||||||
|
// surface passes are code-pattern / CRA-specific and don't apply to
|
||||||
|
// IEC 61131-3 control logic, so only the semantic pass runs here.
|
||||||
|
crate::controls::semantic_stamp_findings(
|
||||||
|
&self.config,
|
||||||
|
self.llm.clone(),
|
||||||
|
&path,
|
||||||
|
&mut source_findings,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
all_findings.extend(source_findings);
|
||||||
// Control-application SBOM: CODESYS libraries + runtime from a
|
// Control-application SBOM: CODESYS libraries + runtime from a
|
||||||
// `.projectarchive` (uploaded, or committed in the working tree).
|
// `.projectarchive` (uploaded, or committed in the working tree).
|
||||||
let archive = a
|
let archive = a
|
||||||
|
|||||||
Reference in New Issue
Block a user