From adac0b88f98098e6b56cb40f38ead6e52cf0a463 Mon Sep 17 00:00:00 2001 From: meha Date: Sat, 21 Oct 2023 16:22:50 +0100 Subject: [PATCH] Add REST API endpoints for component integrity metadata (#377) * changes for end points with integrity meta Signed-off-by: mehab * unit tests completed Signed-off-by: mehab * refactoring Signed-off-by: mehab * address pr review comments Signed-off-by: mehab * more refactoring Signed-off-by: mehab --------- Signed-off-by: mehab Signed-off-by: meha --- .../org/dependencytrack/model/Component.java | 10 + .../model/ComponentMetaInformation.java | 7 + .../persistence/ComponentQueryManager.java | 19 +- .../persistence/QueryManager.java | 28 ++- .../resources/v1/ComponentResource.java | 78 ++++++- .../util/ComponentIdentificationUtil.java | 3 +- .../persistence/QueryManagerTest.java | 50 +++++ .../resources/v1/ComponentResourceTest.java | 200 +++++++++++++++++- .../resources/v1/TeamResourceTest.java | 17 +- 9 files changed, 380 insertions(+), 32 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/ComponentMetaInformation.java create mode 100644 src/test/java/org/dependencytrack/persistence/QueryManagerTest.java diff --git a/src/main/java/org/dependencytrack/model/Component.java b/src/main/java/org/dependencytrack/model/Component.java index 42d2f2d65..404ee1a8f 100644 --- a/src/main/java/org/dependencytrack/model/Component.java +++ b/src/main/java/org/dependencytrack/model/Component.java @@ -355,6 +355,8 @@ public enum FetchGroup { private transient String licenseId; private transient DependencyMetrics metrics; private transient RepositoryMetaComponent repositoryMeta; + + private transient ComponentMetaInformation componentMetaInformation; private transient boolean isNew; private transient int usedBy; private transient Set dependencyGraph; @@ -741,6 +743,14 @@ public void setRepositoryMeta(RepositoryMetaComponent repositoryMeta) { this.repositoryMeta = repositoryMeta; } + public ComponentMetaInformation getComponentMetaInformation() { + return componentMetaInformation; + } + + public void setComponentMetaInformation(ComponentMetaInformation componentMetaInformation) { + this.componentMetaInformation = componentMetaInformation; + } + public boolean isNew() { return isNew; } diff --git a/src/main/java/org/dependencytrack/model/ComponentMetaInformation.java b/src/main/java/org/dependencytrack/model/ComponentMetaInformation.java new file mode 100644 index 000000000..f78b4c736 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/ComponentMetaInformation.java @@ -0,0 +1,7 @@ +package org.dependencytrack.model; + +import java.util.Date; + +public record ComponentMetaInformation(Date publishedDate, IntegrityMatchStatus integrityMatchStatus, + Date lastFetched) { +} diff --git a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java index 3ae986074..68079092b 100644 --- a/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ComponentQueryManager.java @@ -137,7 +137,8 @@ public List getAllComponents(Project project) { /** * Returns a List of Dependency for the specified Project. - * @param project the Project to retrieve dependencies of + * + * @param project the Project to retrieve dependencies of * @param includeMetrics Optionally includes third-party metadata about the component from external repositories * @return a List of Dependency objects */ @@ -147,15 +148,16 @@ public PaginatedResult getComponents(final Project project, final boolean includ /** * Returns a List of Dependency for the specified Project. - * @param project the Project to retrieve dependencies of + * + * @param project the Project to retrieve dependencies of * @param includeMetrics Optionally includes third-party metadata about the component from external repositories - * @param onlyOutdated Optionally exclude recent components so only outdated components are shown - * @param onlyDirect Optionally exclude transitive dependencies so only direct dependencies are shown + * @param onlyOutdated Optionally exclude recent components so only outdated components are shown + * @param onlyDirect Optionally exclude transitive dependencies so only direct dependencies are shown * @return a List of Dependency objects */ public PaginatedResult getComponents(final Project project, final boolean includeMetrics, final boolean onlyOutdated, final boolean onlyDirect) { final PaginatedResult result; - String querySring ="SELECT FROM org.dependencytrack.model.Component WHERE project == :project "; + String querySring = "SELECT FROM org.dependencytrack.model.Component WHERE project == :project "; if (filter != null) { querySring += " && (project == :project) && name.toLowerCase().matches(:name)"; } @@ -164,7 +166,7 @@ public PaginatedResult getComponents(final Project project, final boolean includ // Different should always mean version < latestVersion // Hack JDO using % instead of .* to get the SQL LIKE clause working: querySring += - " && !("+ + " && !(" + " SELECT FROM org.dependencytrack.model.RepositoryMetaComponent m " + " WHERE m.name == this.name " + " && m.namespace == this.group " + @@ -198,6 +200,7 @@ public PaginatedResult getComponents(final Project project, final boolean includ if (RepositoryType.UNSUPPORTED != type) { final RepositoryMetaComponent repoMetaComponent = getRepositoryMetaComponent(type, purl.getNamespace(), purl.getName()); component.setRepositoryMeta(repoMetaComponent); + component.setComponentMetaInformation(getMetaInformation(purl, component.getUuid())); } } } @@ -329,6 +332,7 @@ public PaginatedResult getComponents(ComponentIdentity identity, Project project if (RepositoryType.UNSUPPORTED != type) { final RepositoryMetaComponent repoMetaComponent = getRepositoryMetaComponent(type, purl.getNamespace(), purl.getName()); component.setRepositoryMeta(repoMetaComponent); + component.setComponentMetaInformation(getMetaInformation(purl, component.getUuid())); } } } @@ -445,7 +449,7 @@ protected void deleteComponents(Project project) { final Query query = pm.newQuery(Component.class, "project == :project"); query.setParameters(project); List components = query.executeList(); - for(Component component : components) { + for (Component component : components) { executeAndClose(pm.newQuery(Query.JDOQL, "DELETE FROM org.dependencytrack.model.IntegrityAnalysis WHERE component == :component"), component); } try { @@ -742,6 +746,7 @@ public Map getDependencyGraphForComponent(Project project, Co /** * Returns a list of all {@link DependencyGraphResponse} objects by {@link Component} UUID. + * * @param uuids a list of {@link Component} UUIDs * @return a list of {@link DependencyGraphResponse} objects * @since 4.9.0 diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index a19adc55f..88a9036db 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -46,6 +46,7 @@ import org.dependencytrack.model.Classifier; import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentIdentity; +import org.dependencytrack.model.ComponentMetaInformation; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Cpe; import org.dependencytrack.model.Cwe; @@ -53,6 +54,7 @@ import org.dependencytrack.model.Finding; import org.dependencytrack.model.FindingAttribution; import org.dependencytrack.model.IntegrityAnalysis; +import org.dependencytrack.model.IntegrityMatchStatus; import org.dependencytrack.model.IntegrityMetaComponent; import org.dependencytrack.model.License; import org.dependencytrack.model.LicenseGroup; @@ -1399,8 +1401,8 @@ public T detachWithGroups(final T object, final List fetchGroups) { * * @param clazz Class of the object to fetch * @param uuids {@link UUID} list of uuids to fetch + * @param Type of the object * @return The list of objects found - * @param Type of the object * @since 4.9.0 */ public List getObjectsByUuids(final Class clazz, final List uuids) { @@ -1413,8 +1415,8 @@ public List getObjectsByUuids(final Class clazz, final List uuid * * @param clazz Class of the object to fetch * @param uuids {@link UUID} list of uuids to fetch + * @param Type of the object * @return The query to execute - * @param Type of the object * @since 4.9.0 */ public Query getObjectsByUuidsQuery(final Class clazz, final List uuids) { @@ -1519,6 +1521,7 @@ public void recursivelyDeleteTeam(Team team) { /** * Returns a list of all {@link DependencyGraphResponse} objects by {@link Component} UUID. + * * @param uuids a list of {@link Component} UUIDs * @return a list of {@link DependencyGraphResponse} objects * @since 4.9.0 @@ -1529,6 +1532,7 @@ public List getComponentDependencyGraphByUuids(final Li /** * Returns a list of all {@link DependencyGraphResponse} objects by {@link ServiceComponent} UUID. + * * @param uuids a list of {@link ServiceComponent} UUIDs * @return a list of {@link DependencyGraphResponse} objects * @since 4.9.0 @@ -1539,6 +1543,7 @@ public List getServiceDependencyGraphByUuids(final List /** * Returns a list of all {@link RepositoryMetaComponent} objects by {@link RepositoryQueryManager.RepositoryMetaComponentSearch} with batchSize 10. + * * @param list a list of {@link RepositoryQueryManager.RepositoryMetaComponentSearch} * @return a list of {@link RepositoryMetaComponent} objects * @since 4.9.0 @@ -1549,7 +1554,8 @@ public List getRepositoryMetaComponentsBatch(final List /** * Returns a list of all {@link RepositoryMetaComponent} objects by {@link RepositoryQueryManager.RepositoryMetaComponentSearch} UUID. - * @param list a list of {@link RepositoryQueryManager.RepositoryMetaComponentSearch} + * + * @param list a list of {@link RepositoryQueryManager.RepositoryMetaComponentSearch} * @param batchSize the batch size * @return a list of {@link RepositoryMetaComponent} objects * @since 4.9.0 @@ -1832,4 +1838,20 @@ public IntegrityMetaComponent createIntegrityMetaComponent(IntegrityMetaComponen public IntegrityAnalysis getIntegrityAnalysisByComponentUuid(UUID uuid) { return getIntegrityAnalysisQueryManager().getIntegrityAnalysisByComponentUuid(uuid); } + + public ComponentMetaInformation getMetaInformation(PackageURL purl, UUID uuid) { + Date publishedAt = null; + Date lastFetched = null; + IntegrityMatchStatus integrityMatchStatus = null; + final IntegrityMetaComponent integrityMetaComponent = getIntegrityMetaComponent(purl.toString()); + final IntegrityAnalysis integrityAnalysis = getIntegrityAnalysisByComponentUuid(uuid); + if (integrityMetaComponent != null) { + publishedAt = integrityMetaComponent.getPublishedAt(); + lastFetched = integrityMetaComponent.getLastFetch(); + } + if (integrityAnalysis != null) { + integrityMatchStatus = integrityAnalysis.getIntegrityCheckStatus(); + } + return new ComponentMetaInformation(publishedAt, integrityMatchStatus, lastFetched); + } } diff --git a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java index 86ae18150..04d475f1d 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ComponentResource.java @@ -42,6 +42,8 @@ import org.dependencytrack.event.kafka.componentmeta.HandlerFactory; import org.dependencytrack.model.Component; import org.dependencytrack.model.ComponentIdentity; +import org.dependencytrack.model.IntegrityAnalysis; +import org.dependencytrack.model.IntegrityMetaComponent; import org.dependencytrack.model.License; import org.dependencytrack.model.Project; import org.dependencytrack.model.RepositoryMetaComponent; @@ -135,18 +137,24 @@ public Response getComponentByUuid( @ApiParam(value = "The UUID of the component to retrieve", required = true) @PathParam("uuid") String uuid, @ApiParam(value = "Optionally includes third-party metadata about the component from external repositories", required = false) - @QueryParam("includeRepositoryMetaData") boolean includeRepositoryMetaData) { + @QueryParam("includeRepositoryMetaData") boolean includeRepositoryMetaData, + @QueryParam("includeIntegrityMetaData") boolean includeIntegrityMetaData) { try (QueryManager qm = new QueryManager()) { final Component component = qm.getObjectByUuid(Component.class, uuid); if (component != null) { final Project project = component.getProject(); if (qm.hasAccess(super.getPrincipal(), project)) { final Component detachedComponent = qm.detach(Component.class, component.getId()); // TODO: Force project to be loaded. It should be anyway, but JDO seems to be having issues here. - if (includeRepositoryMetaData && detachedComponent.getPurl() != null) { + if ((includeRepositoryMetaData || includeIntegrityMetaData) && detachedComponent.getPurl() != null) { final RepositoryType type = RepositoryType.resolve(detachedComponent.getPurl()); if (RepositoryType.UNSUPPORTED != type) { - final RepositoryMetaComponent repoMetaComponent = qm.getRepositoryMetaComponent(type, detachedComponent.getPurl().getNamespace(), detachedComponent.getPurl().getName()); - detachedComponent.setRepositoryMeta(repoMetaComponent); + if (includeRepositoryMetaData) { + final RepositoryMetaComponent repoMetaComponent = qm.getRepositoryMetaComponent(type, detachedComponent.getPurl().getNamespace(), detachedComponent.getPurl().getName()); + detachedComponent.setRepositoryMeta(repoMetaComponent); + } + if (includeIntegrityMetaData) { + detachedComponent.setComponentMetaInformation(qm.getMetaInformation(component.getPurl(), component.getUuid())); + } } } return Response.ok(detachedComponent).build(); @@ -159,6 +167,68 @@ public Response getComponentByUuid( } } + @GET + @Path("/integritymetadata") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Provides the published date and hashes of the requested version of component " + + "as received from configured repositories for integrity analysis", + response = IntegrityMetaComponent.class + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 404, message = "The integrity meta information for the specified component cannot be found"), + @ApiResponse(code = 400, message = "The package url being queried for is invalid") + }) + public Response getIntegrityMetaComponent( + @ApiParam(value = "The package url of the component", required = true) + @QueryParam("purl") String purl) { + try { + final PackageURL packageURL = new PackageURL(purl); + try (QueryManager qm = new QueryManager(getAlpineRequest())) { + final RepositoryType type = RepositoryType.resolve(packageURL); + if (RepositoryType.UNSUPPORTED == type) { + return Response.noContent().build(); + } + final IntegrityMetaComponent result = qm.getIntegrityMetaComponent(packageURL.toString()); + if (result == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The integrity metadata for the specified component cannot be found.").build(); + } else { + //todo: future enhancement: provide pass-thru capability for component metadata not already present and being tracked + return Response.ok(result).build(); + } + } + } catch (MalformedPackageURLException e) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + } + + @GET + @Path("/{uuid}/integritycheckstatus") + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Provides the integrity check status of component with provided uuid based on the configured " + + "repository for integrity analysis", + response = IntegrityAnalysis.class + ) + @ApiResponses(value = { + @ApiResponse(code = 401, message = "Unauthorized"), + @ApiResponse(code = 404, message = "The integrity analysis information for the specified component cannot be found"), + }) + public Response getIntegrityStatus( + @ApiParam(value = "UUID of the component for which integrity status information is needed", required = true) + @PathParam("uuid") String uuid) { + try (QueryManager qm = new QueryManager(getAlpineRequest())) { + final IntegrityAnalysis result = qm.getIntegrityAnalysisByComponentUuid(UUID.fromString(uuid)); + if (result == null) { + return Response.status(Response.Status.NOT_FOUND).entity("The integrity status for the specified component cannot be found.").build(); + } else { + //todo: future enhancement: provide pass-thru capability for component metadata not already present and being tracked + return Response.ok(result).build(); + } + } + } + @GET @Path("/identity") @Produces(MediaType.APPLICATION_JSON) diff --git a/src/main/java/org/dependencytrack/util/ComponentIdentificationUtil.java b/src/main/java/org/dependencytrack/util/ComponentIdentificationUtil.java index 468348d75..54f0afa4f 100644 --- a/src/main/java/org/dependencytrack/util/ComponentIdentificationUtil.java +++ b/src/main/java/org/dependencytrack/util/ComponentIdentificationUtil.java @@ -30,7 +30,8 @@ */ public class ComponentIdentificationUtil { - private ComponentIdentificationUtil() { } + private ComponentIdentificationUtil() { + } @SuppressWarnings("deprecation") public static boolean doesIdentityMatch(final Component a, final org.cyclonedx.model.Component b) { diff --git a/src/test/java/org/dependencytrack/persistence/QueryManagerTest.java b/src/test/java/org/dependencytrack/persistence/QueryManagerTest.java new file mode 100644 index 000000000..589cce019 --- /dev/null +++ b/src/test/java/org/dependencytrack/persistence/QueryManagerTest.java @@ -0,0 +1,50 @@ +package org.dependencytrack.persistence; + +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.ComponentMetaInformation; +import org.dependencytrack.model.FetchStatus; +import org.dependencytrack.model.IntegrityAnalysis; +import org.dependencytrack.model.IntegrityMatchStatus; +import org.dependencytrack.model.IntegrityMetaComponent; +import org.dependencytrack.model.Project; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Date; + +import static org.dependencytrack.model.IntegrityMatchStatus.HASH_MATCH_PASSED; +import static org.dependencytrack.model.IntegrityMatchStatus.HASH_MATCH_UNKNOWN; + +public class QueryManagerTest extends PersistenceCapableTest { + @Test + public void testGetMetaInformation() { + Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false); + Component component = new Component(); + component.setProject(project); + component.setName("ABC"); + component.setPurl("pkg:maven/org.acme/abc"); + IntegrityAnalysis integrityAnalysis = new IntegrityAnalysis(); + integrityAnalysis.setComponent(component); + integrityAnalysis.setIntegrityCheckStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + Date published = new Date(); + integrityAnalysis.setUpdatedAt(published); + integrityAnalysis.setId(component.getId()); + integrityAnalysis.setMd5HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setSha1HashMatchStatus(HASH_MATCH_UNKNOWN); + integrityAnalysis.setSha256HashMatchStatus(HASH_MATCH_UNKNOWN); + integrityAnalysis.setSha512HashMatchStatus(HASH_MATCH_PASSED); + qm.persist(integrityAnalysis); + IntegrityMetaComponent integrityMetaComponent = new IntegrityMetaComponent(); + integrityMetaComponent.setPurl(component.getPurl().toString()); + integrityMetaComponent.setPublishedAt(published); + integrityMetaComponent.setLastFetch(published); + integrityMetaComponent.setStatus(FetchStatus.PROCESSED); + qm.createIntegrityMetaComponent(integrityMetaComponent); + component = qm.createComponent(component, false); + ComponentMetaInformation componentMetaInformation = qm.getMetaInformation(component.getPurl(), component.getUuid()); + Assert.assertEquals(HASH_MATCH_PASSED, componentMetaInformation.integrityMatchStatus()); + Assert.assertEquals(integrityMetaComponent.getPublishedAt(), componentMetaInformation.publishedDate()); + Assert.assertEquals(integrityMetaComponent.getLastFetch(), componentMetaInformation.lastFetched()); + } +} diff --git a/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java index d56465a7a..99627f489 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ComponentResourceTest.java @@ -27,7 +27,10 @@ import org.dependencytrack.ResourceTest; import org.dependencytrack.event.kafka.KafkaTopics; import org.dependencytrack.model.Component; +import org.dependencytrack.model.FetchStatus; import org.dependencytrack.model.IntegrityAnalysis; +import org.dependencytrack.model.IntegrityMatchStatus; +import org.dependencytrack.model.IntegrityMetaComponent; import org.dependencytrack.model.Project; import org.dependencytrack.model.RepositoryMetaComponent; import org.dependencytrack.model.RepositoryType; @@ -45,6 +48,7 @@ import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -52,6 +56,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.dependencytrack.model.IntegrityMatchStatus.HASH_MATCH_FAILED; +import static org.dependencytrack.model.IntegrityMatchStatus.HASH_MATCH_PASSED; import static org.dependencytrack.model.IntegrityMatchStatus.HASH_MATCH_UNKNOWN; public class ComponentResourceTest extends ResourceTest { @@ -75,6 +81,7 @@ public void getComponentsDefaultRequestTest() { /** * Generate a project with different dependencies + * * @return A project with 1000 dpendencies:
    *
  • 200 outdated dependencies, 75 direct and 125 transitive
  • *
  • 800 recent dependencies, 25 direct, 775 transitive
  • @@ -88,9 +95,9 @@ private Project prepareProject() throws MalformedPackageURLException { Component component = new Component(); component.setProject(project); component.setGroup("component-group"); - component.setName("component-name-"+i); - component.setVersion(String.valueOf(i)+".0"); - component.setPurl(new PackageURL(RepositoryType.MAVEN.toString(), "component-group", "component-name-"+i , String.valueOf(i)+".0", null, null)); + component.setName("component-name-" + i); + component.setVersion(String.valueOf(i) + ".0"); + component.setPurl(new PackageURL(RepositoryType.MAVEN.toString(), "component-group", "component-name-" + i, String.valueOf(i) + ".0", null, null)); component = qm.createComponent(component, false); // direct depencencies if (i < 100) { @@ -103,17 +110,17 @@ private Project prepareProject() throws MalformedPackageURLException { final var metaComponent = new RepositoryMetaComponent(); metaComponent.setRepositoryType(RepositoryType.MAVEN); metaComponent.setNamespace("component-group"); - metaComponent.setName("component-name-"+i); - metaComponent.setLatestVersion(String.valueOf(i+1)+".0"); + metaComponent.setName("component-name-" + i); + metaComponent.setLatestVersion(String.valueOf(i + 1) + ".0"); metaComponent.setLastCheck(new Date()); qm.persist(metaComponent); - } else if (i<500) { + } else if (i < 500) { // 300 recent components, 25 of these are direct dependencies final var metaComponent = new RepositoryMetaComponent(); metaComponent.setRepositoryType(RepositoryType.MAVEN); metaComponent.setNamespace("component-group"); - metaComponent.setName("component-name-"+i); - metaComponent.setLatestVersion(String.valueOf(i)+".0"); + metaComponent.setName("component-name-" + i); + metaComponent.setLatestVersion(String.valueOf(i) + ".0"); metaComponent.setLastCheck(new Date()); qm.persist(metaComponent); } else { @@ -247,6 +254,183 @@ public void getComponentByUuidWithRepositoryMetaDataTest() { Assert.assertEquals(lastCheck.getTime(), json.getJsonObject("repositoryMeta").getJsonNumber("lastCheck").longValue()); } + @Test + public void getComponentByUuidWithPublishedMetaDataTest() { + Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false); + Component component = new Component(); + component.setProject(project); + component.setName("ABC"); + component.setPurl("pkg:maven/org.acme/abc"); + IntegrityAnalysis integrityAnalysis = new IntegrityAnalysis(); + integrityAnalysis.setComponent(component); + integrityAnalysis.setIntegrityCheckStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + Date published = new Date(); + integrityAnalysis.setUpdatedAt(published); + integrityAnalysis.setId(component.getId()); + integrityAnalysis.setMd5HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setSha1HashMatchStatus(HASH_MATCH_UNKNOWN); + integrityAnalysis.setSha256HashMatchStatus(HASH_MATCH_UNKNOWN); + integrityAnalysis.setSha512HashMatchStatus(HASH_MATCH_PASSED); + qm.persist(integrityAnalysis); + IntegrityMetaComponent integrityMetaComponent = new IntegrityMetaComponent(); + integrityMetaComponent.setPurl(component.getPurl().toString()); + integrityMetaComponent.setPublishedAt(published); + integrityMetaComponent.setLastFetch(published); + integrityMetaComponent.setStatus(FetchStatus.PROCESSED); + qm.createIntegrityMetaComponent(integrityMetaComponent); + RepositoryMetaComponent meta = new RepositoryMetaComponent(); + Date lastCheck = new Date(); + meta.setLastCheck(lastCheck); + meta.setNamespace("org.acme"); + meta.setName("abc"); + meta.setLatestVersion("2.0.0"); + meta.setRepositoryType(RepositoryType.MAVEN); + qm.persist(meta); + component = qm.createComponent(component, false); + Response response = target(V1_COMPONENT + "/" + component.getUuid()) + .queryParam("includeRepositoryMetaData", true) + .queryParam("includeIntegrityMetaData", true) + .request().header(X_API_KEY, apiKey).get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("ABC", json.getString("name")); + Assert.assertEquals("MAVEN", json.getJsonObject("repositoryMeta").getString("repositoryType")); + Assert.assertEquals("org.acme", json.getJsonObject("repositoryMeta").getString("namespace")); + Assert.assertEquals("abc", json.getJsonObject("repositoryMeta").getString("name")); + Assert.assertEquals("2.0.0", json.getJsonObject("repositoryMeta").getString("latestVersion")); + Assert.assertEquals(lastCheck.getTime(), json.getJsonObject("repositoryMeta").getJsonNumber("lastCheck").longValue()); + Assert.assertEquals(published.toString(), Date.from(Instant.ofEpochSecond(json.getJsonObject("componentMetaInformation").getJsonNumber("publishedDate").longValue() / 1000)).toString()); + Assert.assertEquals(HASH_MATCH_PASSED.toString(), json.getJsonObject("componentMetaInformation").getString("integrityMatchStatus")); + Assert.assertEquals(published.toString(), Date.from(Instant.ofEpochSecond(json.getJsonObject("componentMetaInformation").getJsonNumber("lastFetched").longValue() / 1000)).toString()); + } + + + @Test + public void integrityCheckStatusPassTest() { + Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false); + Component component = new Component(); + component.setProject(project); + component.setName("ABC"); + component.setPurl("pkg:maven/org.acme/abc"); + IntegrityAnalysis integrityAnalysis = new IntegrityAnalysis(); + integrityAnalysis.setComponent(component); + integrityAnalysis.setIntegrityCheckStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + Date published = new Date(); + integrityAnalysis.setUpdatedAt(published); + integrityAnalysis.setId(component.getId()); + integrityAnalysis.setMd5HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_PASSED); + integrityAnalysis.setSha1HashMatchStatus(HASH_MATCH_UNKNOWN); + integrityAnalysis.setSha256HashMatchStatus(HASH_MATCH_UNKNOWN); + integrityAnalysis.setSha512HashMatchStatus(HASH_MATCH_PASSED); + qm.persist(integrityAnalysis); + component = qm.createComponent(component, false); + Response response = target(V1_COMPONENT + "/" + component.getUuid() + "/integritycheckstatus") + .request().header(X_API_KEY, apiKey).get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals(HASH_MATCH_PASSED.name(), json.getString("md5HashMatchStatus")); + Assert.assertEquals(HASH_MATCH_PASSED.name(), json.getString("integrityCheckStatus")); + Assert.assertEquals(HASH_MATCH_PASSED.name(), json.getString("sha512HashMatchStatus")); + Assert.assertEquals(published.toString(), Date.from(Instant.ofEpochSecond(json.getJsonNumber("updatedAt").longValue() / 1000)).toString()); + } + + @Test + public void integrityCheckStatusFailTest() { + Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false); + Component component = new Component(); + component.setProject(project); + component.setName("ABC"); + component.setPurl("pkg:maven/org.acme/abc"); + IntegrityAnalysis integrityAnalysis = new IntegrityAnalysis(); + integrityAnalysis.setComponent(component); + integrityAnalysis.setIntegrityCheckStatus(IntegrityMatchStatus.HASH_MATCH_FAILED); + Date published = new Date(); + integrityAnalysis.setUpdatedAt(published); + integrityAnalysis.setId(component.getId()); + integrityAnalysis.setMd5HashMatchStatus(IntegrityMatchStatus.HASH_MATCH_FAILED); + integrityAnalysis.setSha1HashMatchStatus(HASH_MATCH_UNKNOWN); + integrityAnalysis.setSha256HashMatchStatus(HASH_MATCH_UNKNOWN); + integrityAnalysis.setSha512HashMatchStatus(HASH_MATCH_FAILED); + qm.persist(integrityAnalysis); + component = qm.createComponent(component, false); + Response response = target(V1_COMPONENT + "/" + component.getUuid() + "/integritycheckstatus") + .request().header(X_API_KEY, apiKey).get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals(HASH_MATCH_FAILED.name(), json.getString("md5HashMatchStatus")); + Assert.assertEquals(HASH_MATCH_FAILED.name(), json.getString("integrityCheckStatus")); + Assert.assertEquals(HASH_MATCH_FAILED.name(), json.getString("sha512HashMatchStatus")); + Assert.assertEquals(published.toString(), Date.from(Instant.ofEpochSecond(json.getJsonNumber("updatedAt").longValue() / 1000)).toString()); + } + + @Test + public void integrityMetaDataFoundTest() { + Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false); + Component component = new Component(); + component.setProject(project); + component.setName("ABC"); + component.setPurl("pkg:maven/io.micrometer/micrometer-registry-prometheus@1.9.4?type=jar"); + Date published = new Date(); + component = qm.createComponent(component, false); + IntegrityMetaComponent integrityMetaComponent = new IntegrityMetaComponent(); + integrityMetaComponent.setPurl(component.getPurl().toString()); + integrityMetaComponent.setPublishedAt(published); + integrityMetaComponent.setLastFetch(published); + integrityMetaComponent.setStatus(FetchStatus.PROCESSED); + integrityMetaComponent.setRepositoryUrl("https://repo1.maven.org/maven2/io/micrometer/micrometer-registry-prometheus/1.9.4/micrometer-registry-prometheus-1.9.4.jar"); + integrityMetaComponent.setMd5("45e5bdba87362b16852ec279c254eb57"); + integrityMetaComponent.setSha1("45e5bdba87362b16852ec279c254eb57"); + qm.createIntegrityMetaComponent(integrityMetaComponent); + + Response response = target(V1_COMPONENT + "/integritymetadata") + .queryParam("purl", component.getPurl()) + .request().header(X_API_KEY, apiKey).get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertEquals("https://repo1.maven.org/maven2/io/micrometer/micrometer-registry-prometheus/1.9.4/micrometer-registry-prometheus-1.9.4.jar", json.getString("repositoryUrl")); + Assert.assertEquals("45e5bdba87362b16852ec279c254eb57", json.getString("md5")); + Assert.assertEquals("45e5bdba87362b16852ec279c254eb57", json.getString("sha1")); + Assert.assertEquals(published.toString(), Date.from(Instant.ofEpochSecond(json.getJsonNumber("publishedAt").longValue() / 1000)).toString()); + } + + @Test + public void integrityMetaDataNotFoundTest() { + Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false); + Component component = new Component(); + component.setProject(project); + component.setName("ABC"); + component.setPurl("pkg:maven/io.micrometer/micrometer-registry-prometheus@1.9.4?type=jar"); + Date published = new Date(); + component = qm.createComponent(component, false); + Response response = target(V1_COMPONENT + "/integritymetadata") + .queryParam("purl", component.getPurl()) + .request().header(X_API_KEY, apiKey).get(Response.class); + Assert.assertEquals(404, response.getStatus(), 0); + } + + @Test + public void integrityMetaDataInvalidPurlTest() { + Project project = qm.createProject("Acme Application", null, null, null, null, null, true, false); + Component component = new Component(); + component.setProject(project); + component.setName("ABC"); + component.setPurl("pkg:maven/io.micrometer/micrometer-registry-prometheus@1.9.4?type=jar"); + Date published = new Date(); + component = qm.createComponent(component, false); + Response response = target(V1_COMPONENT + "/integritymetadata") + .queryParam("purl", "component.getPurl()") + .request().header(X_API_KEY, apiKey).get(Response.class); + Assert.assertEquals(400, response.getStatus(), 0); + } + @Test public void getComponentByIdentityWithCoordinatesTest() { final Project projectA = qm.createProject("projectA", null, "1.0", null, null, null, true, false); diff --git a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java index 69ca6293e..21b846d0b 100644 --- a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java @@ -20,14 +20,13 @@ import alpine.common.util.UuidUtil; import alpine.model.ConfigProperty; +import alpine.model.Team; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -import alpine.model.Team; import org.dependencytrack.ResourceTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; -import org.dependencytrack.persistence.QueryManager; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; @@ -48,15 +47,15 @@ public class TeamResourceTest extends ResourceTest { @Override protected DeploymentContext configureDeployment() { return ServletDeploymentContext.forServlet(new ServletContainer( - new ResourceConfig(TeamResource.class) - .register(ApiFilter.class) - .register(AuthenticationFilter.class))) + new ResourceConfig(TeamResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class))) .build(); } @Test public void getTeamsTest() { - for (int i=0; i<1000; i++) { + for (int i = 0; i < 1000; i++) { qm.createTeam("Team " + i, false); } Response response = target(V1_TEAM).request() @@ -91,7 +90,7 @@ public void getTeamByInvalidUuidTest() { String body = getPlainTextBody(response); Assert.assertEquals("The team could not be found.", body); } - + @Test public void getTeamSelfTest() { initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD); @@ -187,8 +186,8 @@ public void deleteTeamWithAclTest() { if (aclToogle == null) { qm.createConfigProperty(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), "true", ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getDescription()); } else { - aclToogle.setPropertyValue("true"); - qm.persist(aclToogle); + aclToogle.setPropertyValue("true"); + qm.persist(aclToogle); } Project project = qm.createProject("Acme Example", null, "1", null, null, null, true, false); project.addAccessTeam(team);