diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index c099017b9c..49bf3ca2be 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -841,6 +841,10 @@ void deleteFindingAttributions(Project project) { getVulnerabilityQueryManager().deleteFindingAttributions(project); } + public void reconcileFindingsForComponentAnalyzer(Component component, AnalyzerIdentity analyzerIdentity, Set currentVulnIdAndSources) { + getVulnerabilityQueryManager().reconcileFindingsForComponentAnalyzer(component, analyzerIdentity, currentVulnIdAndSources); + } + public List reconcileVulnerableSoftware(final Vulnerability vulnerability, final List vsListOld, final List vsList, diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 7aeee2d765..a3f94916e9 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -49,6 +49,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; @@ -297,6 +298,36 @@ void deleteFindingAttributions(Project project) { query.deletePersistentAll(project); } + /** + * Reconciles findings for a component from a specific analyzer by removing + * any FindingAttributions that are no longer reported by the analyzer. + * + * @param component the component to reconcile findings for + * @param analyzerIdentity the analyzer identity to scope reconciliation + * @param currentVulnIdAndSources the set of VulnIdAndSource reported by the latest scan + */ + public void reconcileFindingsForComponentAnalyzer(Component component, AnalyzerIdentity analyzerIdentity, Set currentVulnIdAndSources) { + runInTransaction(() -> { + final Query query = pm.newQuery(FindingAttribution.class, "component == :component && analyzerIdentity == :analyzerIdentity"); + final List existing = (List) query.execute(component, analyzerIdentity); + final Component persistentComponent = getObjectById(Component.class, component.getId()); + boolean changed = false; + for (final FindingAttribution fa : existing) { + final Vulnerability vuln = getObjectById(Vulnerability.class, fa.getVulnerability().getId()); + final VulnIdAndSource vid = new VulnIdAndSource(vuln.getVulnId(), vuln.getSource()); + if (!currentVulnIdAndSources.contains(vid)) { + persistentComponent.removeVulnerability(vuln); + delete(fa); + changed = true; + } + } + if (changed) { + persist(persistentComponent); + } + }); + } + + /** * Determines if a Component is affected by a specific Vulnerability by checking * {@link Vulnerability#getSource()} and {@link Vulnerability#getVulnId()}. diff --git a/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java index f1752e8a76..cedcea40b0 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/TrivyAnalysisTask.java @@ -46,6 +46,7 @@ import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentProperty; import org.dependencytrack.model.ConfigPropertyConstants; +import org.dependencytrack.model.VulnIdAndSource; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAnalysisLevel; import org.dependencytrack.parser.trivy.TrivyParser; @@ -71,9 +72,11 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import static java.util.Objects.requireNonNullElseGet; import static org.dependencytrack.common.ConfigKey.TRIVY_RETRY_BACKOFF_INITIAL_DURATION_MS; @@ -361,9 +364,10 @@ private void handleResults(final Map componentByPurl, final A } } - for (final Map.Entry> entry : vulnsByComponent.entrySet()) { - final Component component = entry.getKey(); - final List vulns = entry.getValue(); + // Ensure we call handle() for all components that were submitted for analysis, + // even if Trivy reported no vulnerabilities for them (so reconciliation can remove stale findings). + for (final Component component : componentByPurl.values()) { + final List vulns = vulnsByComponent.getOrDefault(component, Collections.emptyList()); handle(component, vulns); } } @@ -371,7 +375,7 @@ private void handleResults(final Map componentByPurl, final A private ArrayList analyzeBlob(final Collection blobs) { final var output = new ArrayList(); - for (final BlobInfo info : blobs) { + for (final BlobInfo info : blobs) { final PutBlobRequest putBlobRequest = PutBlobRequest.newBuilder() .setBlobInfo(info) .setDiffId("sha256:" + DigestUtils.sha256Hex(java.util.UUID.randomUUID().toString())) @@ -492,9 +496,13 @@ private void handle(final Component component, final Collection reportedVulns = new HashSet<>(); for (final trivy.proto.common.Vulnerability trivyVuln : trivyVulns) { final Vulnerability parsedVulnerability = trivyParser.parse(trivyVuln); + // track reported vulnerabilities so we can reconcile stale findings + reportedVulns.add(new VulnIdAndSource(parsedVulnerability.getVulnId(), parsedVulnerability.getSource())); + Vulnerability vulnerability = qm.getVulnerabilityByVulnId(parsedVulnerability.getSource(), parsedVulnerability.getVulnId()); if (vulnerability == null) { LOGGER.debug("Creating unavailable vulnerability:" + parsedVulnerability.getSource() + " - " + parsedVulnerability.getVulnId()); @@ -507,6 +515,9 @@ private void handle(final Component component, final Collection + assertThat(notification.getGroup()).isEqualTo(NotificationGroup.PROJECT_CREATED.name()) + ); + verify(postRequestedFor(urlPathEqualTo("/twirp/trivy.cache.v1.Cache/PutBlob"))); + verify(postRequestedFor(urlPathEqualTo("/twirp/trivy.scanner.v1.Scanner/Scan"))); + verify(postRequestedFor(urlPathEqualTo("/twirp/trivy.cache.v1.Cache/DeleteBlobs"))); + } + private static final ConcurrentLinkedQueue NOTIFICATIONS = new ConcurrentLinkedQueue<>(); public static class NotificationSubscriber implements Subscriber {