diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 76910dd738..c643a783a1 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -314,7 +314,7 @@ public Response uploadBom(@Parameter(required = true) BomSubmitRequest request) } } - project = qm.createProject(StringUtils.trimToNull(request.getProjectName()), null, StringUtils.trimToNull(request.getProjectVersion()), null, parent, null, true, true); + project = qm.createProject(StringUtils.trimToNull(request.getProjectName()), null, StringUtils.trimToNull(request.getProjectVersion()), request.getProjectTags(), parent, null, true, true); Principal principal = getPrincipal(); qm.updateNewProjectACL(project, principal); } else { diff --git a/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java b/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java index 00281d4b9b..99129c7a81 100644 --- a/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java +++ b/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java @@ -18,6 +18,7 @@ */ package org.dependencytrack.resources.v1.vo; +import org.dependencytrack.model.Tag; import alpine.common.validation.RegexSequence; import alpine.server.json.TrimmedStringDeserializer; import com.fasterxml.jackson.annotation.JsonCreator; @@ -28,6 +29,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; +import java.util.List; /** * Defines a custom request object used when uploading bill-of-material (bom) documents. @@ -51,6 +53,8 @@ public final class BomSubmitRequest { @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The project version may only contain printable characters") private final String projectVersion; + private final List projectTags; + @Pattern(regexp = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", message = "The parent UUID must be a valid 36 character UUID") private final String parentUUID; @@ -71,15 +75,17 @@ public final class BomSubmitRequest { public BomSubmitRequest(String project, String projectName, String projectVersion, + List projectTags, boolean autoCreate, String bom) { - this(project, projectName, projectVersion, autoCreate, null, null, null, bom); + this(project, projectName, projectVersion, projectTags, autoCreate, null, null, null, bom); } @JsonCreator public BomSubmitRequest(@JsonProperty(value = "project") String project, @JsonProperty(value = "projectName") String projectName, @JsonProperty(value = "projectVersion") String projectVersion, + @JsonProperty(value = "projectTags") List projectTags, @JsonProperty(value = "autoCreate") boolean autoCreate, @JsonProperty(value = "parentUUID") String parentUUID, @JsonProperty(value = "parentName") String parentName, @@ -88,6 +94,7 @@ public BomSubmitRequest(@JsonProperty(value = "project") String project, this.project = project; this.projectName = projectName; this.projectVersion = projectVersion; + this.projectTags = projectTags; this.autoCreate = autoCreate; this.parentUUID = parentUUID; this.parentName = parentName; @@ -110,6 +117,11 @@ public String getProjectVersion() { return projectVersion; } + @Schema(description = "Overwrite project tags") + public List getProjectTags() { + return projectTags; + } + @Schema(example = "5341f53c-611b-4388-9d9c-731026dc5eec") public String getParentUUID() { return parentUUID; diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 8beb695155..c99eb4c63b 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -38,6 +38,7 @@ import org.dependencytrack.model.ProjectMetadata; import org.dependencytrack.model.ProjectProperty; import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Tag; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.parser.cyclonedx.CycloneDxValidator; import org.dependencytrack.resources.v1.exception.JsonMappingExceptionMapper; @@ -56,6 +57,8 @@ import java.util.Base64; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; @@ -726,7 +729,7 @@ public void uploadBomTest() throws Exception { initializeWithPermissions(Permissions.BOM_UPLOAD); Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); - BomSubmitRequest request = new BomSubmitRequest(project.getUuid().toString(), null, null, false, bomString); + BomSubmitRequest request = new BomSubmitRequest(project.getUuid().toString(), null, null, null, false, bomString); Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -741,7 +744,7 @@ public void uploadBomTest() throws Exception { public void uploadBomInvalidProjectTest() throws Exception { initializeWithPermissions(Permissions.BOM_UPLOAD); String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); - BomSubmitRequest request = new BomSubmitRequest(UUID.randomUUID().toString(), null, null, false, bomString); + BomSubmitRequest request = new BomSubmitRequest(UUID.randomUUID().toString(), null, null, null, false, bomString); Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -755,7 +758,7 @@ public void uploadBomInvalidProjectTest() throws Exception { public void uploadBomAutoCreateTest() throws Exception { initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD); String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); - BomSubmitRequest request = new BomSubmitRequest(null, "Acme Example", "1.0", true, bomString); + BomSubmitRequest request = new BomSubmitRequest(null, "Acme Example", "1.0", null, true, bomString); Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -768,10 +771,35 @@ public void uploadBomAutoCreateTest() throws Exception { Assert.assertNotNull(project); } + @Test + public void uploadBomAutoCreateWithTagsTest() throws Exception { + initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD); + String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); + List tags = Stream.of("tag1", "tag2").map(name -> { + Tag tag = new Tag(); + tag.setName(name); + return tag; + }).collect(Collectors.toList()); + BomSubmitRequest request = new BomSubmitRequest(null, "Acme Example", "1.0", tags, true, bomString); + Response response = jersey.target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(request, MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertNotNull(json.getString("token")); + Assert.assertTrue(UuidUtil.isValidUUID(json.getString("token"))); + Project project = qm.getProject("Acme Example", "1.0"); + Assert.assertNotNull(project); + assertThat(project.getTags()) + .extracting(Tag::getName) + .containsExactlyInAnyOrder("tag1", "tag2"); + } + @Test public void uploadBomUnauthorizedTest() throws Exception { String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); - BomSubmitRequest request = new BomSubmitRequest(null, "Acme Example", "1.0", true, bomString); + BomSubmitRequest request = new BomSubmitRequest(null, "Acme Example", "1.0", null, true, bomString); Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -785,7 +813,7 @@ public void uploadBomAutoCreateTestWithParentTest() throws Exception { initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD); String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); // Upload parent project - BomSubmitRequest request = new BomSubmitRequest(null, "Acme Parent", "1.0", true, bomString); + BomSubmitRequest request = new BomSubmitRequest(null, "Acme Parent", "1.0", null, true, bomString); Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -797,7 +825,7 @@ public void uploadBomAutoCreateTestWithParentTest() throws Exception { String parentUUID = parent.getUuid().toString(); // Upload first child, search parent by UUID - request = new BomSubmitRequest(null, "Acme Example", "1.0", true, parentUUID, null, null, bomString); + request = new BomSubmitRequest(null, "Acme Example", "1.0", null, true, parentUUID, null, null, bomString); response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -813,7 +841,7 @@ public void uploadBomAutoCreateTestWithParentTest() throws Exception { // Upload second child, search parent by name+ver - request = new BomSubmitRequest(null, "Acme Example", "2.0", true, null, "Acme Parent", "1.0", bomString); + request = new BomSubmitRequest(null, "Acme Example", "2.0", null, true, null, "Acme Parent", "1.0", bomString); response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -828,7 +856,7 @@ public void uploadBomAutoCreateTestWithParentTest() throws Exception { Assert.assertEquals(parentUUID, child.getParent().getUuid().toString()); // Upload third child, specify parent's UUID, name, ver. Name and ver are ignored when UUID is specified. - request = new BomSubmitRequest(null, "Acme Example", "3.0", true, parentUUID, "Non-existent parent", "1.0", bomString); + request = new BomSubmitRequest(null, "Acme Example", "3.0", null, true, parentUUID, "Non-existent parent", "1.0", bomString); response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -847,7 +875,7 @@ public void uploadBomAutoCreateTestWithParentTest() throws Exception { public void uploadBomInvalidParentTest() throws Exception { initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD); String bomString = Base64.getEncoder().encodeToString(resourceToByteArray("/unit/bom-1.xml")); - BomSubmitRequest request = new BomSubmitRequest(null, "Acme Example", "1.0", true, UUID.randomUUID().toString(), null, null, bomString); + BomSubmitRequest request = new BomSubmitRequest(null, "Acme Example", "1.0", null, true, UUID.randomUUID().toString(), null, null, bomString); Response response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON)); @@ -855,7 +883,7 @@ public void uploadBomInvalidParentTest() throws Exception { String body = getPlainTextBody(response); Assert.assertEquals("The parent component could not be found.", body); - request = new BomSubmitRequest(null, "Acme Example", "2.0", true, null, "Non-existent parent", null, bomString); + request = new BomSubmitRequest(null, "Acme Example", "2.0", null, true, null, "Non-existent parent", null, bomString); response = jersey.target(V1_BOM).request() .header(X_API_KEY, apiKey) .put(Entity.entity(request, MediaType.APPLICATION_JSON));