diff --git a/.github/workflows/ci-test-e2e.yaml b/.github/workflows/ci-test-e2e.yaml new file mode 100644 index 0000000000..47635f09f4 --- /dev/null +++ b/.github/workflows/ci-test-e2e.yaml @@ -0,0 +1,49 @@ +# 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) OWASP Foundation. All Rights Reserved. +name: E2E Tests + +on: + schedule: + - cron: '0 2 * * *' + workflow_dispatch: { } + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: { } + +jobs: + test-e2e: + name: Test E2E + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # tag=v6.0.2 + with: + persist-credentials: false + + - name: Set up JDK + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # tag=v5.2.0 + with: + distribution: 'temurin' + java-version: '25' + cache: 'maven' + + - name: Execute end-to-end tests + run: make test-e2e diff --git a/Makefile b/Makefile index 076cfaabfc..b98e691f8b 100644 --- a/Makefile +++ b/Makefile @@ -32,11 +32,11 @@ ifdef AGENT endif build: - $(MVND) $(MVN_FLAGS) -q -Pquick package + $(MVND) $(MVN_FLAGS) -Pquick package .PHONY: build build-dist: - $(MVND) $(MVN_FLAGS) -q -Pdist,quick package + $(MVND) $(MVN_FLAGS) -Pdist,quick package .PHONY: build-dist build-image: build @@ -54,15 +54,19 @@ build-v4-migrator-image: build .PHONY: build-v4-migrator-image datanucleus-enhance: - $(MVND) $(MVN_FLAGS) -q -Pquick -pl alpine/alpine-model,apiserver process-classes + $(MVND) $(MVN_FLAGS) \ + -Pquick \ + -Dresolve.skip \ + -pl alpine/alpine-model,apiserver \ + process-classes .PHONY: datanucleus-enhance install: - $(MVND) $(MVN_FLAGS) -q -Pquick install + $(MVND) $(MVN_FLAGS) -Pquick install .PHONY: install lint-java: - $(MVND) $(MVN_FLAGS) -q -Dmaven.build.cache.enabled=false validate + $(MVND) $(MVN_FLAGS) -Dmaven.build.cache.enabled=false validate .PHONY: lint-java lint-openapi: diff --git a/e2e/pom.xml b/e2e/pom.xml index 15cf7d55b7..513634b553 100644 --- a/e2e/pom.xml +++ b/e2e/pom.xml @@ -76,6 +76,12 @@ test + + com.adobe.testing + s3mock-testcontainers + test + + org.slf4j slf4j-simple diff --git a/e2e/src/main/java/org/dependencytrack/e2e/api/ApiClient.java b/e2e/src/main/java/org/dependencytrack/e2e/api/ApiClient.java index ef18c8ed5c..23b5c78086 100644 --- a/e2e/src/main/java/org/dependencytrack/e2e/api/ApiClient.java +++ b/e2e/src/main/java/org/dependencytrack/e2e/api/ApiClient.java @@ -150,11 +150,11 @@ Project lookupProject( Page getAllVulnerabilityPolicies(); @POST - @Path("/v2/vuln-policy-bundles/{uuid}/sync") + @Path("/v2/vuln-policy-bundles/{uuid}/sync-runs") void triggerVulnPolicyBundleSync(@PathParam("uuid") UUID uuid); @GET - @Path("/v2/vuln-policy-bundles/{uuid}/sync") + @Path("/v2/vuln-policy-bundles/{uuid}/sync-runs/latest") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) VulnPolicyBundleSyncStatus getVulnPolicyBundleSyncStatus(@PathParam("uuid") UUID uuid); diff --git a/e2e/src/test/java/org/dependencytrack/e2e/BomUploadS3FileStorageE2ET.java b/e2e/src/test/java/org/dependencytrack/e2e/BomUploadS3FileStorageE2ET.java new file mode 100644 index 0000000000..65036d98ea --- /dev/null +++ b/e2e/src/test/java/org/dependencytrack/e2e/BomUploadS3FileStorageE2ET.java @@ -0,0 +1,103 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.e2e; + +import com.adobe.testing.s3mock.testcontainers.S3MockContainer; +import org.dependencytrack.e2e.api.model.BomUploadRequest; +import org.dependencytrack.e2e.api.model.EventProcessingResponse; +import org.dependencytrack.e2e.api.model.EventTokenResponse; +import org.dependencytrack.e2e.api.model.Project; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; +import java.util.Base64; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class BomUploadS3FileStorageE2ET extends AbstractE2ET { + + private static final DockerImageName S3MOCK_IMAGE = + DockerImageName.parse("adobe/s3mock").withTag("5.0.0"); + + private S3MockContainer s3MockContainer; + + @Override + @BeforeEach + void beforeEach() throws Exception { + s3MockContainer = new S3MockContainer(S3MOCK_IMAGE) + .withInitialBuckets("dtrack") + .withNetwork(internalNetwork) + .withNetworkAliases("s3mock"); + s3MockContainer.start(); + + super.beforeEach(); + } + + @Override + protected void customizeApiServerContainer(GenericContainer container) { + container + .withEnv("DT_FILE_STORAGE_PROVIDER", "s3") + .withEnv("DT_FILE_STORAGE_S3_ENDPOINT", "http://s3mock:9090") + .withEnv("DT_FILE_STORAGE_S3_BUCKET", "dtrack") + .withEnv("DT_FILE_STORAGE_S3_ACCESS_KEY", "foo") + .withEnv("DT_FILE_STORAGE_S3_SECRET_KEY", "bar") + // Provide a hardcoded KEK to prevent the API server from generating a KEK keyset on disk. + .withEnv("DT_SECRET_MANAGEMENT_DATABASE_KEK", "Ccbo1y2QTKVHZbANVb/ER5yvTn5yZe5UtIpZ+eRSnTg=") + // Point the data directory at a read-only mount. + // If the API server were to write BOM files to local disk, the write would fail. + // A successful BOM upload and processing thus proves that S3 storage is in use, + // without having to race against the API server deleting the temporary file again. + .withTmpFs(Map.of("/data", "ro")); + } + + @AfterEach + void afterEachS3() { + Optional.ofNullable(s3MockContainer).ifPresent(GenericContainer::stop); + } + + @Test + void shouldUploadAndProcessBomWhenS3FileStorageConfigured() throws Exception { + final byte[] bomBytes = getClass().getResourceAsStream("/dtrack-apiserver-4.5.0.bom.json").readAllBytes(); + final String bomBase64 = Base64.getEncoder().encodeToString(bomBytes); + + final EventTokenResponse response = apiClient.uploadBom( + new BomUploadRequest("foo", "bar", true, bomBase64)); + assertThat(response.token()).isNotEmpty(); + + await("BOM processing") + .atMost(Duration.ofSeconds(15)) + .pollDelay(Duration.ofMillis(250)) + .untilAsserted(() -> { + final EventProcessingResponse processingResponse = + apiClient.isEventBeingProcessed(response.token()); + assertThat(processingResponse.processing()).isFalse(); + }); + + final Project project = apiClient.lookupProject("foo", "bar"); + assertThat(project).isNotNull(); + } + +}