Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,10 @@ void deleteFindingAttributions(Project project) {
getVulnerabilityQueryManager().deleteFindingAttributions(project);
}

public void reconcileFindingsForComponentAnalyzer(Component component, AnalyzerIdentity analyzerIdentity, Set<VulnIdAndSource> currentVulnIdAndSources) {
getVulnerabilityQueryManager().reconcileFindingsForComponentAnalyzer(component, analyzerIdentity, currentVulnIdAndSources);
}

public List<VulnerableSoftware> reconcileVulnerableSoftware(final Vulnerability vulnerability,
final List<VulnerableSoftware> vsListOld,
final List<VulnerableSoftware> vsList,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<VulnIdAndSource> currentVulnIdAndSources) {
runInTransaction(() -> {
final Query<FindingAttribution> query = pm.newQuery(FindingAttribution.class, "component == :component && analyzerIdentity == :analyzerIdentity");
final List<FindingAttribution> existing = (List<FindingAttribution>) 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()}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -361,17 +364,18 @@ private void handleResults(final Map<String, Component> componentByPurl, final A
}
}

for (final Map.Entry<Component, List<trivy.proto.common.Vulnerability>> entry : vulnsByComponent.entrySet()) {
final Component component = entry.getKey();
final List<trivy.proto.common.Vulnerability> 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<trivy.proto.common.Vulnerability> vulns = vulnsByComponent.getOrDefault(component, Collections.emptyList());
handle(component, vulns);
}
}

private ArrayList<Result> analyzeBlob(final Collection<BlobInfo> blobs) {
final var output = new ArrayList<Result>();

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()))
Expand Down Expand Up @@ -492,9 +496,13 @@ private void handle(final Component component, final Collection<trivy.proto.comm
}

boolean didCreateVulns = false;
final Set<VulnIdAndSource> 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());
Expand All @@ -507,6 +515,9 @@ private void handle(final Component component, final Collection<trivy.proto.comm
qm.addVulnerability(vulnerability, persistentComponent, this.getAnalyzerIdentity());
}

// Reconcile findings for this component and analyzer: remove any attributions not reported in this scan
qm.reconcileFindingsForComponentAnalyzer(persistentComponent, this.getAnalyzerIdentity(), reportedVulns);

if (didCreateVulns) {
Event.dispatch(new IndexEvent(IndexEvent.Action.COMMIT, Vulnerability.class));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,70 @@ void testAnalyzeRespectsConfiguredScanningOption() throws InvalidProtocolBufferE
assertThat(scanRequest.getOptions().getPkgTypes(0)).isEqualTo("library");
}

@Test
void testReconcilesAndRemovesStaleTrivyFindings() {
stubFor(post(urlPathEqualTo("/twirp/trivy.cache.v1.Cache/PutBlob"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/protobuf")));

stubFor(post(urlPathEqualTo("/twirp/trivy.scanner.v1.Scanner/Scan"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/protobuf")
.withBody(ScanResponse.newBuilder()
.addResults(Result.newBuilder()
.setClass_("lang-pkgs")
.setTarget("java")
.setType("jar")
.build())
.build()
.toByteArray())));

stubFor(post(urlPathEqualTo("/twirp/trivy.cache.v1.Cache/DeleteBlobs"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")));

var project = new Project();
project.setName("acme-app");
project = qm.createProject(project, null, false);

var component = new Component();
component.setProject(project);
component.setGroup("com.example");
component.setName("openssl-provider");
component.setVersion("3.5.4-1~deb13u1");
component.setPurl("pkg:maven/com.example/openssl-provider@3.5.4-1~deb13u1?foo=bar#baz");
component = qm.createComponent(component, false);

// previously Trivy reported CVE-2025-15467; create a vulnerability and attribute it to the component
var vuln = new Vulnerability();
vuln.setVulnId("CVE-2025-15467");
vuln.setSource(Vulnerability.Source.NVD.name());
vuln.setTitle("reverted vuln");
vuln = qm.createVulnerability(vuln, false);

qm.addVulnerability(vuln, component, org.dependencytrack.tasks.scanners.AnalyzerIdentity.TRIVY_ANALYZER);

assertThat(qm.getAllVulnerabilities(component)).hasSize(1);

// run analysis again; Trivy reports no vulnerabilities for this component
new TrivyAnalysisTask().inform(new TrivyAnalysisEvent(
List.of(component), VulnerabilityAnalysisLevel.BOM_UPLOAD_ANALYSIS));

// stale finding should be removed
assertThat(qm.getAllVulnerabilities(component)).isEmpty();

assertThat(NOTIFICATIONS).satisfiesExactly(
notification ->
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<Notification> NOTIFICATIONS = new ConcurrentLinkedQueue<>();

public static class NotificationSubscriber implements Subscriber {
Expand Down
Loading