Skip to content

Commit

Permalink
Add support for custom license resolution by name
Browse files Browse the repository at this point in the history
Ported from DependencyTrack/dependency-track#2769

Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Oct 19, 2023
1 parent f06b808 commit 89c86e4
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -498,15 +499,21 @@ private static Map<ComponentIdentity, Component> processComponents(final QueryMa
// they appear multiple times for different components.
final var licenseCache = new HashMap<String, License>();

// We support resolution of custom licenses by their name.
// To avoid any conflicts with license IDs, cache those separately.
final var customLicenseCache = new HashMap<String, License>();

final var persistentComponents = new HashMap<ComponentIdentity, Component>();
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;
Expand Down Expand Up @@ -576,6 +583,7 @@ private static Map<ComponentIdentity, Component> 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);
Expand Down Expand Up @@ -828,6 +836,33 @@ private static License resolveLicense(final PersistenceManager pm, final Map<Str
return license;
}

/**
* Lookup a custom {@link License} by its name, and cache the result in {@code cache}.
*
* @param pm The {@link PersistenceManager} to use
* @param cache A {@link Map} to use for caching
* @param licenseName The {@link License} name to lookup
* @return The resolved {@link License}, or {@code null} if no {@link License} was found
*/
private static License resolveCustomLicense(final PersistenceManager pm, final Map<String, License> cache, final String licenseName) {
if (cache.containsKey(licenseName)) {
return cache.get(licenseName);
}

final Query<License> 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<Dependency> dependencies, final String bomRef) {
if (dependencies == null || dependencies.isEmpty() || bomRef == null) {
return null;
Expand Down Expand Up @@ -987,7 +1022,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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -54,6 +56,7 @@

import static org.apache.commons.io.IOUtils.resourceToURL;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.dependencytrack.assertion.Assertions.assertConditionWithTimeout;
import static org.dependencytrack.model.WorkflowStatus.CANCELLED;
import static org.dependencytrack.model.WorkflowStatus.COMPLETED;
Expand Down Expand Up @@ -673,6 +676,49 @@ public void informWithDelayedBomProcessedNotificationAndNoComponents() throws Ex
);
}

@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.
Expand Down
39 changes: 39 additions & 0 deletions src/test/resources/unit/bom-custom-license.json
Original file line number Diff line number Diff line change
@@ -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": " "
}
}
]
}
]
}

0 comments on commit 89c86e4

Please sign in to comment.