Refresh scanner details of findings that are seen again

A rescan only touched the timestamp of findings it had seen before, so a
newly published fix version, a changed severity and the package source
never reached existing rows. The repository now updates those fields
while keeping first_seen and the status the user set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-03 19:50:18 +02:00
parent aba2c69416
commit 70cf25fc7f
5 changed files with 93 additions and 23 deletions

View File

@ -595,17 +595,18 @@ impl FindingRepository for MemFindings {
self.0.lock().unwrap().push(finding.clone());
Ok(())
}
async fn touch(
async fn refresh(
&self,
ids: &[Uuid],
updates: &[(Uuid, RawFinding)],
last_seen: chrono::DateTime<Utc>,
) -> Result<(), DomainError> {
self.0
.lock()
.unwrap()
.iter_mut()
.filter(|f| ids.contains(&f.id))
.for_each(|f| f.last_seen = last_seen);
let mut v = self.0.lock().unwrap();
for (id, raw) in updates {
if let Some(f) = v.iter_mut().find(|f| f.id == *id) {
f.raw = raw.clone();
f.last_seen = last_seen;
}
}
Ok(())
}
async fn set_status(&self, id: Uuid, status: FindingStatus) -> Result<(), DomainError> {

View File

@ -363,3 +363,48 @@ async fn targets_are_reported_with_their_scope_and_open_count() {
// an image without findings is not listed
assert!(targets.iter().all(|t| t.target != PG));
}
#[tokio::test]
async fn rescan_refreshes_scanner_details_of_findings_that_are_still_present() {
let mut first = raw("CVE-1", "openssl", "3.0.1", Severity::Medium, None);
first.source = String::new();
let scanner = FakeScanner::default().with("os", Ok(vec![first]));
let (f, svc) = fixture(scanner);
svc.scan(&VecLog::default()).await.unwrap();
let before = f.findings.0.lock().unwrap()[0].clone();
svc.set_status(before.id, FindingStatus::Acknowledged)
.await
.unwrap();
// the scanner now rates it higher, knows a fix and reports the package source
let mut updated = raw(
"CVE-1",
"openssl",
"3.0.1",
Severity::Critical,
Some("3.0.2"),
);
updated.source = "debian".into();
updated.title = "openssl: corrected title".into();
*f.results.lock().unwrap() =
std::collections::HashMap::from([("os".to_string(), Ok(vec![updated]))]);
let report = svc.scan(&VecLog::default()).await.unwrap();
assert!(
report.new_findings.is_empty(),
"same finding, not a new one"
);
let after = f.findings.0.lock().unwrap()[0].clone();
assert_eq!(after.id, before.id);
assert_eq!(after.raw.severity, Severity::Critical);
assert_eq!(after.raw.fixed_version.as_deref(), Some("3.0.2"));
assert_eq!(after.raw.source, "debian");
assert_eq!(after.raw.title, "openssl: corrected title");
assert_eq!(after.first_seen, before.first_seen, "first_seen is kept");
assert!(after.last_seen > before.last_seen);
assert_eq!(
after.status,
FindingStatus::Acknowledged,
"the user's decision is kept"
);
}

View File

@ -146,7 +146,7 @@ impl VulnerabilityService {
continue;
}
match existing.iter().find(|f| f.raw.key() == r.key()) {
Some(f) => still_present.push(f.id),
Some(f) => still_present.push((f.id, r)),
None => {
let f = Finding {
id: Uuid::new_v4(),
@ -162,9 +162,12 @@ impl VulnerabilityService {
}
}
}
self.findings.touch(&still_present, now).await?;
self.findings.refresh(&still_present, now).await?;
let mut fixed = 0;
for f in existing.iter().filter(|f| !still_present.contains(&f.id)) {
for f in existing
.iter()
.filter(|f| !still_present.iter().any(|(id, _)| *id == f.id))
{
self.findings.set_status(f.id, FindingStatus::Fixed).await?;
fixed += 1;
}