Skip to content

Commit

Permalink
Fixes #3509 Allows definition of pURL in component or projects either…
Browse files Browse the repository at this point in the history
… as string or object to conform to swagger api defintion

Signed-off-by: Sebastien Delcoigne <sebastien.delcoigne@gmail.com>
  • Loading branch information
sebD committed Feb 29, 2024
1 parent aa60898 commit f8563f9
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 6 deletions.
4 changes: 3 additions & 1 deletion src/main/java/org/dependencytrack/model/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.model.validation.ValidSpdxExpression;
import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter;
import org.dependencytrack.resources.v1.serializers.CustomPackageURLDeserializer;
import org.dependencytrack.resources.v1.serializers.CustomPackageURLSerializer;

import javax.jdo.annotations.Column;
Expand Down Expand Up @@ -249,7 +250,7 @@ public enum FetchGroup {
@Index(name = "COMPONENT_PURL_IDX")
@Size(max = 255)
@com.github.packageurl.validator.PackageURL
@JsonDeserialize(using = TrimmedStringDeserializer.class)
@JsonDeserialize(using = CustomPackageURLDeserializer.class)
private String purl;

@Persistent(defaultFetchGroup = "true")
Expand Down Expand Up @@ -555,6 +556,7 @@ public void setCpe(String cpe) {
}

@JsonSerialize(using = CustomPackageURLSerializer.class)
@JsonDeserialize(using = CustomPackageURLDeserializer.class)
public PackageURL getPurl() {
if (purl == null) {
return null;
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/dependencytrack/model/Project.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.github.packageurl.PackageURL;
import io.swagger.annotations.ApiModelProperty;
import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter;
import org.dependencytrack.resources.v1.serializers.CustomPackageURLDeserializer;
import org.dependencytrack.resources.v1.serializers.CustomPackageURLSerializer;

import javax.jdo.annotations.Column;
Expand Down Expand Up @@ -197,7 +198,7 @@ public enum FetchGroup {
@Index(name = "PROJECT_PURL_IDX")
@Size(max = 255)
@com.github.packageurl.validator.PackageURL
@JsonDeserialize(using = TrimmedStringDeserializer.class)
@JsonDeserialize(using = CustomPackageURLDeserializer.class)
private String purl;

@Persistent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* This file is part of Dependency-Track.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) Steve Springett. All Rights Reserved.
*/
package org.dependencytrack.resources.v1.serializers;

import alpine.common.logging.Logger;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURLBuilder;

import java.io.IOException;

/**
* This class deserializes a PackageURL from either the canonicalized form
* or the object form.
*
* @author Sebastien Delcoigne
* @since 4.11.0
*/
public class CustomPackageURLDeserializer extends JsonDeserializer<String> {

private static final Logger LOGGER = Logger.getLogger(CustomPackageURLDeserializer.class);

@Override
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
try {
if (jsonParser.getCurrentToken() == JsonToken.START_OBJECT) {
PackageURLBuilder builder = PackageURLBuilder.aPackageURL();
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String name = jsonParser.getCurrentName();
switch (name) {
case "type" -> {
jsonParser.nextToken();
builder.withType(jsonParser.getText());
}
case "namespace" -> {
jsonParser.nextToken();
builder.withNamespace(jsonParser.getText());
}
case "name" -> {
jsonParser.nextToken();
builder.withName(jsonParser.getText());
}
case "version" -> {
jsonParser.nextToken();
builder.withVersion(jsonParser.getText());
}
case "subpath" -> {
jsonParser.nextToken();
builder.withSubpath(jsonParser.getText());
}
case "qualifiers" -> {
extractQualifiers(jsonParser, builder);
}
}
}
return builder.build().canonicalize();
} else if (jsonParser.getCurrentToken() == JsonToken.VALUE_STRING) {
return jsonParser.getValueAsString();
}
} catch (MalformedPackageURLException e) {
LOGGER.warn("Malformed pURL encountered", e);
}
return null;
}

private void extractQualifiers(JsonParser jsonParser, PackageURLBuilder builder) throws IOException {
if (jsonParser.getCurrentToken() == JsonToken.START_OBJECT) {
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String key = jsonParser.getCurrentName();
jsonParser.nextToken();
String value = jsonParser.getText();
builder.withQualifier(key, value);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ public void createComponentTest() {
component.setProject(project);
component.setName("My Component");
component.setVersion("1.0");
component.setPurl("pkg:npm/space/acme-example@1.0");
Response response = target(V1_COMPONENT + "/project/" + project.getUuid().toString()).request()
.header(X_API_KEY, apiKey)
.put(Entity.entity(component, MediaType.APPLICATION_JSON));
Expand All @@ -485,6 +486,35 @@ public void createComponentTest() {
Assert.assertNotNull(json);
Assert.assertEquals("My Component", json.getString("name"));
Assert.assertEquals("1.0", json.getString("version"));
Assert.assertEquals("pkg:npm/space/acme-example@1.0", json.getString("purl"));
Assert.assertTrue(UuidUtil.isValidUUID(json.getString("uuid")));
}

@Test
public void createComponentWithPurlPassedAsObjectTest() {
Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false);
String component = """
{
"name": "My Component",
"version": "1.0",
"purl": {
"scheme": "pkg",
"type": "npm",
"namespace": "space",
"name": "my-component",
"version": "1.0"
}
}
""";
Response response = target(V1_COMPONENT + "/project/" + project.getUuid().toString()).request()
.header(X_API_KEY, apiKey)
.put(Entity.json(component));
Assert.assertEquals(201, response.getStatus(), 0);
JsonObject json = parseJsonObject(response);
Assert.assertNotNull(json);
Assert.assertEquals("My Component", json.getString("name"));
Assert.assertEquals("1.0", json.getString("version"));
Assert.assertEquals("pkg:npm/space/my-component@1.0", json.getString("purl"));
Assert.assertTrue(UuidUtil.isValidUUID(json.getString("uuid")));
}

Expand Down Expand Up @@ -531,6 +561,7 @@ public void updateComponentTest() {
component.setVersion("1.0");
component = qm.createComponent(component, false);
component.setDescription("Test component");
component.setPurl("pkg:npm/space/acme-example@1.0");
Response response = target(V1_COMPONENT).request()
.header(X_API_KEY, apiKey)
.post(Entity.entity(component, MediaType.APPLICATION_JSON));
Expand All @@ -540,6 +571,42 @@ public void updateComponentTest() {
Assert.assertEquals("My Component", json.getString("name"));
Assert.assertEquals("1.0", json.getString("version"));
Assert.assertEquals("Test component", json.getString("description"));
Assert.assertEquals("pkg:npm/space/acme-example@1.0", json.getString("purl"));
}

@Test
public void updateComponentWithPurlPassedAsObjectTest() {
Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false);
Component component = new Component();
component.setProject(project);
component.setName("My Component");
component.setVersion("1.0");
qm.createComponent(component, false);
String componentUpdate = """
{
"uuid": "%s",
"name": "My Component",
"version": "1.0",
"description": "Test component",
"purl": {
"scheme": "pkg",
"type": "npm",
"namespace": "space",
"name": "my-component",
"version": "1.0"
}
}
""".formatted(component.getUuid());
Response response = target(V1_COMPONENT).request()
.header(X_API_KEY, apiKey)
.post(Entity.entity(componentUpdate, MediaType.APPLICATION_JSON));
Assert.assertEquals(200, response.getStatus(), 0);
JsonObject json = parseJsonObject(response);
Assert.assertNotNull(json);
Assert.assertEquals("My Component", json.getString("name"));
Assert.assertEquals("1.0", json.getString("version"));
Assert.assertEquals("Test component", json.getString("description"));
Assert.assertEquals("pkg:npm/space/my-component@1.0", json.getString("purl"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,20 +343,53 @@ public void createProjectTest(){
project.setName("Acme Example");
project.setVersion("1.0");
project.setDescription("Test project");
project.setPurl("pkg:npm/space/acme-example@1.0");
Response response = target(V1_PROJECT)
.request()
.header(X_API_KEY, apiKey)
.put(Entity.entity(project, MediaType.APPLICATION_JSON));
.put(Entity.json(project));
Assert.assertEquals(201, response.getStatus(), 0);
JsonObject json = parseJsonObject(response);
Assert.assertNotNull(json);
Assert.assertEquals("Acme Example", json.getString("name"));
Assert.assertEquals("1.0", json.getString("version"));
Assert.assertEquals("Test project", json.getString("description"));
Assert.assertTrue(json.getBoolean("active"));
Assert.assertEquals("pkg:npm/space/acme-example@1.0", json.getString("purl"));
Assert.assertTrue(UuidUtil.isValidUUID(json.getString("uuid")));
}

@Test
public void createProjectWithPurlObjectTest(){
String project = """
{
"name" : "Test",
"version": "1.0",
"description": "Test project",
"purl": {
"scheme": "pkg",
"type": "npm",
"namespace": "space",
"name": "test",
"version": "1.0"
}
}
""";
Response response = target(V1_PROJECT)
.request()
.header(X_API_KEY, apiKey)
.put(Entity.json(project));
Assert.assertEquals(201, response.getStatus(), 0);
JsonObject json = parseJsonObject(response);
Assert.assertNotNull(json);
Assert.assertEquals("Test", json.getString("name"));
Assert.assertEquals("1.0", json.getString("version"));
Assert.assertEquals("Test project", json.getString("description"));
Assert.assertTrue(json.getBoolean("active"));
Assert.assertTrue(UuidUtil.isValidUUID(json.getString("uuid")));
Assert.assertEquals("pkg:npm/space/test@1.0", json.getString("purl"));
}

@Test
public void createProjectDuplicateTest() {
Project project = new Project();
Expand All @@ -365,12 +398,12 @@ public void createProjectDuplicateTest() {
Response response = target(V1_PROJECT)
.request()
.header(X_API_KEY, apiKey)
.put(Entity.entity(project, MediaType.APPLICATION_JSON));
.put(Entity.json(project));
Assert.assertEquals(201, response.getStatus(), 0);
response = target(V1_PROJECT)
.request()
.header(X_API_KEY, apiKey)
.put(Entity.entity(project, MediaType.APPLICATION_JSON));
.put(Entity.json(project));
Assert.assertEquals(409, response.getStatus(), 0);
String body = getPlainTextBody(response);
Assert.assertEquals("A project with the specified name already exists.", body);
Expand Down Expand Up @@ -401,7 +434,7 @@ public void createProjectEmptyTest() {
Response response = target(V1_PROJECT)
.request()
.header(X_API_KEY, apiKey)
.put(Entity.entity(project, MediaType.APPLICATION_JSON));
.put(Entity.json(project));
Assert.assertEquals(400, response.getStatus(), 0);
}

Expand Down

0 comments on commit f8563f9

Please sign in to comment.