Skip to content

Commit

Permalink
Fix directDependencies of cloned projects referring to original com…
Browse files Browse the repository at this point in the history
…ponent UUIDs

Fixes DependencyTrack#4153

Signed-off-by: nscuro <nscuro@protonmail.com>
  • Loading branch information
nscuro committed Sep 24, 2024
1 parent 8c2d933 commit 3eac1d6
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ public Component cloneComponent(Component sourceComponent, Project destinationPr
component.setResolvedLicense(sourceComponent.getResolvedLicense());
component.setAuthors(sourceComponent.getAuthors());
component.setSupplier(sourceComponent.getSupplier());
component.setDirectDependencies(sourceComponent.getDirectDependencies());
// TODO Add support for parent component and children components
component.setProject(destinationProject);
return createComponent(component, commitIndex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
import alpine.notification.NotificationLevel;
import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.github.packageurl.PackageURL;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -58,12 +61,15 @@
import javax.jdo.Query;
import javax.jdo.metadata.MemberMetadata;
import javax.jdo.metadata.TypeMetadata;
import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

final class ProjectQueryManager extends QueryManager implements IQueryManager {
Expand Down Expand Up @@ -512,6 +518,8 @@ public Project clone(
final boolean includeACL,
final boolean includePolicyViolations
) {
final var jsonMapper = new JsonMapper();

final Project clonedProject = callInTransaction(() -> {
final Project source = getObjectByUuid(Project.class, from, Project.FetchGroup.ALL.name());
if (source == null) {
Expand Down Expand Up @@ -539,7 +547,7 @@ public Project clone(
project.setCpe(source.getCpe());
project.setPurl(source.getPurl());
project.setSwidTagId(source.getSwidTagId());
if (includeComponents && includeServices) {
if (source.getDirectDependencies() != null && includeComponents && includeServices) {
project.setDirectDependencies(source.getDirectDependencies());
}
project.setParent(source.getParent());
Expand Down Expand Up @@ -573,7 +581,17 @@ public Project clone(
}
}

final Map<Long, Component> clonedComponents = new HashMap<>();
final var projectDirectDepsSourceComponentUuids = new HashSet<UUID>();
if (project.getDirectDependencies() != null) {
projectDirectDepsSourceComponentUuids.addAll(
parseDirectDependenciesUuids(jsonMapper, project.getDirectDependencies()));
}

final var clonedComponentById = new HashMap<Long, Component>();
final var clonedComponentBySourceComponentId = new HashMap<Long, Component>();
final var directDepsSourceComponentUuidsByClonedComponentId = new HashMap<Long, Set<UUID>>();
final var clonedComponentUuidBySourceComponentUuid = new HashMap<UUID, UUID>();

if (includeComponents) {
final List<Component> sourceComponents = getAllComponents(source);
if (sourceComponents != null) {
Expand All @@ -584,11 +602,44 @@ public Project clone(
final FindingAttribution sourceAttribution = this.getFindingAttribution(vuln, sourceComponent);
this.addVulnerability(vuln, clonedComponent, sourceAttribution.getAnalyzerIdentity(), sourceAttribution.getAlternateIdentifier(), sourceAttribution.getReferenceUrl(), sourceAttribution.getAttributedOn());
}
clonedComponents.put(sourceComponent.getId(), clonedComponent);

clonedComponentById.put(clonedComponent.getId(), clonedComponent);
clonedComponentBySourceComponentId.put(sourceComponent.getId(), clonedComponent);
clonedComponentUuidBySourceComponentUuid.put(sourceComponent.getUuid(), clonedComponent.getUuid());

if (clonedComponent.getDirectDependencies() != null) {
final Set<UUID> directDepsUuids = parseDirectDependenciesUuids(jsonMapper, clonedComponent.getDirectDependencies());
if (!directDepsUuids.isEmpty()) {
directDepsSourceComponentUuidsByClonedComponentId.put(clonedComponent.getId(), directDepsUuids);
}
}
}
}
}

if (!projectDirectDepsSourceComponentUuids.isEmpty()) {
String directDependencies = project.getDirectDependencies();
for (final UUID sourceComponentUuid : projectDirectDepsSourceComponentUuids) {
final UUID clonedComponentUuid = clonedComponentUuidBySourceComponentUuid.get(sourceComponentUuid);
directDependencies = directDependencies.replace(sourceComponentUuid.toString(), clonedComponentUuid.toString());
}

project.setDirectDependencies(directDependencies);
}

for (final long componentId : directDepsSourceComponentUuidsByClonedComponentId.keySet()) {
final Component component = clonedComponentById.get(componentId);
final Set<UUID> sourceComponentUuids = directDepsSourceComponentUuidsByClonedComponentId.get(componentId);

String directDependencies = component.getDirectDependencies();
for (final UUID sourceComponentUuid : sourceComponentUuids) {
final UUID clonedComponentUuid = clonedComponentUuidBySourceComponentUuid.get(sourceComponentUuid);
directDependencies = directDependencies.replace(sourceComponentUuid.toString(), clonedComponentUuid.toString());
}

component.setDirectDependencies(directDependencies);
}

if (includeServices) {
final List<ServiceComponent> sourceServices = getAllServiceComponents(source);
if (sourceServices != null) {
Expand All @@ -604,7 +655,7 @@ public Project clone(
for (final Analysis sourceAnalysis : analyses) {
Analysis analysis = new Analysis();
analysis.setAnalysisState(sourceAnalysis.getAnalysisState());
final Component clonedComponent = clonedComponents.get(sourceAnalysis.getComponent().getId());
final Component clonedComponent = clonedComponentBySourceComponentId.get(sourceAnalysis.getComponent().getId());
if (clonedComponent == null) {
break;
}
Expand Down Expand Up @@ -642,7 +693,7 @@ public Project clone(
final List<PolicyViolation> sourcePolicyViolations = getAllPolicyViolations(source);
if (sourcePolicyViolations != null) {
for (final PolicyViolation policyViolation : sourcePolicyViolations) {
final Component destinationComponent = clonedComponents.get(policyViolation.getComponent().getId());
final Component destinationComponent = clonedComponentBySourceComponentId.get(policyViolation.getComponent().getId());
final PolicyViolation clonedPolicyViolation = clonePolicyViolation(policyViolation, destinationComponent);
persist(clonedPolicyViolation);
}
Expand All @@ -657,6 +708,30 @@ public Project clone(
return clonedProject;
}

private static Set<UUID> parseDirectDependenciesUuids(
final JsonMapper jsonMapper,
final String directDependencies) throws IOException {
final var uuids = new HashSet<UUID>();
try (final JsonParser jsonParser = jsonMapper.createParser(directDependencies)) {
JsonToken currentToken = jsonParser.nextToken();
if (currentToken != JsonToken.START_ARRAY) {
throw new IllegalArgumentException("""
Expected directDependencies to be a JSON array, \
but encountered token: %s""".formatted(currentToken));
}

while (jsonParser.nextToken() != null) {
if (jsonParser.currentToken() == JsonToken.FIELD_NAME
&& "uuid".equals(jsonParser.currentName())
&& jsonParser.nextToken() == JsonToken.VALUE_STRING) {
uuids.add(UUID.fromString(jsonParser.getValueAsString()));
}
}
}

return uuids;
}

/**
* Deletes a Project and all objects dependant on the project.
* @param project the Project to delete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,11 @@
import alpine.event.framework.EventService;
import alpine.model.IConfigProperty.PropertyType;
import alpine.model.ManagedUser;
import alpine.model.Team;
import alpine.model.Permission;
import alpine.model.Team;
import alpine.server.auth.JsonWebToken;
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFilter;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.cyclonedx.model.ExternalReference.Type;
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
Expand All @@ -44,6 +36,7 @@
import org.dependencytrack.model.AnalysisResponse;
import org.dependencytrack.model.AnalysisState;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.ComponentIdentity;
import org.dependencytrack.model.ConfigPropertyConstants;
import org.dependencytrack.model.ExternalReference;
import org.dependencytrack.model.OrganizationalContact;
Expand All @@ -60,11 +53,20 @@
import org.glassfish.jersey.client.HttpUrlConnectorProvider;
import org.glassfish.jersey.server.ResourceConfig;
import org.hamcrest.CoreMatchers;
import org.json.JSONArray;
import org.junit.After;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Test;

import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
Expand All @@ -78,6 +80,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;

public class ProjectResourceTest extends ResourceTest {
private ManagedUser testUser;
Expand Down Expand Up @@ -1107,32 +1110,41 @@ public void cloneProjectTest() {
final var componentSupplier = new OrganizationalEntity();
componentSupplier.setName("componentSupplier");

final var component = new Component();
component.setProject(project);
component.setName("acme-lib");
component.setVersion("2.0.0");
component.setSupplier(componentSupplier);
qm.persist(component);
final var componentA = new Component();
componentA.setProject(project);
componentA.setName("acme-lib-a");
componentA.setVersion("2.0.0");
componentA.setSupplier(componentSupplier);
qm.persist(componentA);

final var componentB = new Component();
componentB.setProject(project);
componentB.setName("acme-lib-b");
componentB.setVersion("2.1.0");
qm.persist(componentB);

final var service = new ServiceComponent();
service.setProject(project);
service.setName("acme-service");
service.setVersion("3.0.0");
qm.persist(service);

project.setDirectDependencies(new JSONArray().put(new ComponentIdentity(componentA).toJSON()).toString());
componentA.setDirectDependencies(new JSONArray().put(new ComponentIdentity(componentB).toJSON()).toString());

final var vuln = new Vulnerability();
vuln.setVulnId("INT-123");
vuln.setSource(Vulnerability.Source.INTERNAL);
qm.persist(vuln);

qm.addVulnerability(vuln, component, AnalyzerIdentity.INTERNAL_ANALYZER);
final Analysis analysis = qm.makeAnalysis(component, vuln, AnalysisState.NOT_AFFECTED,
qm.addVulnerability(vuln, componentA, AnalyzerIdentity.INTERNAL_ANALYZER);
final Analysis analysis = qm.makeAnalysis(componentA, vuln, AnalysisState.NOT_AFFECTED,
AnalysisJustification.REQUIRES_ENVIRONMENT, AnalysisResponse.WILL_NOT_FIX, "details", false);
qm.makeAnalysisComment(analysis, "comment", "commenter");

final Response response = jersey.target("%s/clone".formatted(V1_PROJECT)).request()
.header(X_API_KEY, apiKey)
.put(Entity.json("""
.put(Entity.json(/* language=JSON */ """
{
"project": "%s",
"version": "1.1.0",
Expand Down Expand Up @@ -1163,6 +1175,18 @@ public void cloneProjectTest() {
assertThat(clonedProject.getManufacturer()).isNotNull();
assertThat(clonedProject.getManufacturer().getName()).isEqualTo("projectManufacturer");
assertThat(clonedProject.getAccessTeams()).containsOnly(team);
assertThatJson(clonedProject.getDirectDependencies())
.withMatcher("notSourceComponentUuid", not(equalTo(componentA.getUuid().toString())))
.isEqualTo(/* language=JSON */ """
[
{
"objectType": "COMPONENT",
"uuid": "${json-unit.matches:notSourceComponentUuid}",
"name": "acme-lib-a",
"version": "2.0.0"
}
]
""");

final List<ProjectProperty> clonedProperties = qm.getProjectProperties(clonedProject);
assertThat(clonedProperties).satisfiesExactly(clonedProperty -> {
Expand All @@ -1184,24 +1208,42 @@ public void cloneProjectTest() {
assertThat(clonedMetadata.getSupplier())
.satisfies(entity -> assertThat(entity.getName()).isEqualTo("metadataSupplier"));

assertThat(qm.getAllComponents(clonedProject)).satisfiesExactly(clonedComponent -> {
assertThat(clonedComponent.getUuid()).isNotEqualTo(component.getUuid());
assertThat(clonedComponent.getName()).isEqualTo("acme-lib");
assertThat(clonedComponent.getVersion()).isEqualTo("2.0.0");
assertThat(clonedComponent.getSupplier()).isNotNull();
assertThat(clonedComponent.getSupplier().getName()).isEqualTo("componentSupplier");

assertThat(qm.getAllVulnerabilities(clonedComponent)).containsOnly(vuln);

assertThat(qm.getAnalysis(clonedComponent, vuln)).satisfies(clonedAnalysis -> {
assertThat(clonedAnalysis.getId()).isNotEqualTo(analysis.getId());
assertThat(clonedAnalysis.getAnalysisState()).isEqualTo(AnalysisState.NOT_AFFECTED);
assertThat(clonedAnalysis.getAnalysisJustification()).isEqualTo(AnalysisJustification.REQUIRES_ENVIRONMENT);
assertThat(clonedAnalysis.getAnalysisResponse()).isEqualTo(AnalysisResponse.WILL_NOT_FIX);
assertThat(clonedAnalysis.getAnalysisDetails()).isEqualTo("details");
assertThat(clonedAnalysis.isSuppressed()).isFalse();
});
});
assertThat(qm.getAllComponents(clonedProject)).satisfiesExactlyInAnyOrder(
clonedComponent -> {
assertThat(clonedComponent.getUuid()).isNotEqualTo(componentA.getUuid());
assertThat(clonedComponent.getName()).isEqualTo("acme-lib-a");
assertThat(clonedComponent.getVersion()).isEqualTo("2.0.0");
assertThat(clonedComponent.getSupplier()).isNotNull();
assertThat(clonedComponent.getSupplier().getName()).isEqualTo("componentSupplier");
assertThatJson(clonedComponent.getDirectDependencies())
.withMatcher("notSourceComponentUuid", not(equalTo(componentB.getUuid().toString())))
.isEqualTo(/* language=JSON */ """
[
{
"objectType": "COMPONENT",
"uuid": "${json-unit.matches:notSourceComponentUuid}",
"name": "acme-lib-b",
"version": "2.1.0"
}
]
""");

assertThat(qm.getAllVulnerabilities(clonedComponent)).containsOnly(vuln);

assertThat(qm.getAnalysis(clonedComponent, vuln)).satisfies(clonedAnalysis -> {
assertThat(clonedAnalysis.getId()).isNotEqualTo(analysis.getId());
assertThat(clonedAnalysis.getAnalysisState()).isEqualTo(AnalysisState.NOT_AFFECTED);
assertThat(clonedAnalysis.getAnalysisJustification()).isEqualTo(AnalysisJustification.REQUIRES_ENVIRONMENT);
assertThat(clonedAnalysis.getAnalysisResponse()).isEqualTo(AnalysisResponse.WILL_NOT_FIX);
assertThat(clonedAnalysis.getAnalysisDetails()).isEqualTo("details");
assertThat(clonedAnalysis.isSuppressed()).isFalse();
});
},
clonedComponent -> {
assertThat(clonedComponent.getUuid()).isNotEqualTo(componentA.getUuid());
assertThat(clonedComponent.getName()).isEqualTo("acme-lib-b");
assertThat(clonedComponent.getVersion()).isEqualTo("2.1.0");
});

assertThat(qm.getAllServiceComponents(clonedProject)).satisfiesExactly(clonedService -> {
assertThat(clonedService.getUuid()).isNotEqualTo(service.getUuid());
Expand Down

0 comments on commit 3eac1d6

Please sign in to comment.