From becb111b09bb0a026ad71624642512b8b2aa3ed1 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Thu, 20 Apr 2023 09:48:06 +0200 Subject: [PATCH 01/13] lower log level, do not swallow exception details Signed-off-by: Valentijn Scholten --- .../tasks/repositories/RepositoryMetaAnalyzerTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java b/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java index 56ddcb070d..e7500b101a 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/RepositoryMetaAnalyzerTask.java @@ -128,7 +128,7 @@ private void analyze(final QueryManager qm, final Component component) { try { cacheLoader.call(); } catch (Exception e) { - LOGGER.error("Error while fetching component meta model for component(id="+component.getId()+"; purl="+component.getPurl()+") : "+e.getMessage()); + LOGGER.warn("Error while fetching component meta model for component(id="+component.getId()+"; purl="+component.getPurl()+") : "+e.getMessage(), e); } } } From 1897537bbe039f3796d8bb1f6111970643fd4692 Mon Sep 17 00:00:00 2001 From: Florian Heubeck Date: Thu, 20 Apr 2023 12:51:12 +0200 Subject: [PATCH 02/13] Issue #2695: Add externalReferences to PATCH project Signed-off-by: Florian Heubeck --- .../persistence/ProjectQueryManager.java | 1 + .../resources/v1/ProjectResource.java | 16 +++++++-- .../resources/v1/ProjectResourceTest.java | 35 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java index 43e64996ea..64486f20a1 100644 --- a/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java @@ -517,6 +517,7 @@ public Project updateProject(Project transientProject, boolean commitIndex) { project.setCpe(transientProject.getCpe()); project.setPurl(transientProject.getPurl()); project.setSwidTagId(transientProject.getSwidTagId()); + project.setExternalReferences(transientProject.getExternalReferences()); if (Boolean.TRUE.equals(project.isActive()) && !Boolean.TRUE.equals(transientProject.isActive()) && hasActiveChild(project)){ throw new IllegalArgumentException("Project cannot be set to inactive if active children are present."); diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 7b18fb916f..58a8ba3a98 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -41,6 +41,7 @@ import org.dependencytrack.resources.v1.vo.CloneProjectRequest; import java.security.Principal; +import java.util.Collection; import java.util.List; import java.util.Set; import java.util.function.BiConsumer; @@ -326,7 +327,7 @@ public Response updateProject(Project jsonProject) { } } } - + @PATCH @Path("/{uuid}") @Consumes(MediaType.APPLICATION_JSON) @@ -393,10 +394,14 @@ public Response patchProject( modified |= project.getParent() == null || !parent.getUuid().equals(project.getParent().getUuid()); project.setParent(parent); } - if (jsonProject.getTags() != null && (!Collections.isEmpty(jsonProject.getTags()) || !Collections.isEmpty(project.getTags()))) { + if (isCollectionModified(jsonProject.getTags(), project.getTags())) { modified = true; project.setTags(jsonProject.getTags()); } + if (isCollectionModified(jsonProject.getExternalReferences(), project.getExternalReferences())) { + modified = true; + project.setExternalReferences(jsonProject.getExternalReferences()); + } if (modified) { try { project = qm.updateProject(project, true); @@ -415,6 +420,13 @@ public Response patchProject( } } + /** + * returns `true` if the given [updated] collection should be considered an update of the [original] collection. + */ + private static boolean isCollectionModified(Collection updated, Collection original) { + return updated != null && (!Collections.isEmpty(updated) || !Collections.isEmpty(original)); + } + /** * updates the given target object using the supplied setter method with the * new value from the source object using the supplied getter method. But diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index 14218fc7ce..81a4e92550 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -22,8 +22,10 @@ import alpine.notification.Notification; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; +import org.cyclonedx.model.ExternalReference.Type; import org.dependencytrack.ResourceTest; import org.dependencytrack.model.ConfigPropertyConstants; +import org.dependencytrack.model.ExternalReference; import org.dependencytrack.model.Project; import org.dependencytrack.model.Tag; import org.glassfish.jersey.client.HttpUrlConnectorProvider; @@ -544,6 +546,39 @@ public void patchProjectSuccessfullyPatchedTest() { Assert.assertEquals("tag4", jsonTags.get(0).asJsonObject().getString("name")); } + @Test + public void patchProjectExternalReferencesTest() { + final var project = qm.createProject("referred-project", "ExtRef test project", "1.0", null, null, null, true, false); + final var ref1 = new ExternalReference(); + ref1.setType(Type.VCS); + ref1.setUrl("https://github.com/DependencyTrack/awesomeness"); + final var ref2 = new ExternalReference(); + ref2.setType(Type.WEBSITE); + ref2.setUrl("https://dependencytrack.org"); + ref2.setComment("Worth a visit!"); + final var externalReferences = List.of(ref1, ref2); + final var jsonProject = new Project(); + jsonProject.setExternalReferences(externalReferences); + + final var response = target(V1_PROJECT + "/" + project.getUuid()) + .request() + .header(X_API_KEY, apiKey) + .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) + .method("PATCH", Entity.json(jsonProject)); + + Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + final var json = parseJsonObject(response); + final var patchedExternalReferences = json.getJsonArray("externalReferences"); + Assert.assertEquals(2, patchedExternalReferences.size()); + final var patchedRef1 = patchedExternalReferences.getJsonObject(0); + final var patchedRef2 = patchedExternalReferences.getJsonObject(1); + Assert.assertEquals("vcs", patchedRef1.getString("type")); + Assert.assertEquals("https://github.com/DependencyTrack/awesomeness", patchedRef1.getString("url")); + Assert.assertEquals("website", patchedRef2.getString("type")); + Assert.assertEquals("https://dependencytrack.org", patchedRef2.getString("url")); + Assert.assertEquals("Worth a visit!", patchedRef2.getString("comment")); + } + @Test public void patchProjectParentTest() { final Project parent = qm.createProject("ABC", null, "1.0", null, null, null, true, false); From 0ededb0782f5cca1b4f7846080e730474919ae68 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 23 Apr 2023 20:08:24 +0200 Subject: [PATCH 03/13] Prevent correlation of unrelated vulnerability aliases Co-authored-by: sahibamittal Signed-off-by: nscuro --- .../model/VulnerabilityAlias.java | 53 ++ .../persistence/QueryManager.java | 23 + .../VulnerabilityQueryManager.java | 171 +++-- .../VulnerabilityQueryManagerTest.java | 659 ++++++++++++++++++ 4 files changed, 854 insertions(+), 52 deletions(-) create mode 100644 src/test/java/org/dependencytrack/persistence/VulnerabilityQueryManagerTest.java diff --git a/src/main/java/org/dependencytrack/model/VulnerabilityAlias.java b/src/main/java/org/dependencytrack/model/VulnerabilityAlias.java index 7d9f066ef4..487eb76294 100644 --- a/src/main/java/org/dependencytrack/model/VulnerabilityAlias.java +++ b/src/main/java/org/dependencytrack/model/VulnerabilityAlias.java @@ -230,4 +230,57 @@ private static String firstNonNull(String first, String second) { return first != null ? first : second; } + /** + * Compute how many vulnerability identifiers of this {@link VulnerabilityAlias} + * match with those of a given other {@link VulnerabilityAlias}. + *

+ * Identifiers are considered to be matches when they are equal but not {@code null}. + * + * @param other The {@link VulnerabilityAlias} to compute matches with + * @return Number of matching identifiers + */ + public int computeMatches(final VulnerabilityAlias other) { + var matches = 0; + + if (this.getCveId() != null && this.getCveId().equals(other.getCveId())) { + matches++; + } + if (this.getGhsaId() != null && this.getGhsaId().equals(other.getGhsaId())) { + matches++; + } + if (this.getGsdId() != null && this.getGsdId().equals(other.getGsdId())) { + matches++; + } + if (this.getOsvId() != null && this.getOsvId().equals(other.getOsvId())) { + matches++; + } + if (this.getSnykId() != null && this.getSnykId().equals(other.getSnykId())) { + matches++; + } + if (this.getSonatypeId() != null && this.getSonatypeId().equals(other.getSonatypeId())) { + matches++; + } + if (this.getVulnDbId() != null && this.getVulnDbId().equals(other.getVulnDbId())) { + matches++; + } + + return matches; + } + + @Override + public String toString() { + return "VulnerabilityAlias{" + + "id=" + id + + ", internalId='" + internalId + '\'' + + ", cveId='" + cveId + '\'' + + ", ghsaId='" + ghsaId + '\'' + + ", sonatypeId='" + sonatypeId + '\'' + + ", osvId='" + osvId + '\'' + + ", snykId='" + snykId + '\'' + + ", gsdId='" + gsdId + '\'' + + ", vulnDbId='" + vulnDbId + '\'' + + ", uuid=" + uuid + + '}'; + } + } diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 11b467419b..da42e71319 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -88,6 +88,7 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.function.Supplier; /** * This QueryManager provides a concrete extension of {@link AlpineQueryManager} by @@ -1315,6 +1316,28 @@ public void runInTransaction(final Runnable runnable) { } } + /** + * Convenience method to execute a given {@link Supplier} within the context of a {@link Transaction}. + *

+ * Eventually, this may be moved to {@link alpine.persistence.AbstractAlpineQueryManager}. + * + * @param supplier The {@link Supplier} to execute + * @since 4.9.0 + */ + public T runInTransaction(final Supplier supplier) { + final Transaction trx = pm.currentTransaction(); + try { + trx.begin(); + final T result = supplier.get(); + trx.commit(); + return result; + } finally { + if (trx.isActive()) { + trx.rollback(); + } + } + } + /** * Convenience method to ensure that any active transaction is rolled back. *

diff --git a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java index 4b0d47e59f..085d821f0d 100644 --- a/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/VulnerabilityQueryManager.java @@ -38,11 +38,11 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -493,59 +493,126 @@ public List getProjects(Vulnerability vulnerability) { return projects; } - public synchronized VulnerabilityAlias synchronizeVulnerabilityAlias(VulnerabilityAlias alias) { - final Map params = new HashMap<>(); - String filter = ""; - if (alias.getCveId() != null) { - filter += "(cveId == :cveId)"; - params.put("cveId", alias.getCveId()); - } - if (alias.getSonatypeId() != null) { - if (filter.length() > 0) filter += " && "; - filter += "(sonatypeId == :sonatypeId || sonatypeId == null)"; - params.put("sonatypeId", alias.getSonatypeId()); - } - if (alias.getGhsaId() != null) { - if (filter.length() > 0) filter += " && "; - filter += "(ghsaId == :ghsaId || ghsaId == null)"; - params.put("ghsaId", alias.getGhsaId()); - } - if (alias.getOsvId() != null) { - if (filter.length() > 0) filter += " && "; - filter += "(osvId == :osvId || osvId == null)"; - params.put("osvId", alias.getOsvId()); - } - if (alias.getSnykId() != null) { - if (filter.length() > 0) filter += " && "; - filter += "(snykId == :snykId || snykId == null)"; - params.put("snykId", alias.getSnykId()); - } - if (alias.getGsdId() != null) { - if (filter.length() > 0) filter += " && "; - filter += "(gsdId == :gsdId || gsdId == null)"; - params.put("gsdId", alias.getGsdId()); - } - if (alias.getVulnDbId() != null) { - if (filter.length() > 0) filter += " && "; - filter += "(vulnDbId == :vulnDbId || vulnDbId == null)"; - params.put("vulnDbId", alias.getVulnDbId()); - } - if (alias.getInternalId() != null) { - if (filter.length() > 0) filter += " && "; - filter += "(internalId == :internalId || internalId == null)"; - params.put("internalId", alias.getInternalId()); - } - final Query query = pm.newQuery(VulnerabilityAlias.class); - query.setFilter(filter); - final VulnerabilityAlias existingAlias = singleResult(query.executeWithMap(params)); - if (existingAlias != null) { - existingAlias.copyFieldsFrom(alias); - return persist(existingAlias); - } else { - return persist(alias); - } + public synchronized VulnerabilityAlias synchronizeVulnerabilityAlias(final VulnerabilityAlias alias) { + return runInTransaction(() -> { + // Query existing aliases that match AT LEAST ONE identifier of the given alias. + // + // For each data source, we want to know the existing aliases where the respective identifier either: + // 1. matches, or + // 2. is not set (is null) + // + // Given the existing alias: + // {cveId: "CVE-123", ghsaId: "GHSA-123"} + // The logic allows us to merge it with this incoming alias: + // {cveId: "CVE-123", sonatypeId: "OSSINDEX-123"} + // Forming the final result of: + // {cveId: "CVE-123", ghsaId: "GHSA-123", sonatypeId: "OSSINDEX-123"} + // Because CVE-123 aliases GHSA-123, and CVE-123 aliases OSSINDEX-123, we can infer that GHSA-123 aliases OSSINDEX-123. + // + // If the given alias has both a CVE and a GHSA ID, the final query will look like this: + // (cveId == :cveId || cveId == null) && (ghsaId == :ghsaId || ghsaId == null) + // && (cveId != null || ghsaId != null) + // + // Note that this logic only works for "true" aliases, not for "related" vulnerabilities. + // Some data sources will provide advisories, which combine multiple vulnerabilities into one, + // but still advertise them as aliases. See https://github.com/google/osv.dev/issues/888 for example. + var filter = ""; + var mustMatchAnyFilter = ""; + final var params = new HashMap(); + if (alias.getCveId() != null) { + filter += "(cveId == :cveId || cveId == null)"; + mustMatchAnyFilter += "cveId != null"; + params.put("cveId", alias.getCveId()); + } + if (alias.getSonatypeId() != null) { + if (filter.length() > 0) { + filter += " && "; + mustMatchAnyFilter += " || "; + } + filter += "(sonatypeId == :sonatypeId || sonatypeId == null)"; + mustMatchAnyFilter += "sonatypeId != null"; + params.put("sonatypeId", alias.getSonatypeId()); + } + if (alias.getGhsaId() != null) { + if (filter.length() > 0) { + filter += " && "; + mustMatchAnyFilter += " || "; + } + filter += "(ghsaId == :ghsaId || ghsaId == null)"; + mustMatchAnyFilter += "ghsaId != null"; + params.put("ghsaId", alias.getGhsaId()); + } + if (alias.getOsvId() != null) { + if (filter.length() > 0) { + filter += " && "; + mustMatchAnyFilter += " || "; + } + filter += "(osvId == :osvId || osvId == null)"; + mustMatchAnyFilter += "osvId != null"; + params.put("osvId", alias.getOsvId()); + } + if (alias.getSnykId() != null) { + if (filter.length() > 0) { + filter += " && "; + mustMatchAnyFilter += " || "; + } + filter += "(snykId == :snykId || snykId == null)"; + mustMatchAnyFilter += "snykId != null"; + params.put("snykId", alias.getSnykId()); + } + if (alias.getGsdId() != null) { + if (filter.length() > 0) { + filter += " && "; + mustMatchAnyFilter += " || "; + } + filter += "(gsdId == :gsdId || gsdId == null)"; + mustMatchAnyFilter += "gsdId != null"; + params.put("gsdId", alias.getGsdId()); + } + if (alias.getVulnDbId() != null) { + if (filter.length() > 0) { + filter += " && "; + mustMatchAnyFilter += " || "; + } + filter += "(vulnDbId == :vulnDbId || vulnDbId == null)"; + mustMatchAnyFilter += "vulnDbId != null"; + params.put("vulnDbId", alias.getVulnDbId()); + } + if (alias.getInternalId() != null) { + if (filter.length() > 0) { + filter += " && "; + mustMatchAnyFilter += " || "; + } + filter += "(internalId == :internalId || internalId == null)"; + mustMatchAnyFilter += "internalId != null"; + params.put("internalId", alias.getInternalId()); + } + filter += " && (" + mustMatchAnyFilter + ")"; + final Query query = pm.newQuery(VulnerabilityAlias.class); + query.setFilter(filter); + query.setNamedParameters(params); + final List candidates = query.executeList(); + if (candidates.isEmpty()) { + // No matches at all; Create new alias. + return pm.makePersistent(alias); + } + + final VulnerabilityAlias bestMatch; + if (candidates.size() > 1) { + // In case there are multiple candidates, find candidate with most matching identifiers. + bestMatch = candidates.stream() + .max(Comparator.comparingInt(alias::computeMatches)) + .get(); // Safe because we checked candidates for emptiness before. + } else { + bestMatch = candidates.get(0); + } + + bestMatch.copyFieldsFrom(alias); + return bestMatch; + }); } + @SuppressWarnings("unchecked") public List getVulnerabilityAliases(Vulnerability vulnerability) { final Query query; diff --git a/src/test/java/org/dependencytrack/persistence/VulnerabilityQueryManagerTest.java b/src/test/java/org/dependencytrack/persistence/VulnerabilityQueryManagerTest.java new file mode 100644 index 0000000000..01f6be9e6d --- /dev/null +++ b/src/test/java/org/dependencytrack/persistence/VulnerabilityQueryManagerTest.java @@ -0,0 +1,659 @@ +/* + * 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.persistence; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.VulnerabilityAlias; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +import javax.jdo.Query; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.persistence.VulnerabilityQueryManagerTest.SynchronizeVulnerabilityAliasTest.VulnerabilityAliasBuilder.anAlias; + +@RunWith(Suite.class) +@SuiteClasses(VulnerabilityQueryManagerTest.SynchronizeVulnerabilityAliasTest.class) +public class VulnerabilityQueryManagerTest { + + @RunWith(JUnitParamsRunner.class) + public static class SynchronizeVulnerabilityAliasTest extends PersistenceCapableTest { + + @Test + @SuppressWarnings({"unchecked", "resource", "JUnitMalformedDeclaration"}) + @Parameters(method = "synchronizeVulnerabilityAliasTestParams") + public void synchronizeVulnerabilityAliasTest(final String description, + final List reportedAliases, + final List expectedAliases) { + for (final VulnerabilityAlias reportedAlias : reportedAliases) { + qm.synchronizeVulnerabilityAlias(reportedAlias); + } + + final var aliasAsserts = new ArrayList>(); + for (final VulnerabilityAlias expectedAlias : expectedAliases) { + aliasAsserts.add(alias -> { + assertThat(alias.getCveId()).isEqualTo(expectedAlias.getCveId()); + assertThat(alias.getGhsaId()).isEqualTo(expectedAlias.getGhsaId()); + assertThat(alias.getGsdId()).isEqualTo(expectedAlias.getGsdId()); + assertThat(alias.getInternalId()).isEqualTo(expectedAlias.getInternalId()); + assertThat(alias.getOsvId()).isEqualTo(expectedAlias.getOsvId()); + assertThat(alias.getSnykId()).isEqualTo(expectedAlias.getSnykId()); + assertThat(alias.getSonatypeId()).isEqualTo(expectedAlias.getSonatypeId()); + assertThat(alias.getVulnDbId()).isEqualTo(expectedAlias.getVulnDbId()); + }); + } + + final Query query = qm.getPersistenceManager().newQuery(VulnerabilityAlias.class); + final List aliases = query.executeList(); + + assertThat(aliases).as(description).satisfiesExactlyInAnyOrder(aliasAsserts.toArray(new Consumer[0])); + } + + @SuppressWarnings("unused") + private Object[] synchronizeVulnerabilityAliasTestParams() { + return new Object[]{ + new Object[]{ + "Aliases must not be merged when identifiers do not match", + // Reported + Arrays.asList( + anAlias() + .withGhsaId("GHSA-1000") + .withSnykId("SNYK-2000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build()), + // Expected + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build(), + anAlias() + .withGhsaId("GHSA-1000") + .withSnykId("SNYK-2000") + .build()) + }, + new Object[]{ + "Must merge with best match among existing aliases", + // Reported + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withOsvId("GO-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withOsvId("GO-2000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withOsvId("GO-2000") + .withSnykId("SNYK-1000") + .build()), + // Expected + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withOsvId("GO-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withOsvId("GO-2000") + .withSnykId("SNYK-1000") + .build()) + }, + new Object[]{ + "Aliases with disjoint sets of identifiers must not be merged", + // Reported + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build(), + anAlias() + .withGhsaId("GHSA-1000") + .withOsvId("GO-1000") + .build()), + // Expected + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build(), + anAlias() + .withGhsaId("GHSA-1000") + .withOsvId("GO-1000") + .build()), + }, + new Object[]{ + "Aliases with disjoint sets of identifiers must not be merged when arriving in reverse order", + // Reported + Arrays.asList( + anAlias() + .withGhsaId("GHSA-1000") + .withOsvId("GO-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build()), + // Expected + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build(), + anAlias() + .withGhsaId("GHSA-1000") + .withOsvId("GO-1000") + .build()), + }, + new Object[]{ + "Aliases with one matching identifier must be merged", + // Reported + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withOsvId("GO-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build()), + // Expected + singletonList( + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .withOsvId("GO-1000") + .build()) + }, + new Object[]{ + "Aliases with one matching identifier must be merged when arriving in reverse order", + // Reported + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withOsvId("GO-1000") + .build()), + // Expected + singletonList( + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .withOsvId("GO-1000") + .build()) + }, + new Object[]{ + "Aliases with one matching identifier and multiple additional identifiers must be merged", + // Reported + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withGhsaId("GHSA-1000") + .withOsvId("GO-1000") + .build()), + // Expected + singletonList( + anAlias() + .withCveId("CVE-1000") + .withGhsaId("GHSA-1000") + .withSnykId("SNYK-1000") + .withOsvId("GO-1000") + .build()) + }, + new Object[]{ + "Aliases with one matching identifier and multiple additional identifiers must be merged when arriving in reverse order", + // Reported + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withGhsaId("GHSA-1000") + .withOsvId("GO-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build()), + // Expected + singletonList( + anAlias() + .withCveId("CVE-1000") + .withGhsaId("GHSA-1000") + .withSnykId("SNYK-1000") + .withOsvId("GO-1000") + .build()) + }, + new Object[]{ + "Aliases with two matching identifiers must be merged", + // Reported + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withGhsaId("GHSA-1000") + .withSnykId("SNYK-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withGhsaId("GHSA-1000") + .withOsvId("GO-1000") + .build()), + // Expected + singletonList( + anAlias() + .withCveId("CVE-1000") + .withGhsaId("GHSA-1000") + .withSnykId("SNYK-1000") + .withOsvId("GO-1000") + .build()) + }, + new Object[]{ + "Aliases with two matching identifiers must be merged when arriving in reverse order", + // Reported + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withGhsaId("GHSA-1000") + .withOsvId("GO-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withGhsaId("GHSA-1000") + .withSnykId("SNYK-1000") + .build()), + // Expected + singletonList( + anAlias() + .withCveId("CVE-1000") + .withGhsaId("GHSA-1000") + .withSnykId("SNYK-1000") + .withOsvId("GO-1000") + .build()) + }, + new Object[]{ + "Aliases with one matching and one conflicting identifier must not be merged", + // Reported + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withOsvId("GO-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withOsvId("GO-2000") + .build()), + // Expected + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withOsvId("GO-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withOsvId("GO-2000") + .build()) + }, + new Object[]{ + "In-the-wild example: GO-2022-0586", + // Reported + Arrays.asList( + anAlias() + .withCveId("CVE-2022-26945") + .withOsvId("GO-2022-0586") + .build(), + anAlias() + .withCveId("CVE-2022-30321") + .withOsvId("GO-2022-0586") + .build(), + anAlias() + .withCveId("CVE-2022-30322") + .withOsvId("GO-2022-0586") + .build(), + anAlias() + .withCveId("CVE-2022-30323") + .withOsvId("GO-2022-0586") + .build(), + anAlias() + .withGhsaId("GHSA-28r2-q6m8-9hpx") + .withOsvId("GO-2022-0586") + .build(), + anAlias() + .withGhsaId("GHSA-cjr4-fv6c-f3mv") + .withOsvId("GO-2022-0586") + .build(), + anAlias() + .withGhsaId("GHSA-fcgg-rvwg-jv58") + .withOsvId("GO-2022-0586") + .build(), + anAlias() + .withGhsaId("GHSA-x24g-9w7v-vprh") + .withOsvId("GO-2022-0586") + .build()), + // Expected + Arrays.asList( + anAlias() + .withCveId("CVE-2022-26945") + .withOsvId("GO-2022-0586") + .withGhsaId("GHSA-28r2-q6m8-9hpx") + .build(), + anAlias() + .withCveId("CVE-2022-30321") + .withGhsaId("GHSA-cjr4-fv6c-f3mv") + .withOsvId("GO-2022-0586") + .build(), + anAlias() + .withCveId("CVE-2022-30322") + .withGhsaId("GHSA-fcgg-rvwg-jv58") + .withOsvId("GO-2022-0586") + .build(), + anAlias() + .withCveId("CVE-2022-30323") + .withGhsaId("GHSA-x24g-9w7v-vprh") + .withOsvId("GO-2022-0586") + .build()) + }, + new Object[]{ + "Aliases with identical identifiers must not create duplicates", + // Reported + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build()), + // Expected + singletonList( + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build()), + }, + new Object[]{ + "In-the-wild example: SNYK-JAVA-ORGECLIPSEJETTY-2945452 and SNYK-JAVA-ORGECLIPSEJETTY-2945453", + // Reported + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-1000") + .build(), + anAlias() + .withGhsaId("GHSA-1000") + .withSnykId("SNYK-1000") + .build(), + anAlias() + .withGhsaId("GHSA-1000") + .withSnykId("SNYK-2000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withSnykId("SNYK-2000") + .build()), + // Expected + Arrays.asList( + anAlias() + .withCveId("CVE-1000") + .withGhsaId("GHSA-1000") + .withSnykId("SNYK-1000") + .build(), + anAlias() + .withCveId("CVE-1000") + .withGhsaId("GHSA-1000") + .withSnykId("SNYK-2000") + .build()) + }, + // NOTE: This test is meant to act as documentation of the solution's behavior + // in case ambiguous data is being reported by vulnerability sources. + // In this case, one Snyk vulnerability refers to multiple CVEs and GHSAs. + // We have no way of knowing which CVEs and GHSAs alias each other. + // Synchronization may cause unrelated CVEs and GHSAs to be correlated. + new Object[]{ + "In-the-wild example: SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135", + // Reported + Arrays.asList( + // Snyk reports various GHSAs and one CVE as aliases of SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135. + anAlias() + .withGhsaId("GHSA-9fwf-46g9-45rx") + .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") + .build(), + anAlias() + .withGhsaId("GHSA-3f7h-mf4q-vrm4") + .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") + .build(), + anAlias() + .withGhsaId("GHSA-5hc5-c3m9-8vcj") + .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") + .build(), + anAlias() + .withGhsaId("GHSA-fv22-xp26-mm9w") + .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") + .build(), + anAlias() + .withGhsaId("GHSA-4rv7-wj6m-6c6r") + .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") + .build(), + anAlias() + .withCveId("CVE-2022-40152") + .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") + .build(), + // GitHub Advisories and / or OSV report relationships between CVEs and GHSAs. + anAlias() + .withCveId("CVE-2022-40152") + .withGhsaId("GHSA-3f7h-mf4q-vrm4") + .build(), + anAlias() + .withCveId("CVE-2022-40154") + .withGhsaId("GHSA-9fwf-46g9-45rx") + .build(), + anAlias() + .withCveId("CVE-2022-40156") + .withGhsaId("GHSA-4rv7-wj6m-6c6r") + .build(), + anAlias() + .withCveId("CVE-2022-40153") + .withGhsaId("GHSA-fv22-xp26-mm9w") + .build(), + anAlias() + .withCveId("CVE-2022-40155") + .withGhsaId("GHSA-5hc5-c3m9-8vcj") + .build()), + // Expected (what it should be) +// Arrays.asList( +// anAlias() +// .withCveId("CVE-2022-40152") +// .withGhsaId("GHSA-3f7h-mf4q-vrm4") +// .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") +// .build(), +// anAlias() +// .withCveId("CVE-2022-40153") +// .withGhsaId("GHSA-fv22-xp26-mm9w") +// .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") +// .build(), +// anAlias() +// .withCveId("CVE-2022-40154") +// .withGhsaId("GHSA-9fwf-46g9-45rx") +// .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") +// .build(), +// anAlias() +// .withCveId("CVE-2022-40155") +// .withGhsaId("GHSA-5hc5-c3m9-8vcj") +// .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") +// .build(), +// anAlias() +// .withCveId("CVE-2022-40156") +// .withGhsaId("GHSA-4rv7-wj6m-6c6r") +// .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") +// .build()) + // Expected (what it is) + Arrays.asList( + anAlias() + .withCveId("CVE-2022-40152") + .withGhsaId("GHSA-9fwf-46g9-45rx") // MISMATCH: Should be GHSA-3f7h-mf4q-vrm4 + .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") + .build(), + anAlias() + .withCveId("CVE-2022-40152") + .withGhsaId("GHSA-3f7h-mf4q-vrm4") + .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") + .build(), + anAlias() + .withCveId("CVE-2022-40155") + .withGhsaId("GHSA-5hc5-c3m9-8vcj") + .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") + .build(), + anAlias() + .withCveId("CVE-2022-40153") + .withGhsaId("GHSA-fv22-xp26-mm9w") + .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") + .build(), + anAlias() + .withCveId("CVE-2022-40156") + .withGhsaId("GHSA-4rv7-wj6m-6c6r") + .withSnykId("SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135") + .build(), + anAlias() + .withCveId("CVE-2022-40154") + .withGhsaId("GHSA-9fwf-46g9-45rx") + // Should have SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135 + .build()) + }, + new Object[]{ + "In-the-wild example: SNYK-JAVA-IONETTY-10822*", + // Reported + Arrays.asList( + // Multiple SNYK vulnerabilities refer to the same CVE and GHSA pair. + anAlias() + .withCveId("CVE-2021-21290") + .withSnykId("SNYK-JAVA-IONETTY-1082234") + .build(), + anAlias() + .withGhsaId("GHSA-5mcr-gq6c-3hq2") + .withSnykId("SNYK-JAVA-IONETTY-1082234") + .build(), + anAlias() + .withCveId("CVE-2021-21290") + .withSnykId("SNYK-JAVA-IONETTY-1082235") + .build(), + anAlias() + .withGhsaId("GHSA-5mcr-gq6c-3hq2") + .withSnykId("SNYK-JAVA-IONETTY-1082235") + .build(), + anAlias() + .withCveId("CVE-2021-21290") + .withSnykId("SNYK-JAVA-IONETTY-1082236") + .build(), + anAlias() + .withGhsaId("GHSA-5mcr-gq6c-3hq2") + .withSnykId("SNYK-JAVA-IONETTY-1082236") + .build(), + anAlias() + .withCveId("CVE-2021-21290") + .withSnykId("SNYK-JAVA-IONETTY-1082238") + .build(), + anAlias() + .withGhsaId("GHSA-5mcr-gq6c-3hq2") + .withSnykId("SNYK-JAVA-IONETTY-1082238") + .build(), + // GitHub Advisories and / or OSV report the CVE and GHSA to be aliases. + anAlias() + .withCveId("CVE-2021-21290") + .withGhsaId("GHSA-5mcr-gq6c-3hq2") + .build()), + // Expected + Arrays.asList( + anAlias() + .withCveId("CVE-2021-21290") + .withGhsaId("GHSA-5mcr-gq6c-3hq2") + .withSnykId("SNYK-JAVA-IONETTY-1082234") + .build(), + anAlias() + .withCveId("CVE-2021-21290") + .withGhsaId("GHSA-5mcr-gq6c-3hq2") + .withSnykId("SNYK-JAVA-IONETTY-1082235") + .build(), + anAlias() + .withCveId("CVE-2021-21290") + .withGhsaId("GHSA-5mcr-gq6c-3hq2") + .withSnykId("SNYK-JAVA-IONETTY-1082236") + .build(), + anAlias() + .withCveId("CVE-2021-21290") + .withGhsaId("GHSA-5mcr-gq6c-3hq2") + .withSnykId("SNYK-JAVA-IONETTY-1082238") + .build()) + } + }; + } + + static class VulnerabilityAliasBuilder { + private final VulnerabilityAlias alias = new VulnerabilityAlias(); + + static VulnerabilityAliasBuilder anAlias() { + return new VulnerabilityAliasBuilder(); + } + + VulnerabilityAlias build() { + return alias; + } + + VulnerabilityAliasBuilder withCveId(final String cveId) { + this.alias.setCveId(cveId); + return this; + } + + VulnerabilityAliasBuilder withGhsaId(final String ghsaId) { + this.alias.setGhsaId(ghsaId); + return this; + } + + VulnerabilityAliasBuilder withOsvId(final String osvId) { + this.alias.setOsvId(osvId); + return this; + } + + VulnerabilityAliasBuilder withSnykId(final String snykId) { + this.alias.setSnykId(snykId); + return this; + } + } + + } + +} \ No newline at end of file From 0cb28971cb39d015bbd37686c47eb6c5d078932c Mon Sep 17 00:00:00 2001 From: nscuro Date: Wed, 26 Apr 2023 00:38:15 +0200 Subject: [PATCH 04/13] Fix NPE for email alerts without explicit destination Fixes #2698 Signed-off-by: nscuro --- .../publisher/SendMailPublisher.java | 13 ++++--- .../publisher/SendMailPublisherTest.java | 36 ++++++++++++++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java index ed80d178f3..3794d947cc 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/SendMailPublisher.java @@ -33,6 +33,7 @@ import org.dependencytrack.persistence.QueryManager; import javax.json.JsonObject; +import javax.json.JsonString; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -122,17 +123,21 @@ public PebbleEngine getTemplateEngine() { } static String[] parseDestination(final JsonObject config) { - String destinationString = config.getString("destination"); - if ((destinationString == null) || destinationString.isEmpty()) { + JsonString destinationString = config.getJsonString("destination"); + if ((destinationString == null) || destinationString.getString().isEmpty()) { return null; } - return destinationString.split(","); + return destinationString.getString().split(","); } static String[] parseDestination(final JsonObject config, final List teams) { String[] destination = teams.stream().flatMap( team -> Stream.of( - Arrays.stream(config.getString("destination").split(",")).filter(Predicate.not(String::isEmpty)), + Optional.ofNullable(config.getJsonString("destination")) + .map(JsonString::getString) + .stream() + .flatMap(dest -> Arrays.stream(dest.split(","))) + .filter(Predicate.not(String::isEmpty)), Optional.ofNullable(team.getManagedUsers()).orElseGet(Collections::emptyList).stream().map(ManagedUser::getEmail).filter(Objects::nonNull), Optional.ofNullable(team.getLdapUsers()).orElseGet(Collections::emptyList).stream().map(LdapUser::getEmail).filter(Objects::nonNull), Optional.ofNullable(team.getOidcUsers()).orElseGet(Collections::emptyList).stream().map(OidcUser::getEmail).filter(Objects::nonNull) diff --git a/src/test/java/org/dependencytrack/notification/publisher/SendMailPublisherTest.java b/src/test/java/org/dependencytrack/notification/publisher/SendMailPublisherTest.java index d6c5d4e6ed..4a5cbb661a 100644 --- a/src/test/java/org/dependencytrack/notification/publisher/SendMailPublisherTest.java +++ b/src/test/java/org/dependencytrack/notification/publisher/SendMailPublisherTest.java @@ -17,7 +17,6 @@ private static JsonObject configWithDestination(final String destination) { return Json.createObjectBuilder().add("destination", destination).build(); } - @Test public void testSingleDestination() { JsonObject config = configWithDestination("john@doe.com"); @@ -32,6 +31,10 @@ public void testMultipleDestinations() { SendMailPublisher.parseDestination(config)); } + @Test + public void testNullDestination() { + Assert.assertArrayEquals(null, SendMailPublisher.parseDestination(Json.createObjectBuilder().build())); + } @Test public void testEmptyDestinations() { @@ -293,6 +296,37 @@ public void testConfigDestinationAndTeamAsDestination(){ Assert.assertArrayEquals(new String[] {"john@doe.com", "steve@jobs.org", "managedUser@Test.com", "ldapUser@Test.com"}, SendMailPublisher.parseDestination(config, teams)); } + @Test + public void testNullConfigDestinationAndTeamsDestination() { + JsonObject config = Json.createObjectBuilder().build(); + ManagedUser managedUser = new ManagedUser(); + managedUser.setUsername("ManagedUserTest"); + managedUser.setEmail("managedUser@Test.com"); + List managedUsers = new ArrayList<>(); + managedUsers.add(managedUser); + + LdapUser ldapUser = new LdapUser(); + ldapUser.setUsername("ldapUserTest"); + ldapUser.setEmail("ldapUser@Test.com"); + List ldapUsers = new ArrayList<>(); + ldapUsers.add(ldapUser); + + OidcUser oidcUser = new OidcUser(); + oidcUser.setUsername("oidcUserTest"); + oidcUser.setEmail("john@doe.com"); + List oidcUsers = new ArrayList<>(); + oidcUsers.add(oidcUser); + + List teams = new ArrayList<>(); + Team team = new Team(); + team.setManagedUsers(managedUsers); + team.setLdapUsers(ldapUsers); + team.setOidcUsers(oidcUsers); + teams.add(team); + + Assert.assertArrayEquals(new String[] {"managedUser@Test.com", "ldapUser@Test.com", "john@doe.com"}, SendMailPublisher.parseDestination(config, teams)); + } + @Test public void testEmptyManagedUsersAsDestination(){ JsonObject config = configWithDestination("john@doe.com,steve@jobs.org"); From f6d64b03a63fdb35f3ab58c774e2c97008bc0028 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 30 Apr 2023 19:25:38 +0200 Subject: [PATCH 05/13] Fix broken pagination in DefectDojo integration Additionally: * Only fetch existing tests when reimport is actually enabled * Add proper tests that verify the entirety of the integration instead of only individual parts * Add test case for manual testing against a local DefectDojo instance Fixes #2707 Signed-off-by: nscuro --- .../defectdojo/DefectDojoClient.java | 21 +- .../defectdojo/DefectDojoUploader.java | 8 +- .../defectdojo/DefectDojoClientTest.java | 112 --- .../defectdojo/DefectDojoUploaderTest.java | 687 +++++++++++++++++- 4 files changed, 694 insertions(+), 134 deletions(-) delete mode 100644 src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoClientTest.java diff --git a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java index 56bff6e7c7..46d7edbc38 100644 --- a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java +++ b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java @@ -19,6 +19,7 @@ package org.dependencytrack.integrations.defectdojo; import alpine.common.logging.Logger; +import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; @@ -69,7 +70,7 @@ public void uploadDependencyTrackFindings(final String token, final String engag .addPart("active", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) .addPart("minimum_severity", new StringBody("Info", ContentType.MULTIPART_FORM_DATA)) .addPart("close_old_findings", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) - .addPart("push_to_jira", new StringBody("push_to_jira", ContentType.MULTIPART_FORM_DATA)) + .addPart("push_to_jira", new StringBody("false", ContentType.MULTIPART_FORM_DATA)) .addPart("scan_date", new StringBody(DATE_FORMAT.format(new Date()), ContentType.MULTIPART_FORM_DATA)) .build(); request.setEntity(data); @@ -87,7 +88,7 @@ public void uploadDependencyTrackFindings(final String token, final String engag } // Pulling DefectDojo 'tests' API endpoint with engagementID filter on, and retrieve a list of existing tests - public ArrayList getDojoTestIds(final String token, final String eid) { + public ArrayList getDojoTestIds(final String token, final String eid) { LOGGER.debug("Pulling DefectDojo Tests API ..."); String testsUri = "/api/v2/tests/"; LOGGER.debug("Make the first pagination call"); @@ -104,17 +105,15 @@ public ArrayList getDojoTestIds(final String token, final String eid) { String stringResponse = EntityUtils.toString(response.getEntity()); JSONObject dojoObj = new JSONObject(stringResponse); JSONArray dojoArray = dojoObj.getJSONArray("results"); - ArrayList dojoTests = jsonToList(dojoArray); - String nextUrl = ""; - while (dojoObj.get("next") != null) { - nextUrl = dojoObj.get("next").toString(); + ArrayList dojoTests = jsonToList(dojoArray); + while (StringUtils.isNotBlank(dojoObj.optString("next"))) { + final String nextUrl = dojoObj.getString("next"); LOGGER.debug("Making the subsequent pagination call on " + nextUrl); uriBuilder = new URIBuilder(nextUrl); request = new HttpGet(uriBuilder.build().toString()); request.addHeader("accept", "application/json"); request.addHeader("Authorization", "Token " + token); try (CloseableHttpResponse response1 = HttpClientPool.getClient().execute(request)) { - nextUrl = dojoObj.get("next").toString(); stringResponse = EntityUtils.toString(response1.getEntity()); } dojoObj = new JSONObject(stringResponse); @@ -136,9 +135,9 @@ public ArrayList getDojoTestIds(final String token, final String eid) { } // Given the engagement id and scan type, search for existing test id - public String getDojoTestId(final String engagementID, final ArrayList dojoTests) { + public String getDojoTestId(final String engagementID, final ArrayList dojoTests) { for (int i = 0; i < dojoTests.size(); i++) { - String s = dojoTests.get(i).toString(); + String s = dojoTests.get(i); JSONObject dojoTest = new JSONObject(s); if (dojoTest.get("engagement").toString().equals(engagementID) && dojoTest.get("scan_type").toString().equals("Dependency Track Finding Packaging Format (FPF) Export")) { @@ -150,7 +149,7 @@ public String getDojoTestId(final String engagementID, final ArrayList dojoTests // JSONArray to ArrayList simple converter public ArrayList jsonToList(final JSONArray jsonArray) { - ArrayList list = new ArrayList(); + ArrayList list = new ArrayList<>(); if (jsonArray != null) { for (int i = 0; i < jsonArray.length(); i++) { list.add(jsonArray.get(i).toString()); @@ -177,7 +176,7 @@ public void reimportDependencyTrackFindings(final String token, final String eng .addPart("active", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) .addPart("minimum_severity", new StringBody("Info", ContentType.MULTIPART_FORM_DATA)) .addPart("close_old_findings", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) - .addPart("push_to_jira", new StringBody("push_to_jira", ContentType.MULTIPART_FORM_DATA)) + .addPart("push_to_jira", new StringBody("false", ContentType.MULTIPART_FORM_DATA)) .addPart("do_not_reactivate", new StringBody(doNotReactivate.toString(), ContentType.MULTIPART_FORM_DATA)) .addPart("test", new StringBody(testId, ContentType.MULTIPART_FORM_DATA)) .addPart("scan_date", new StringBody(DATE_FORMAT.format(new Date()), ContentType.MULTIPART_FORM_DATA)) diff --git a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java index 9ded0c4ecd..a20248ab98 100644 --- a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java +++ b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploader.java @@ -97,13 +97,13 @@ public InputStream process(final Project project, final List findings) public void upload(final Project project, final InputStream payload) { final ConfigProperty defectDojoUrl = qm.getConfigProperty(DEFECTDOJO_URL.getGroupName(), DEFECTDOJO_URL.getPropertyName()); final ConfigProperty apiKey = qm.getConfigProperty(DEFECTDOJO_API_KEY.getGroupName(), DEFECTDOJO_API_KEY.getPropertyName()); - final ConfigProperty globalReimportEnabled = qm.getConfigProperty(DEFECTDOJO_REIMPORT_ENABLED.getGroupName(), DEFECTDOJO_REIMPORT_ENABLED.getPropertyName()); + final boolean globalReimportEnabled = qm.isEnabled(DEFECTDOJO_REIMPORT_ENABLED); final ProjectProperty engagementId = qm.getProjectProperty(project, DEFECTDOJO_ENABLED.getGroupName(), ENGAGEMENTID_PROPERTY); try { final DefectDojoClient client = new DefectDojoClient(this, new URL(defectDojoUrl.getPropertyValue())); - final ArrayList testsIds = client.getDojoTestIds(apiKey.getPropertyValue(), engagementId.getPropertyValue()); - final String testId = client.getDojoTestId(engagementId.getPropertyValue(), testsIds); - if (isReimportConfigured(project) || Boolean.parseBoolean(globalReimportEnabled.getPropertyValue())) { + if (isReimportConfigured(project) || globalReimportEnabled) { + final ArrayList testsIds = client.getDojoTestIds(apiKey.getPropertyValue(), engagementId.getPropertyValue()); + final String testId = client.getDojoTestId(engagementId.getPropertyValue(), testsIds); LOGGER.debug("Found existing test Id: " + testId); if (testId.equals("")) { client.uploadDependencyTrackFindings(apiKey.getPropertyValue(), engagementId.getPropertyValue(), payload); diff --git a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoClientTest.java b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoClientTest.java deleted file mode 100644 index 96b9abc8a2..0000000000 --- a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoClientTest.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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.integrations.defectdojo; - -import com.github.tomakehurst.wiremock.client.WireMock; -import com.github.tomakehurst.wiremock.junit.WireMockRule; -import com.github.tomakehurst.wiremock.matching.EqualToPattern; -import org.apache.commons.io.input.NullInputStream; -import org.apache.http.HttpHeaders; -import org.apache.http.entity.ContentType; -import org.junit.Rule; -import org.junit.Test; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.net.URL; - -public class DefectDojoClientTest { - - @Rule - public WireMockRule wireMockRule = new WireMockRule(); - - - @Test - public void testUploadFindingsPositiveCase() throws Exception { - WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/defectdojo/api/v2/import-scan/")) - .withMultipartRequestBody(WireMock.aMultipart().withName("engagement"). - withBody(WireMock.equalTo("12345")))); - InputStream stream = new ByteArrayInputStream("test input" .getBytes()); - String token = "db975c97-98b1-4988-8d6a-9c3e044dfff3"; - String engagementId = "12345"; - DefectDojoUploader uploader = new DefectDojoUploader(); - DefectDojoClient client = new DefectDojoClient(uploader, new URL(wireMockRule.baseUrl() + "/defectdojo")); - client.uploadDependencyTrackFindings(token, engagementId, stream); - - WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/defectdojo/api/v2/import-scan/")) - .withAnyRequestBodyPart(WireMock.aMultipart().withName("engagement"). - withBody(WireMock.equalTo("12345") - )).withAnyRequestBodyPart(WireMock.aMultipart().withName("file") - .withBody(WireMock.equalTo("test input")).withHeader("Content-Type", WireMock.equalTo(ContentType.APPLICATION_OCTET_STREAM.getMimeType())))); - } - - - @Test - public void testUploadFindingsNegativeCase() throws Exception { - String token = "db975c97-98b1-4988-8d6a-9c3e044dfff2"; - String engagementId = ""; - WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/defectdojo/api/v2/import-scan/")) - .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("Token " + token)) - .withMultipartRequestBody(WireMock.aMultipart().withName("engagement"). - withBody(WireMock.equalTo(""))).willReturn(WireMock.aResponse().withStatus(400).withHeader(HttpHeaders.CONTENT_TYPE, "application/json"))); - DefectDojoUploader uploader = new DefectDojoUploader(); - DefectDojoClient client = new DefectDojoClient(uploader, new URL(wireMockRule.baseUrl() + "/defectdojo")); - client.uploadDependencyTrackFindings(token, engagementId, new NullInputStream(16)); - WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/defectdojo/api/v2/import-scan/")) - .withAnyRequestBodyPart(WireMock.aMultipart().withName("engagement"). - withBody(WireMock.equalTo("") - ))); - } - - @Test - public void testReimportFindingsPositiveCase() throws Exception { - String token = "db975c97-98b1-4988-8d6a-9c3e044dfff3"; - String testId = "15"; - String engagementId = "67890"; - WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/defectdojo/api/v2/reimport-scan/")) - .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("Token " + token)) - .withMultipartRequestBody(WireMock.aMultipart().withName("engagement"). - withBody(WireMock.equalTo(engagementId))).willReturn(WireMock.aResponse().withStatus(201).withHeader(HttpHeaders.CONTENT_TYPE, "application/json"))); - DefectDojoUploader uploader = new DefectDojoUploader(); - DefectDojoClient client = new DefectDojoClient(uploader, new URL(wireMockRule.baseUrl() + "/defectdojo")); - client.reimportDependencyTrackFindings(token, engagementId, new NullInputStream(0), testId, false); - WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/defectdojo/api/v2/reimport-scan/")) - .withAnyRequestBodyPart(WireMock.aMultipart().withName("engagement"). - withBody(WireMock.equalTo(engagementId) - ))); - } - - @Test - public void testReimportFindingsNegativeCase() throws Exception { - String token = "db975c97-98b1-4988-8d6a-9c3e044dfff2"; - String testId = "14"; - String engagementId = ""; - WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/defectdojo/api/v2/reimport-scan/")) - .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("Token " + token)) - .withMultipartRequestBody(WireMock.aMultipart().withName("engagement"). - withBody(WireMock.equalTo(""))).willReturn(WireMock.aResponse().withStatus(400).withHeader(HttpHeaders.CONTENT_TYPE, "application/json"))); - DefectDojoUploader uploader = new DefectDojoUploader(); - DefectDojoClient client = new DefectDojoClient(uploader, new URL(wireMockRule.baseUrl() + "/defectdojo")); - client.reimportDependencyTrackFindings(token, engagementId, new NullInputStream(16), testId, false); - WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/defectdojo/api/v2/reimport-scan/")) - .withAnyRequestBodyPart(WireMock.aMultipart().withName("engagement"). - withBody(WireMock.equalTo(engagementId) - ))); - } -} diff --git a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java index 5bf644640c..e3af639b96 100644 --- a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java +++ b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java @@ -19,18 +19,47 @@ package org.dependencytrack.integrations.defectdojo; import alpine.model.IConfigProperty; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.apache.http.HttpHeaders; import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.Finding; import org.dependencytrack.model.Project; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; +import javax.ws.rs.core.MediaType; import java.io.InputStream; -import java.util.ArrayList; +import java.util.List; +import static com.github.tomakehurst.wiremock.client.WireMock.aMultipart; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_API_KEY; import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_REIMPORT_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_URL; public class DefectDojoUploaderTest extends PersistenceCapableTest { + @Rule + public WireMockRule wireMockRule = new WireMockRule(options().dynamicPort()); + @Test public void testIntegrationMetadata() { DefectDojoUploader extension = new DefectDojoUploader(); @@ -72,11 +101,655 @@ public void testIntegrationDisabledCases() { } @Test - public void testIntegrationFindings() throws Exception { - Project project = qm.createProject("ACME Example", null, "1.0", null, null, null, true, false); - DefectDojoUploader extension = new DefectDojoUploader(); - extension.setQueryManager(qm); - InputStream in = extension.process(project, new ArrayList<>()); - Assert.assertTrue(in != null && in.available() > 0); + public void testUpload() { + qm.createConfigProperty( + DEFECTDOJO_ENABLED.getGroupName(), + DEFECTDOJO_ENABLED.getPropertyName(), + "true", + DEFECTDOJO_ENABLED.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_URL.getGroupName(), + DEFECTDOJO_URL.getPropertyName(), + wireMockRule.baseUrl(), + DEFECTDOJO_URL.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_API_KEY.getGroupName(), + DEFECTDOJO_API_KEY.getPropertyName(), + "dojoApiKey", + DEFECTDOJO_API_KEY.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_REIMPORT_ENABLED.getGroupName(), + DEFECTDOJO_REIMPORT_ENABLED.getPropertyName(), + DEFECTDOJO_REIMPORT_ENABLED.getDefaultPropertyValue(), + DEFECTDOJO_REIMPORT_ENABLED.getPropertyType(), + null + ); + + stubFor(post(urlPathEqualTo("/api/v2/import-scan/")) + .willReturn(aResponse() + .withStatus(201))); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + component.setVersion("1.2.3"); + qm.persist(component); + + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-123"); + vuln.setSource(Vulnerability.Source.INTERNAL); + vuln.setSeverity(Severity.HIGH); + qm.persist(vuln); + + qm.addVulnerability(vuln, component, AnalyzerIdentity.INTERNAL_ANALYZER); + + qm.createProjectProperty(project, "integrations", "defectdojo.engagementId", + "666", IConfigProperty.PropertyType.STRING, null); + + final var uploader = new DefectDojoUploader(); + uploader.setQueryManager(qm); + + final List findings = qm.getFindings(project); + final InputStream inputStream = uploader.process(project, findings); + uploader.upload(project, inputStream); + + verify(postRequestedFor(urlPathEqualTo("/api/v2/import-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey")) + .withAnyRequestBodyPart(aMultipart() + .withName("engagement") + .withBody(equalTo("666"))) + .withAnyRequestBodyPart(aMultipart() + .withName("scan_type") + .withBody(equalTo("Dependency Track Finding Packaging Format (FPF) Export"))) + .withAnyRequestBodyPart(aMultipart() + .withName("verified") + .withBody(equalTo("true"))) + .withAnyRequestBodyPart(aMultipart() + .withName("minimum_severity") + .withBody(equalTo("Info"))) + .withAnyRequestBodyPart(aMultipart() + .withName("close_old_findings") + .withBody(equalTo("true"))) + .withAnyRequestBodyPart(aMultipart() + .withName("push_to_jira") + .withBody(equalTo("false"))) + .withAnyRequestBodyPart(aMultipart() + .withName("scan_date") + .withBody(matching("\\d{4}-\\d{2}-\\d{2}"))) + .withAnyRequestBodyPart(aMultipart() + .withName("file") + .withBody(equalToJson(""" + { + "version": "1.2", + "meta": { + "application": "Dependency-Track", + "version": "${json-unit.any-string}", + "timestamp": "${json-unit.any-string}" + }, + "project": { + "uuid": "${json-unit.any-string}", + "name": "acme-app", + "version": "1.0.0" + }, + "findings": [ + { + "component": { + "uuid": "${json-unit.any-string}", + "name": "acme-lib", + "version": "1.2.3", + "project": "${json-unit.any-string}" + }, + "attribution": { + "analyzerIdentity": "INTERNAL_ANALYZER", + "attributedOn": "${json-unit.any-string}" + }, + "vulnerability": { + "uuid": "${json-unit.any-string}", + "vulnId": "INT-123", + "source": "INTERNAL", + "aliases": [], + "severity": "HIGH", + "severityRank": 1 + }, + "analysis": { + "isSuppressed": false + }, + "matrix": "${json-unit.any-string}" + } + ] + } + """, true, false)))); + } + + @Test + public void testUploadWithGlobalReimport() { + qm.createConfigProperty( + DEFECTDOJO_ENABLED.getGroupName(), + DEFECTDOJO_ENABLED.getPropertyName(), + "true", + DEFECTDOJO_ENABLED.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_URL.getGroupName(), + DEFECTDOJO_URL.getPropertyName(), + wireMockRule.baseUrl(), + DEFECTDOJO_URL.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_API_KEY.getGroupName(), + DEFECTDOJO_API_KEY.getPropertyName(), + "dojoApiKey", + DEFECTDOJO_API_KEY.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_REIMPORT_ENABLED.getGroupName(), + DEFECTDOJO_REIMPORT_ENABLED.getPropertyName(), + "true", + DEFECTDOJO_REIMPORT_ENABLED.getPropertyType(), + null + ); + + stubFor(get(urlPathEqualTo("/api/v2/tests/")) + .withQueryParam("engagement", equalTo("666")) + .withQueryParam("limit", equalTo("100")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .withBody(""" + { + "count": 2, + "next": "%s/api/v2/tests/?engagement=666&limit=100&offset=100", + "previous": null, + "results": [ + { + "id": 1, + "tags": [], + "test_type_name": "CycloneDX Scan", + "finding_groups": [], + "scan_type": "CycloneDX Scan", + "title": null, + "description": null, + "target_start": "2023-04-29T00:00:00Z", + "target_end": "2023-04-29T21:36:22.798765Z", + "estimated_time": null, + "actual_time": null, + "percent_complete": 100, + "updated": "2023-04-29T21:36:22.858597Z", + "created": "2023-04-29T21:36:22.802993Z", + "version": "", + "build_id": "", + "commit_hash": "", + "branch_tag": "", + "engagement": 666, + "lead": 1, + "test_type": 54, + "environment": 7, + "api_scan_configuration": null, + "notes": [], + "files": [] + } + ], + "prefetch": {} + } + """.formatted(wireMockRule.baseUrl())))); + + stubFor(get(urlPathEqualTo("/api/v2/tests/")) + .withQueryParam("engagement", equalTo("666")) + .withQueryParam("limit", equalTo("100")) + .withQueryParam("offset", equalTo("100")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey")) + .willReturn(aResponse() + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .withBody(""" + { + "count": 2, + "next": null, + "previous": "%s/api/v2/tests/?engagement=666&limit=100", + "results": [ + { + "id": 2, + "tags": [], + "test_type_name": "Dependency Track Finding Packaging Format (FPF) Export", + "finding_groups": [], + "scan_type": "Dependency Track Finding Packaging Format (FPF) Export", + "title": null, + "description": null, + "target_start": "2023-04-29T00:00:00Z", + "target_end": "2023-04-29T21:39:21.513481Z", + "estimated_time": null, + "actual_time": null, + "percent_complete": 100, + "updated": "2023-04-29T21:39:21.617857Z", + "created": "2023-04-29T21:39:21.516206Z", + "version": "", + "build_id": "", + "commit_hash": "", + "branch_tag": "", + "engagement": 666, + "lead": 1, + "test_type": 63, + "environment": 7, + "api_scan_configuration": null, + "notes": [], + "files": [] + } + ], + "prefetch": {} + } + """.formatted(wireMockRule.baseUrl())))); + + stubFor(post(urlPathEqualTo("/api/v2/reimport-scan/")) + .willReturn(aResponse() + .withStatus(201))); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + component.setVersion("1.2.3"); + qm.persist(component); + + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-123"); + vuln.setSource(Vulnerability.Source.INTERNAL); + vuln.setSeverity(Severity.HIGH); + qm.persist(vuln); + + qm.addVulnerability(vuln, component, AnalyzerIdentity.INTERNAL_ANALYZER); + + qm.createProjectProperty(project, "integrations", "defectdojo.engagementId", + "666", IConfigProperty.PropertyType.STRING, null); + + final var uploader = new DefectDojoUploader(); + uploader.setQueryManager(qm); + + final List findings = qm.getFindings(project); + final InputStream inputStream = uploader.process(project, findings); + uploader.upload(project, inputStream); + + verify(2, getRequestedFor(urlPathEqualTo("/api/v2/tests/"))); + + verify(postRequestedFor(urlPathEqualTo("/api/v2/reimport-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey")) + .withAnyRequestBodyPart(aMultipart() + .withName("engagement") + .withBody(equalTo("666"))) + .withAnyRequestBodyPart(aMultipart() + .withName("test") + .withBody(equalTo("2"))) + .withAnyRequestBodyPart(aMultipart() + .withName("scan_type") + .withBody(equalTo("Dependency Track Finding Packaging Format (FPF) Export"))) + .withAnyRequestBodyPart(aMultipart() + .withName("verified") + .withBody(equalTo("true"))) + .withAnyRequestBodyPart(aMultipart() + .withName("minimum_severity") + .withBody(equalTo("Info"))) + .withAnyRequestBodyPart(aMultipart() + .withName("close_old_findings") + .withBody(equalTo("true"))) + .withAnyRequestBodyPart(aMultipart() + .withName("push_to_jira") + .withBody(equalTo("false"))) + .withAnyRequestBodyPart(aMultipart() + .withName("scan_date") + .withBody(matching("\\d{4}-\\d{2}-\\d{2}"))) + .withAnyRequestBodyPart(aMultipart() + .withName("file") + .withBody(equalToJson(""" + { + "version": "1.2", + "meta": { + "application": "Dependency-Track", + "version": "${json-unit.any-string}", + "timestamp": "${json-unit.any-string}" + }, + "project": { + "uuid": "${json-unit.any-string}", + "name": "acme-app", + "version": "1.0.0" + }, + "findings": [ + { + "component": { + "uuid": "${json-unit.any-string}", + "name": "acme-lib", + "version": "1.2.3", + "project": "${json-unit.any-string}" + }, + "attribution": { + "analyzerIdentity": "INTERNAL_ANALYZER", + "attributedOn": "${json-unit.any-string}" + }, + "vulnerability": { + "uuid": "${json-unit.any-string}", + "vulnId": "INT-123", + "source": "INTERNAL", + "aliases": [], + "severity": "HIGH", + "severityRank": 1 + }, + "analysis": { + "isSuppressed": false + }, + "matrix": "${json-unit.any-string}" + } + ] + } + """, true, false)))); } + + @Test + public void testUploadWithProjectLevelReimport() { + qm.createConfigProperty( + DEFECTDOJO_ENABLED.getGroupName(), + DEFECTDOJO_ENABLED.getPropertyName(), + "true", + DEFECTDOJO_ENABLED.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_URL.getGroupName(), + DEFECTDOJO_URL.getPropertyName(), + wireMockRule.baseUrl(), + DEFECTDOJO_URL.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_API_KEY.getGroupName(), + DEFECTDOJO_API_KEY.getPropertyName(), + "dojoApiKey", + DEFECTDOJO_API_KEY.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_REIMPORT_ENABLED.getGroupName(), + DEFECTDOJO_REIMPORT_ENABLED.getPropertyName(), + "false", + DEFECTDOJO_REIMPORT_ENABLED.getPropertyType(), + null + ); + + stubFor(get(urlPathEqualTo("/api/v2/tests/")) + .withQueryParam("engagement", equalTo("666")) + .withQueryParam("limit", equalTo("100")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .withBody(""" + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "tags": [], + "test_type_name": "Dependency Track Finding Packaging Format (FPF) Export", + "finding_groups": [], + "scan_type": "Dependency Track Finding Packaging Format (FPF) Export", + "title": null, + "description": null, + "target_start": "2023-04-29T00:00:00Z", + "target_end": "2023-04-29T21:39:21.513481Z", + "estimated_time": null, + "actual_time": null, + "percent_complete": 100, + "updated": "2023-04-29T21:39:21.617857Z", + "created": "2023-04-29T21:39:21.516206Z", + "version": "", + "build_id": "", + "commit_hash": "", + "branch_tag": "", + "engagement": 666, + "lead": 1, + "test_type": 63, + "environment": 7, + "api_scan_configuration": null, + "notes": [], + "files": [] + } + ], + "prefetch": {} + } + """))); + + stubFor(post(urlPathEqualTo("/api/v2/reimport-scan/")) + .willReturn(aResponse() + .withStatus(201))); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + component.setVersion("1.2.3"); + qm.persist(component); + + qm.createProjectProperty(project, "integrations", "defectdojo.engagementId", + "666", IConfigProperty.PropertyType.STRING, null); + qm.createProjectProperty(project, "integrations", "defectdojo.reimport", + "true", IConfigProperty.PropertyType.BOOLEAN, null); + + final var uploader = new DefectDojoUploader(); + uploader.setQueryManager(qm); + + final List findings = qm.getFindings(project); + final InputStream inputStream = uploader.process(project, findings); + uploader.upload(project, inputStream); + + verify(1, getRequestedFor(urlPathEqualTo("/api/v2/tests/"))); + + verify(postRequestedFor(urlPathEqualTo("/api/v2/reimport-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey")) + .withAnyRequestBodyPart(aMultipart() + .withName("file") + .withBody(equalToJson(""" + { + "version": "1.2", + "meta": { + "application": "Dependency-Track", + "version": "${json-unit.any-string}", + "timestamp": "${json-unit.any-string}" + }, + "project": { + "uuid": "${json-unit.any-string}", + "name": "acme-app", + "version": "1.0.0" + }, + "findings": [] + } + """, true, false)))); + } + + @Test + public void testUploadWithReimportAndNoExistingTest() { + qm.createConfigProperty( + DEFECTDOJO_ENABLED.getGroupName(), + DEFECTDOJO_ENABLED.getPropertyName(), + "true", + DEFECTDOJO_ENABLED.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_URL.getGroupName(), + DEFECTDOJO_URL.getPropertyName(), + wireMockRule.baseUrl(), + DEFECTDOJO_URL.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_API_KEY.getGroupName(), + DEFECTDOJO_API_KEY.getPropertyName(), + "dojoApiKey", + DEFECTDOJO_API_KEY.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_REIMPORT_ENABLED.getGroupName(), + DEFECTDOJO_REIMPORT_ENABLED.getPropertyName(), + "true", + DEFECTDOJO_REIMPORT_ENABLED.getPropertyType(), + null + ); + + stubFor(get(urlPathEqualTo("/api/v2/tests/")) + .withQueryParam("engagement", equalTo("666")) + .withQueryParam("limit", equalTo("100")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .withBody(""" + { + "count": 0, + "next": null, + "previous": null, + "results": [], + "prefetch": {} + } + """))); + + stubFor(post(urlPathEqualTo("/api/v2/import-scan/")) + .willReturn(aResponse() + .withStatus(201))); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + component.setVersion("1.2.3"); + qm.persist(component); + + qm.createProjectProperty(project, "integrations", "defectdojo.engagementId", + "666", IConfigProperty.PropertyType.STRING, null); + + final var uploader = new DefectDojoUploader(); + uploader.setQueryManager(qm); + + final List findings = qm.getFindings(project); + final InputStream inputStream = uploader.process(project, findings); + uploader.upload(project, inputStream); + + verify(1, getRequestedFor(urlPathEqualTo("/api/v2/tests/"))); + + verify(postRequestedFor(urlPathEqualTo("/api/v2/import-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Token dojoApiKey")) + .withAnyRequestBodyPart(aMultipart() + .withName("file") + .withBody(equalToJson(""" + { + "version": "1.2", + "meta": { + "application": "Dependency-Track", + "version": "${json-unit.any-string}", + "timestamp": "${json-unit.any-string}" + }, + "project": { + "uuid": "${json-unit.any-string}", + "name": "acme-app", + "version": "1.0.0" + }, + "findings": [] + } + """, true, false)))); + } + + /** + * Un-ignore this test to test the integration against a local DefectDojo deployment. + *

+ * Consult the documentation + * for instructions on how to set it up. + */ + @Test + @Ignore + public void testUploadIntegration() { + final var baseUrl = "http://localhost:8080"; + final var apiKey = ""; + final var engagementId = ""; + final var globalReimport = false; + final var projectReimport = false; + + qm.createConfigProperty( + DEFECTDOJO_URL.getGroupName(), + DEFECTDOJO_URL.getPropertyName(), + baseUrl, + DEFECTDOJO_URL.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_API_KEY.getGroupName(), + DEFECTDOJO_API_KEY.getPropertyName(), + apiKey, + DEFECTDOJO_API_KEY.getPropertyType(), + null + ); + qm.createConfigProperty( + DEFECTDOJO_REIMPORT_ENABLED.getGroupName(), + DEFECTDOJO_REIMPORT_ENABLED.getPropertyName(), + Boolean.toString(globalReimport), + DEFECTDOJO_REIMPORT_ENABLED.getPropertyType(), + null + ); + + final var project = new Project(); + project.setName("acme-app"); + project.setVersion("1.0.0"); + qm.persist(project); + + final var component = new Component(); + component.setProject(project); + component.setName("acme-lib"); + component.setVersion("1.2.3"); + qm.persist(component); + + final var vuln = new Vulnerability(); + vuln.setVulnId("INT-123"); + vuln.setSource(Vulnerability.Source.INTERNAL); + vuln.setSeverity(Severity.HIGH); + qm.persist(vuln); + + qm.addVulnerability(vuln, component, AnalyzerIdentity.INTERNAL_ANALYZER); + + qm.createProjectProperty(project, "integrations", "defectdojo.engagementId", + engagementId, IConfigProperty.PropertyType.STRING, null); + qm.createProjectProperty(project, "integrations", "defectdojo.reimport", + Boolean.toString(projectReimport), IConfigProperty.PropertyType.BOOLEAN, null); + + final var uploader = new DefectDojoUploader(); + uploader.setQueryManager(qm); + + final List findings = qm.getFindings(project); + final InputStream inputStream = uploader.process(project, findings); + uploader.upload(project, inputStream); + } + } From abb810356ea3c1130f2b6c9908bbb196e983f24d Mon Sep 17 00:00:00 2001 From: Jakub Rak Date: Mon, 10 Apr 2023 15:41:18 +0200 Subject: [PATCH 06/13] Fix policy violation search Signed-off-by: Jakub Rak --- .../persistence/PolicyQueryManager.java | 15 ++-- .../v1/PolicyViolationResourceTest.java | 81 +++++++++++++------ 2 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java index c38b961623..0b983226d5 100644 --- a/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java @@ -268,16 +268,19 @@ public List getAllPolicyViolations(final Project project) { */ @SuppressWarnings("unchecked") public PaginatedResult getPolicyViolations(final Project project, boolean includeSuppressed) { + PaginatedResult result; + final String projectFilter = includeSuppressed ? "project.id == :pid" : "project.id == :pid && (analysis.suppressed == false || analysis.suppressed == null)"; final Query query = pm.newQuery(PolicyViolation.class); - if (includeSuppressed) { - query.setFilter("project.id == :pid"); - } else { - query.setFilter("project.id == :pid && (analysis.suppressed == false || analysis.suppressed == null)"); - } if (orderBy == null) { query.setOrdering("timestamp desc, component.name, component.version"); } - final PaginatedResult result = execute(query, project.getId()); + if (filter != null) { + query.setFilter(projectFilter + " && (policyCondition.policy.name.toLowerCase().matches(:filter) || component.name.toLowerCase().matches(:filter))"); + final String filterString = ".*" + filter.toLowerCase() + ".*"; + result = execute(query, project.getId(), filterString); + } else { + result = execute(query, project.getId()); + } for (final PolicyViolation violation: result.getList(PolicyViolation.class)) { violation.getPolicyCondition().getPolicy(); // force policy to ne included since its not the default violation.getComponent().getResolvedLicense(); // force resolved license to ne included since its not the default diff --git a/src/test/java/org/dependencytrack/resources/v1/PolicyViolationResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/PolicyViolationResourceTest.java index d3e6a8da72..bd04257547 100644 --- a/src/test/java/org/dependencytrack/resources/v1/PolicyViolationResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/PolicyViolationResourceTest.java @@ -37,6 +37,7 @@ import javax.json.JsonArray; import javax.json.JsonObject; import javax.ws.rs.core.Response; +import java.util.ArrayList; import java.util.Date; import java.util.UUID; @@ -112,40 +113,70 @@ public void getViolationsByProjectTest() { final Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); - var component = new Component(); - component.setProject(project); - component.setName("Acme Component"); - component.setVersion("1.0"); - component = qm.createComponent(component, false); - - final Policy policy = qm.createPolicy("Blacklisted Version", Policy.Operator.ALL, Policy.ViolationState.FAIL); - final PolicyCondition condition = qm.createPolicyCondition(policy, PolicyCondition.Subject.VERSION, PolicyCondition.Operator.NUMERIC_EQUAL, "1.0"); - - var violation = new PolicyViolation(); - violation.setType(PolicyViolation.Type.OPERATIONAL); - violation.setComponent(component); - violation.setPolicyCondition(condition); - violation.setTimestamp(new Date()); - violation = qm.persist(violation); + var component0 = new Component(); + component0.setProject(project); + component0.setName("Acme Component 0"); + component0.setVersion("1.0"); + component0 = qm.createComponent(component0, false); + + var component1 = new Component(); + component1.setProject(project); + component1.setName("Acme Component 1"); + component1.setVersion("1.0"); + component1 = qm.createComponent(component1, false); + + final Policy policy0 = qm.createPolicy("Blacklisted Version 0", Policy.Operator.ALL, Policy.ViolationState.FAIL); + final PolicyCondition condition0 = qm.createPolicyCondition(policy0, PolicyCondition.Subject.VERSION, PolicyCondition.Operator.NUMERIC_EQUAL, "1.0"); + + final Policy policy1 = qm.createPolicy("Blacklisted Version 1", Policy.Operator.ALL, Policy.ViolationState.FAIL); + final PolicyCondition condition1 = qm.createPolicyCondition(policy1, PolicyCondition.Subject.VERSION, PolicyCondition.Operator.NUMERIC_EQUAL, "1.0"); + + ArrayList filteredPolicyViolations = new ArrayList<>(); + for (int i=0; i<10; i++) { + final boolean componentFilter = (i == 3); + final boolean conditionFilter = (i == 7); + + var violation = new PolicyViolation(); + violation.setType(PolicyViolation.Type.OPERATIONAL); + violation.setComponent(componentFilter ? component0 : component1); + violation.setPolicyCondition(conditionFilter ? condition0 : condition1); + violation.setTimestamp(new Date()); + violation = qm.persist(violation); + + if (conditionFilter || componentFilter) { + filteredPolicyViolations.add(violation); + } + } final Response response = target(V1_POLICY_VIOLATION) + .queryParam("searchText", "0") .path("/project/" + project.getUuid()) .request() .header(X_API_KEY, apiKey) .get(); assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); - assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1"); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("2"); final JsonArray jsonArray = parseJsonArray(response); - assertThat(jsonArray).hasSize(1); - - final JsonObject jsonObject = jsonArray.getJsonObject(0); - assertThat(jsonObject.getString("uuid")).isEqualTo(violation.getUuid().toString()); - assertThat(jsonObject.getString("type")).isEqualTo(PolicyViolation.Type.OPERATIONAL.name()); - assertThat(jsonObject.getJsonObject("policyCondition")).isNotNull(); - assertThat(jsonObject.getJsonObject("policyCondition").getJsonObject("policy")).isNotNull(); - assertThat(jsonObject.getJsonObject("policyCondition").getJsonObject("policy").getString("name")).isEqualTo("Blacklisted Version"); - assertThat(jsonObject.getJsonObject("policyCondition").getJsonObject("policy").getString("violationState")).isEqualTo("FAIL"); + assertThat(jsonArray).hasSize(2); + + final JsonObject jsonObject0 = jsonArray.getJsonObject(0); + assertThat(jsonObject0.getString("uuid")).isEqualTo(filteredPolicyViolations.get(1).getUuid().toString()); + assertThat(jsonObject0.getString("type")).isEqualTo(PolicyViolation.Type.OPERATIONAL.name()); + assertThat(jsonObject0.getJsonObject("policyCondition")).isNotNull(); + assertThat(jsonObject0.getJsonObject("policyCondition").getJsonObject("policy")).isNotNull(); + assertThat(jsonObject0.getJsonObject("policyCondition").getJsonObject("policy").getString("violationState")).isEqualTo("FAIL"); + assertThat(jsonObject0.getJsonObject("policyCondition").getJsonObject("policy").getString("name")).isEqualTo("Blacklisted Version 0"); + assertThat(jsonObject0.getJsonObject("component").getString("name")).isEqualTo("Acme Component 1"); + + final JsonObject jsonObject1 = jsonArray.getJsonObject(1); + assertThat(jsonObject1.getString("uuid")).isEqualTo(filteredPolicyViolations.get(0).getUuid().toString()); + assertThat(jsonObject1.getString("type")).isEqualTo(PolicyViolation.Type.OPERATIONAL.name()); + assertThat(jsonObject1.getJsonObject("policyCondition")).isNotNull(); + assertThat(jsonObject1.getJsonObject("policyCondition").getJsonObject("policy")).isNotNull(); + assertThat(jsonObject1.getJsonObject("policyCondition").getJsonObject("policy").getString("violationState")).isEqualTo("FAIL"); + assertThat(jsonObject1.getJsonObject("policyCondition").getJsonObject("policy").getString("name")).isEqualTo("Blacklisted Version 1"); + assertThat(jsonObject1.getJsonObject("component").getString("name")).isEqualTo("Acme Component 0"); } @Test From 7469881e9d546f3a90c7c5395110d6e644647805 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 May 2023 18:17:50 +0200 Subject: [PATCH 07/13] Fix NPE in `DefectDojoClient` when `scan_type` of a test is `null` Fixes #2628 Signed-off-by: nscuro --- .../defectdojo/DefectDojoClient.java | 17 +++++----- .../defectdojo/DefectDojoUploaderTest.java | 34 ++++++++++++++++--- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java index 46d7edbc38..bbab50e3c4 100644 --- a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java +++ b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java @@ -117,8 +117,10 @@ public ArrayList getDojoTestIds(final String token, final String eid) { stringResponse = EntityUtils.toString(response1.getEntity()); } dojoObj = new JSONObject(stringResponse); - dojoArray = dojoObj.getJSONArray("results"); - dojoTests.addAll(jsonToList(dojoArray)); + dojoArray = dojoObj.optJSONArray("results"); + if (dojoArray != null) { + dojoTests.addAll(jsonToList(dojoArray)); + } } LOGGER.debug("Successfully retrieved the test list "); return dojoTests; @@ -136,12 +138,11 @@ public ArrayList getDojoTestIds(final String token, final String eid) { // Given the engagement id and scan type, search for existing test id public String getDojoTestId(final String engagementID, final ArrayList dojoTests) { - for (int i = 0; i < dojoTests.size(); i++) { - String s = dojoTests.get(i); - JSONObject dojoTest = new JSONObject(s); - if (dojoTest.get("engagement").toString().equals(engagementID) && - dojoTest.get("scan_type").toString().equals("Dependency Track Finding Packaging Format (FPF) Export")) { - return dojoTest.get("id").toString(); + for (final String dojoTestJson : dojoTests) { + JSONObject dojoTest = new JSONObject(dojoTestJson); + if (dojoTest.optString("engagement").equals(engagementID) && + dojoTest.optString("scan_type").equals("Dependency Track Finding Packaging Format (FPF) Export")) { + return dojoTest.optString("id"); } } return ""; diff --git a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java index e3af639b96..e9533eeba0 100644 --- a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java +++ b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoUploaderTest.java @@ -272,7 +272,7 @@ public void testUploadWithGlobalReimport() { .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) .withBody(""" { - "count": 2, + "count": 3, "next": "%s/api/v2/tests/?engagement=666&limit=100&offset=100", "previous": null, "results": [ @@ -302,6 +302,32 @@ public void testUploadWithGlobalReimport() { "api_scan_configuration": null, "notes": [], "files": [] + }, + { + "id": 2, + "tags": [], + "test_type_name": "API Test", + "finding_groups": [], + "title": null, + "description": null, + "target_start": "2023-04-29T00:00:00Z", + "target_end": "2023-04-29T21:36:22.798765Z", + "estimated_time": null, + "actual_time": null, + "percent_complete": 100, + "updated": "2023-04-29T21:36:22.858597Z", + "created": "2023-04-29T21:36:22.802993Z", + "version": "", + "build_id": "", + "commit_hash": "", + "branch_tag": "", + "engagement": 666, + "lead": 1, + "test_type": 1, + "environment": 7, + "api_scan_configuration": null, + "notes": [], + "files": [] } ], "prefetch": {} @@ -317,12 +343,12 @@ public void testUploadWithGlobalReimport() { .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) .withBody(""" { - "count": 2, + "count": 3, "next": null, "previous": "%s/api/v2/tests/?engagement=666&limit=100", "results": [ { - "id": 2, + "id": 3, "tags": [], "test_type_name": "Dependency Track Finding Packaging Format (FPF) Export", "finding_groups": [], @@ -395,7 +421,7 @@ public void testUploadWithGlobalReimport() { .withBody(equalTo("666"))) .withAnyRequestBodyPart(aMultipart() .withName("test") - .withBody(equalTo("2"))) + .withBody(equalTo("3"))) .withAnyRequestBodyPart(aMultipart() .withName("scan_type") .withBody(equalTo("Dependency Track Finding Packaging Format (FPF) Export"))) From be88037089642283d250fa726d80321d4005b385 Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 May 2023 18:40:15 +0200 Subject: [PATCH 08/13] Add missing config docs for `alpine.oidc.client.id` Signed-off-by: nscuro --- docs/_docs/getting-started/configuration.md | 6 ++++++ src/main/resources/application.properties | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/docs/_docs/getting-started/configuration.md b/docs/_docs/getting-started/configuration.md index c40b09fcb1..acbef529ce 100644 --- a/docs/_docs/getting-started/configuration.md +++ b/docs/_docs/getting-started/configuration.md @@ -334,6 +334,12 @@ alpine.metrics.auth.password= # If enabled, alpine.oidc.* properties should be set accordingly. alpine.oidc.enabled=false +# Optional +# Defines the client ID to be used for OpenID Connect. +# The client ID should be the same as the one configured for the frontend, +# and will only be used to validate ID tokens. +alpine.oidc.client.id= + # Optional # Defines the issuer URL to be used for OpenID Connect. # This issuer MUST support provider configuration via the /.well-known/openid-configuration endpoint. diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e31033ac89..0f4b1df289 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -301,6 +301,12 @@ alpine.metrics.auth.password= # If enabled, alpine.oidc.* properties should be set accordingly. alpine.oidc.enabled=false +# Optional +# Defines the client ID to be used for OpenID Connect. +# The client ID should be the same as the one configured for the frontend, +# and will only be used to validate ID tokens. +alpine.oidc.client.id= + # Optional # Defines the issuer URL to be used for OpenID Connect. # This issuer MUST support provider configuration via the /.well-known/openid-configuration endpoint. From e4e5c70072863f863aae224ef3d1e138a89ad30e Mon Sep 17 00:00:00 2001 From: nscuro Date: Sun, 14 May 2023 19:09:07 +0200 Subject: [PATCH 09/13] Prepare changelog for v4.8.1 Signed-off-by: nscuro --- .github/ISSUE_TEMPLATE/defect-report.yml | 1 + docs/_posts/2023-05-xx-v4.8.1.md | 57 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 docs/_posts/2023-05-xx-v4.8.1.md diff --git a/.github/ISSUE_TEMPLATE/defect-report.yml b/.github/ISSUE_TEMPLATE/defect-report.yml index 2d93bd04bd..260757d86f 100644 --- a/.github/ISSUE_TEMPLATE/defect-report.yml +++ b/.github/ISSUE_TEMPLATE/defect-report.yml @@ -62,6 +62,7 @@ body: options: - 4.7.x - 4.8.0 + - 4.8.1 - 4.9.0-SNAPSHOT validations: required: true diff --git a/docs/_posts/2023-05-xx-v4.8.1.md b/docs/_posts/2023-05-xx-v4.8.1.md new file mode 100644 index 0000000000..cd0482ddce --- /dev/null +++ b/docs/_posts/2023-05-xx-v4.8.1.md @@ -0,0 +1,57 @@ +--- +title: v4.8.1 +type: patch +--- + +**Fixes:** + +* TODO + +For a complete list of changes, refer to the respective GitHub milestones: + +* [API server milestone 4.8.1](https://github.com/DependencyTrack/dependency-track/milestone/32?closed=1) +* [Frontend milestone 4.8.1](https://github.com/DependencyTrack/frontend/milestone/15?closed=1) + +We thank all organizations and individuals who contributed to this release, from logging issues to taking part in +discussions on GitHub & Slack to testing of fixes. +Special thanks to everyone who contributed code to fix defects: + +###### dependency-track-apiserver.jar + +| Algorithm | Checksum | +|:----------|:---------| +| SHA-1 | | +| SHA-256 | | + +###### dependency-track-bundled.jar + +| Algorithm | Checksum | +|:----------|:---------| +| SHA-1 | | +| SHA-256 | | + +###### frontend-dist.zip + +| Algorithm | Checksum | +|:----------|:---------| +| SHA-1 | | +| SHA-256 | | + +###### Software Bill of Materials (SBOM) + +* API Server: [bom.json](https://github.com/DependencyTrack/dependency-track/releases/download/4.8.1/bom.json) +* Frontend: [bom.json](https://github.com/DependencyTrack/frontend/releases/download/4.8.1/bom.json) + +[apiserver/#2194]: https://github.com/DependencyTrack/dependency-track/issues/2194 +[apiserver/#2622]: https://github.com/DependencyTrack/dependency-track/issues/2622 +[apiserver/#2628]: https://github.com/DependencyTrack/dependency-track/issues/2628 +[apiserver/#2695]: https://github.com/DependencyTrack/dependency-track/issues/2695 +[apiserver/#2697]: https://github.com/DependencyTrack/dependency-track/pull/2697 +[apiserver/#2698]: https://github.com/DependencyTrack/dependency-track/issues/2698 +[apiserver/#2707]: https://github.com/DependencyTrack/dependency-track/issues/2707 +[apiserver/#2743]: https://github.com/DependencyTrack/dependency-track/pull/2743 + +[frontend/#477]: https://github.com/DependencyTrack/frontend/issues/477 +[frontend/#483]: https://github.com/DependencyTrack/frontend/issues/483 +[frontend/#486]: https://github.com/DependencyTrack/frontend/issues/486 +[frontend/#495]: https://github.com/DependencyTrack/frontend/issues/495 \ No newline at end of file From b09f565e0f423706d2ff1466c21196bf99af3ae2 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 15 May 2023 10:07:18 +0200 Subject: [PATCH 10/13] Bump Alpine to 2.2.2 Signed-off-by: nscuro --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 78ecbffda7..9c297aee55 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ us.springett alpine-parent - 2.2.1 + 2.2.2 4.0.0 From 2ba8c80d073cc7abaec748bbe317f69c774204a8 Mon Sep 17 00:00:00 2001 From: nscuro Date: Mon, 15 May 2023 23:26:15 +0200 Subject: [PATCH 11/13] Complete changelog for v4.8.1 Signed-off-by: nscuro --- docs/_posts/2023-05-xx-v4.8.1.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/_posts/2023-05-xx-v4.8.1.md b/docs/_posts/2023-05-xx-v4.8.1.md index cd0482ddce..cad14c3989 100644 --- a/docs/_posts/2023-05-xx-v4.8.1.md +++ b/docs/_posts/2023-05-xx-v4.8.1.md @@ -5,7 +5,19 @@ type: patch **Fixes:** -* TODO +* Fix unrelated vulnerabilities being correlated during alias synchronization - [apiserver/#2194] +* Fix `NullPointerException` when email alert is configured with just teams as destination - [apiserver/#2698] +* Fix broken pagination in DefectDojo integration - [apiserver/#2707] +* Fix search function in policy violation tab not working - [apiserver/#2622] +* Fix `PATCH /api/v1/project` endpoint not updating external references - [apiserver/#2695] +* Fix `NullPointerException` in DefectDojo integration - [apiserver/#2628] +* Fix retrieval of OIDC JWK sets not respecting HTTP proxy settings - [apiserver/#2696] +* Lower log level for repository meta analyzer to `WARN` and include exception details - [apiserver/#2697] +* Add missing config docs for `alpine.oidc.client.id` - [apiserver/#2743] +* Fix not all vulnerability aliases being displayed in the UI - [frontend/#477] +* Fix broken vulnerability alias links - [frontend/#486] +* Fix broken project tag links on tabs other than "Overview" - [frontend/#483] +* Fix broken project version links on tabs other than "Overview" - [frontend/#495] For a complete list of changes, refer to the respective GitHub milestones: @@ -14,7 +26,9 @@ For a complete list of changes, refer to the respective GitHub milestones: We thank all organizations and individuals who contributed to this release, from logging issues to taking part in discussions on GitHub & Slack to testing of fixes. -Special thanks to everyone who contributed code to fix defects: + +Special thanks to everyone who contributed code to fix defects: +[@heubeck], [@jakubrak], [@sahibamittal], [@valentijnscholten] ###### dependency-track-apiserver.jar @@ -46,6 +60,7 @@ Special thanks to everyone who contributed code to fix defects: [apiserver/#2622]: https://github.com/DependencyTrack/dependency-track/issues/2622 [apiserver/#2628]: https://github.com/DependencyTrack/dependency-track/issues/2628 [apiserver/#2695]: https://github.com/DependencyTrack/dependency-track/issues/2695 +[apiserver/#2696]: https://github.com/DependencyTrack/dependency-track/issues/2696 [apiserver/#2697]: https://github.com/DependencyTrack/dependency-track/pull/2697 [apiserver/#2698]: https://github.com/DependencyTrack/dependency-track/issues/2698 [apiserver/#2707]: https://github.com/DependencyTrack/dependency-track/issues/2707 @@ -54,4 +69,9 @@ Special thanks to everyone who contributed code to fix defects: [frontend/#477]: https://github.com/DependencyTrack/frontend/issues/477 [frontend/#483]: https://github.com/DependencyTrack/frontend/issues/483 [frontend/#486]: https://github.com/DependencyTrack/frontend/issues/486 -[frontend/#495]: https://github.com/DependencyTrack/frontend/issues/495 \ No newline at end of file +[frontend/#495]: https://github.com/DependencyTrack/frontend/issues/495 + +[@heubeck]: https://github.com/heubeck +[@jakubrak]: https://github.com/jakubrak +[@sahibamittal]: https://github.com/sahibamittal +[@valentijnscholten]: https://github.com/valentijnscholten \ No newline at end of file From bf4891dff070fd0d839df61f5130f8665989a008 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 16 May 2023 12:07:07 +0200 Subject: [PATCH 12/13] Set release date for v4.8.1 Signed-off-by: nscuro --- docs/_posts/{2023-05-xx-v4.8.1.md => 2023-05-16-v4.8.1.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/_posts/{2023-05-xx-v4.8.1.md => 2023-05-16-v4.8.1.md} (100%) diff --git a/docs/_posts/2023-05-xx-v4.8.1.md b/docs/_posts/2023-05-16-v4.8.1.md similarity index 100% rename from docs/_posts/2023-05-xx-v4.8.1.md rename to docs/_posts/2023-05-16-v4.8.1.md From 61b29e30116499af04f72138363c1261cca24e81 Mon Sep 17 00:00:00 2001 From: nscuro Date: Tue, 16 May 2023 12:29:59 +0200 Subject: [PATCH 13/13] Bump frontend to 4.8.1 Signed-off-by: nscuro --- docs/_posts/2023-05-16-v4.8.1.md | 8 ++++---- pom.xml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/_posts/2023-05-16-v4.8.1.md b/docs/_posts/2023-05-16-v4.8.1.md index cad14c3989..a27a3d56bc 100644 --- a/docs/_posts/2023-05-16-v4.8.1.md +++ b/docs/_posts/2023-05-16-v4.8.1.md @@ -46,10 +46,10 @@ Special thanks to everyone who contributed code to fix defects: ###### frontend-dist.zip -| Algorithm | Checksum | -|:----------|:---------| -| SHA-1 | | -| SHA-256 | | +| Algorithm | Checksum | +|:----------|:-----------------------------------------------------------------| +| SHA-1 | 01bc042e1f510e089b9db937852dbcde69eca603 | +| SHA-256 | f946994c0f66647bd34c9e10997f2b62c08ab17ebbfe42edf149be12a47b2278 | ###### Software Bill of Materials (SBOM) diff --git a/pom.xml b/pom.xml index 9c297aee55..832e17e3c5 100644 --- a/pom.xml +++ b/pom.xml @@ -81,7 +81,7 @@ - 4.8.0 + 4.8.1 ${project.parent.version} 2.0.2 1.4.1