diff --git a/apiserver/src/main/java/org/dependencytrack/pkgmetadata/FetchPackageMetadataResolutionCandidatesActivity.java b/apiserver/src/main/java/org/dependencytrack/pkgmetadata/FetchPackageMetadataResolutionCandidatesActivity.java index 2c14c5f1c9..d8f96dc1c6 100644 --- a/apiserver/src/main/java/org/dependencytrack/pkgmetadata/FetchPackageMetadataResolutionCandidatesActivity.java +++ b/apiserver/src/main/java/org/dependencytrack/pkgmetadata/FetchPackageMetadataResolutionCandidatesActivity.java @@ -73,10 +73,9 @@ public FetchPackageMetadataResolutionCandidatesRes execute( try { purl = new PackageURL(purlStr); } catch (MalformedPackageURLException e) { - LOGGER.warn("Failed to parse PURL '{}'; Assigning to empty resolver", purlStr, e); - purlsByResolver - .computeIfAbsent("", k -> new ArrayList<>()) - .add(purlStr); + // Malformed PURL cannot be resolved and is data quality issue. + // Skip to avoid resolution attempts and spamming WARN logs on every cycle. + LOGGER.debug("Skipping malformed PURL '{}'", purlStr, e); continue; } diff --git a/apiserver/src/test/java/org/dependencytrack/pkgmetadata/ResolvePackageMetadataWorkflowTest.java b/apiserver/src/test/java/org/dependencytrack/pkgmetadata/ResolvePackageMetadataWorkflowTest.java index ab7e372019..c6d5538cb9 100644 --- a/apiserver/src/test/java/org/dependencytrack/pkgmetadata/ResolvePackageMetadataWorkflowTest.java +++ b/apiserver/src/test/java/org/dependencytrack/pkgmetadata/ResolvePackageMetadataWorkflowTest.java @@ -276,6 +276,65 @@ void shouldPersistEmptyResultsForUnknownResolver() { }); } + @Test + void shouldSkipMalformedPurlAndNotReselectIt() { + final String malformedPurl = "pkg:nuget/serilog.sinks.file%A%20...5.0.0@5.0.0"; + final Instant resolvedAt = Instant.now(); + mockResolveFnRef.set(purl -> new PackageMetadata("9.9.9", Instant.now(), resolvedAt, null)); + + var project = new Project(); + project.setName("test-project"); + project = qm.persist(project); + + final var malformedComponent = new Component(); + malformedComponent.setProject(project); + malformedComponent.setName("serilog.sinks.file"); + malformedComponent.setVersion("5.0.0"); + malformedComponent.setPurl(malformedPurl); + qm.persist(malformedComponent); + + final var componentFoo = new Component(); + componentFoo.setProject(project); + componentFoo.setGroup("org.acme"); + componentFoo.setName("foo"); + componentFoo.setVersion("1.0"); + componentFoo.setPurl("pkg:maven/org.acme/foo@1.0"); + qm.persist(componentFoo); + + final var fetchActivity = new FetchPackageMetadataResolutionCandidatesActivity(pluginManager); + final List candidatePurls = fetchActivity.execute(null, null) + .getCandidateGroupsList().stream() + .flatMap(group -> group.getPurlsList().stream()) + .toList(); + assertThat(candidatePurls).contains("pkg:maven/org.acme/foo@1.0") + .doesNotContain(malformedPurl); + + // Running workflow again should not reselect the malformed PURL + for (int i = 0; i < 2; i++) { + final UUID runId = workflowTest.getEngine().createRun( + new CreateWorkflowRunRequest<>(ResolvePackageMetadataWorkflow.class)); + workflowTest.awaitRunStatus(runId, WorkflowRunStatus.COMPLETED); + } + + List metadataPurls = withJdbiHandle(handle -> handle + .createQuery(""" + SELECT "PURL" + FROM "PACKAGE_METADATA" + """) + .mapTo(String.class) + .list()); + assertThat(metadataPurls).containsExactly("pkg:maven/org.acme/foo"); + + metadataPurls = withJdbiHandle(handle -> handle + .createQuery(""" + SELECT "PURL" + FROM "PACKAGE_ARTIFACT_METADATA" + """) + .mapTo(String.class) + .list()); + assertThat(metadataPurls).containsExactly("pkg:maven/org.acme/foo@1.0"); + } + @Test void shouldPassPriorArtifactMetadataToResolverWhenAvailable() { useJdbiTransaction(handle -> {