From 1d34be6e83dfebaba4c130b13bbad94fc7d6da53 Mon Sep 17 00:00:00 2001 From: nscuro Date: Thu, 19 Oct 2023 14:51:56 +0200 Subject: [PATCH] Add support for custom license resolution by name Ported from https://github.com/DependencyTrack/dependency-track/pull/2769 Co-authored-by: mhh Signed-off-by: nscuro --- .../tasks/BomUploadProcessingTask.java | 43 ++++++++++++++++-- .../tasks/BomUploadProcessingTaskTest.java | 45 +++++++++++++++++++ .../resources/unit/bom-custom-license.json | 39 ++++++++++++++++ 3 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 src/test/resources/unit/bom-custom-license.json diff --git a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java index 6306059cf..8bbffe9ef 100644 --- a/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java +++ b/src/main/java/org/dependencytrack/tasks/BomUploadProcessingTask.java @@ -84,6 +84,7 @@ import java.util.function.Predicate; import java.util.stream.Stream; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.datanucleus.PropertyNames.PROPERTY_FLUSH_MODE; import static org.datanucleus.PropertyNames.PROPERTY_PERSISTENCE_BY_REACHABILITY_AT_COMMIT; import static org.dependencytrack.common.ConfigKey.BOM_UPLOAD_PROCESSING_TRX_FLUSH_THRESHOLD; @@ -500,15 +501,21 @@ private static Map processComponents(final QueryMa // they appear multiple times for different components. final var licenseCache = new HashMap(); + // We support resolution of custom licenses by their name. + // To avoid any conflicts with license IDs, cache those separately. + final var customLicenseCache = new HashMap(); + final var persistentComponents = new HashMap(); try (final var flushHelper = new FlushHelper(qm, FLUSH_THRESHOLD)) { for (final Component component : components) { component.setInternal(isInternalComponent(component, qm)); - // Try to resolve the license by its ID. - // Note: licenseId is a transient field of Component and will not survive this transaction. - if (component.getLicenseId() != null) { + if (isNotBlank(component.getLicenseId())) { + // Try to resolve the license by its ID. + // Note: licenseId is a transient field of Component and will not survive this transaction. component.setResolvedLicense(resolveLicense(pm, licenseCache, component.getLicenseId())); + } else if (isNotBlank(component.getLicense())) { + component.setResolvedLicense(resolveCustomLicense(pm, customLicenseCache, component.getLicense())); } final boolean isNewOrUpdated; @@ -578,6 +585,7 @@ private static Map processComponents(final QueryMa // License cache is no longer needed; Let go of it. licenseCache.clear(); + customLicenseCache.clear(); // Delete components that existed before this BOM import, but do not exist anymore. deleteComponentsById(pm, oldComponentIds); @@ -830,6 +838,33 @@ private static License resolveLicense(final PersistenceManager pm, final Map cache, final String licenseName) { + if (cache.containsKey(licenseName)) { + return cache.get(licenseName); + } + + final Query query = pm.newQuery(License.class); + query.setFilter("name == :name && customLicense == true"); + query.setParameters(licenseName); + final License license; + try { + license = query.executeUnique(); + } finally { + query.closeAll(); + } + + cache.put(licenseName, license); + return license; + } + private static org.cyclonedx.model.Dependency findDependencyByBomRef(final List dependencies, final String bomRef) { if (dependencies == null || dependencies.isEmpty() || bomRef == null) { return null; @@ -989,7 +1024,7 @@ private ComponentRepositoryMetaAnalysisEvent createRepoMetaAnalysisEvent(Compone qm.getPersistenceManager().makePersistent(integrityMetaComponent); return new ComponentRepositoryMetaAnalysisEvent(component.getUuid(), component.getPurl().canonicalize(), component.isInternal(), FetchMeta.FETCH_META_INTEGRITY_DATA_AND_LATEST_VERSION); } else { - return new ComponentRepositoryMetaAnalysisEvent(component.getUuid(),component.getPurlCoordinates().canonicalize(), component.isInternal(), FetchMeta.FETCH_META_LATEST_VERSION); + return new ComponentRepositoryMetaAnalysisEvent(component.getUuid(), component.getPurlCoordinates().canonicalize(), component.isInternal(), FetchMeta.FETCH_META_LATEST_VERSION); } } diff --git a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java index d53a0c116..bdc8da8d2 100644 --- a/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java @@ -20,6 +20,7 @@ import com.github.packageurl.PackageURL; import org.apache.kafka.clients.producer.ProducerRecord; +import org.awaitility.Awaitility; import org.dependencytrack.AbstractPostgresEnabledTest; import org.dependencytrack.event.BomUploadEvent; import org.dependencytrack.event.kafka.KafkaEventDispatcher; @@ -30,6 +31,7 @@ import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.FetchStatus; import org.dependencytrack.model.IntegrityMetaComponent; +import org.dependencytrack.model.License; import org.dependencytrack.model.Project; import org.dependencytrack.model.VulnerabilityScan; import org.dependencytrack.model.WorkflowStep; @@ -696,6 +698,49 @@ public void informWithComponentWithoutPurl() throws Exception { .satisfiesExactly(component -> assertThat(component.getName()).isEqualTo("acme-lib")); } + @Test + public void informWithCustomLicenseResolutionTest() throws Exception { + final var customLicense = new License(); + customLicense.setName("custom license foobar"); + qm.createCustomLicense(customLicense, false); + + final Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + + final var bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), createTempBomFile("bom-custom-license.json")); + new BomUploadProcessingTask().inform(bomUploadEvent); + qm.createWorkflowSteps(bomUploadEvent.getChainIdentifier()); + + await("BOM processing") + .atMost(Duration.ofSeconds(5)) + .untilAsserted(() -> assertThat(kafkaMockProducer.history()).satisfiesExactly( + event -> assertThat(event.topic()).isEqualTo(KafkaTopics.NOTIFICATION_PROJECT_CREATED.name()), + event -> assertThat(event.topic()).isEqualTo(KafkaTopics.NOTIFICATION_BOM.name()), + event -> assertThat(event.topic()).isEqualTo(KafkaTopics.VULN_ANALYSIS_COMMAND.name()), + event -> assertThat(event.topic()).isEqualTo(KafkaTopics.VULN_ANALYSIS_COMMAND.name()), + event -> assertThat(event.topic()).isEqualTo(KafkaTopics.VULN_ANALYSIS_COMMAND.name()), + event -> assertThat(event.topic()).isEqualTo(KafkaTopics.NOTIFICATION_BOM.name()) + )); + + assertThat(qm.getAllComponents(project)).satisfiesExactly( + component -> { + assertThat(component.getName()).isEqualTo("acme-lib-a"); + assertThat(component.getResolvedLicense()).isNotNull(); + assertThat(component.getResolvedLicense().getName()).isEqualTo("custom license foobar"); + assertThat(component.getLicense()).isEqualTo("custom license foobar"); + }, + component -> { + assertThat(component.getName()).isEqualTo("acme-lib-b"); + assertThat(component.getResolvedLicense()).isNull(); + assertThat(component.getLicense()).isEqualTo("does not exist"); + }, + component -> { + assertThat(component.getName()).isEqualTo("acme-lib-c"); + assertThat(component.getResolvedLicense()).isNull(); + assertThat(component.getLicense()).isNull(); + } + ); + } + private static File createTempBomFile(final String testFileName) throws Exception { // The task will delete the input file after processing it, // so create a temporary copy to not impact other tests. diff --git a/src/test/resources/unit/bom-custom-license.json b/src/test/resources/unit/bom-custom-license.json new file mode 100644 index 000000000..1ca0e6a55 --- /dev/null +++ b/src/test/resources/unit/bom-custom-license.json @@ -0,0 +1,39 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "components": [ + { + "type": "library", + "name": "acme-lib-a", + "licenses": [ + { + "license": { + "name": "custom license foobar" + } + } + ] + }, + { + "type": "library", + "name": "acme-lib-b", + "licenses": [ + { + "license": { + "name": "does not exist" + } + } + ] + }, + { + "type": "library", + "name": "acme-lib-c", + "licenses": [ + { + "license": { + "name": " " + } + } + ] + } + ] +} \ No newline at end of file