diff --git a/hooks/persistence-defectdojo/hook/run.sh b/hooks/persistence-defectdojo/hook/run.sh new file mode 100755 index 0000000000..3ed8b3a33b --- /dev/null +++ b/hooks/persistence-defectdojo/hook/run.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: the secureCodeBox authors +# +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +java -jar ./build/libs/defectdojo-persistenceprovider-1.0.0-SNAPSHOT.jar "$@" diff --git a/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/DefectDojoPersistenceProvider.java b/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/DefectDojoPersistenceProvider.java index 2e2648a40f..f6c853ec93 100644 --- a/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/DefectDojoPersistenceProvider.java +++ b/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/DefectDojoPersistenceProvider.java @@ -3,11 +3,13 @@ // SPDX-License-Identifier: Apache-2.0 package io.securecodebox.persistence; +import io.securecodebox.persistence.config.EnvConfig; import io.securecodebox.persistence.config.PersistenceProviderConfig; import io.securecodebox.persistence.defectdojo.config.Config; import io.securecodebox.persistence.defectdojo.model.Finding; import io.securecodebox.persistence.defectdojo.service.EndpointService; import io.securecodebox.persistence.defectdojo.service.FindingService; +import io.securecodebox.persistence.exceptions.DefectDojoPersistenceException; import io.securecodebox.persistence.mapping.DefectDojoFindingToSecureCodeBoxMapper; import io.securecodebox.persistence.models.Scan; import io.securecodebox.persistence.service.KubernetesService; @@ -16,24 +18,79 @@ import io.securecodebox.persistence.strategies.VersionedEngagementsStrategy; import lombok.extern.slf4j.Slf4j; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; @Slf4j public class DefectDojoPersistenceProvider { + private static final String JAR_FILE = "defectdojo-persistenceprovider-1.0.0-SNAPSHOT.jar"; + private static final String USAGE = "Usage: java -jar " + JAR_FILE + " [ ] [-h|--help]"; + private static final String HELP = """ + This hook imports secureCodeBox findings into DefectDojo. + + This provider supports two modes: + + 1. Read-only Mode: Only imports the findings oneway from secureCodeBox into DefectDojo. + 2. syncFindingBack Mode: Replace the finding in secureCodeBox with the finding modified by DefectDojo. + + This provider uses positional arguments. The first and second argument is required (Read-only Mode). + The third and fourth arguments are optional (syncFindingBack Mode). + + Required arguments + + 1st argument (RAW_RESULT_DOWNLOAD_URL): HTTP URL where the raw finding file (various formats depending on scanner) is available. + 2nd argument (FINDING_DOWNLOAD_URL): HTTP URL where the secureCodeBox finding file (JSON) is available. + + Optional arguments: + + 3rd argument (RAW_RESULT_UPLOAD_URL): HTTP URL where to store modified finding file (various formats depending on scanner). + 4th argument (FINDING_UPLOAD_URL): HTTP URL where to store modified secureCodeBox finding file (JSON). + -h|--help Show this help. + + The hook also looks for various environment variables: + + + + See the documentation for more details: https://www.securecodebox.io/docs/hooks/defectdojo + """; + private static final String HELP_HINT = "Use option -h or --help to get more details about the arguments."; + private static final int EXIT_CODE_OK = 0; + private static final int EXIT_CODE_ERROR = 1; private final S3Service s3Service = new S3Service(); private final KubernetesService kubernetesService = new KubernetesService(); public static void main(String[] args) { - try { - new DefectDojoPersistenceProvider().execute(args); - } catch (Exception e) { - log.error(e.getMessage(), e); - System.exit(1); - } + try { + new DefectDojoPersistenceProvider().execute(args); + System.exit(EXIT_CODE_OK); + } catch (final DefectDojoPersistenceException e) { + // We do not log stack traces on own errors because the message itself must be helpful enough to fix it! + log.error(e.getMessage()); + log.error(USAGE); + log.error(HELP_HINT); + System.exit(EXIT_CODE_ERROR); + } catch (final Exception e) { + // Also log the stack trace as context for unforeseen errors. + log.error(e.getMessage(), e); + log.error(USAGE); + log.error(HELP_HINT); + System.exit(EXIT_CODE_ERROR); + } } private void execute(String[] args) throws Exception { log.info("Starting DefectDojo persistence provider"); + + if (shouldShowHelp(args)) { + showHelp(); + return; // Someone showing the help does not expect that anything more is done. + } + + if (!wrongNumberOfArguments(args)) { + throw new DefectDojoPersistenceException("Wrong number of arguments!"); + } + kubernetesService.init(); var scan = new Scan(kubernetesService.getScanFromKubernetes()); @@ -74,4 +131,28 @@ private void overwriteFindingWithDefectDojoFinding(Config config, List kubernetesService.updateScanInKubernetes(findings); } + boolean shouldShowHelp(String[] args) { + return Arrays.stream(args).anyMatch(arg -> arg.equals("-h") || arg.equals("--help")); + } + + private void showHelp() { + System.out.println(USAGE); + System.out.println(); + final var envVars = Arrays.stream(EnvConfig.EnvVarNames.values()) + .map(name -> " " + name.getLiteral() + ": " + name.getDescription()) + .collect(Collectors.joining("\n")); + System.out.println(HELP.replace("", envVars)); + } + + boolean wrongNumberOfArguments(String[] args) { + if (args.length == 2) { + return true; + } + + if (args.length == 4) { + return true; + } + + return false; + } } diff --git a/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/config/EnvConfig.java b/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/config/EnvConfig.java index deb982dde5..f6c61e5d04 100644 --- a/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/config/EnvConfig.java +++ b/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/config/EnvConfig.java @@ -119,42 +119,28 @@ private String retrieveEnvVar(EnvVarNames name) { * Enumerates all environment variable names used in this hook */ @Getter - enum EnvVarNames { + public enum EnvVarNames { /** - * Enable development mode. - * * @deprecated use {@link #IS_DEV} instead */ @Deprecated - IS_DEV_LEGACY("IS_DEV"), + IS_DEV_LEGACY("IS_DEV", "(deprecated) Enable development mode."), + IS_DEV("DEFECTDOJO_IS_DEV", "Enable development mode."), + SCAN_NAME("SCAN_NAME", "(provided) secureCodeBox wide environment variable populated with name of the scan custom resource."), + NAMESPACE("NAMESPACE", "(provided) secureCodeBox wide environment variable populated with the Kubernetes namespace the scan is running in."), + LOW_PRIVILEGED_MODE("DEFECTDOJO_LOW_PRIVILEGED_MODE", "Whether low privilege mode is enabled."), /** - * Enable development mode. - */ - IS_DEV("DEFECTDOJO_IS_DEV"), - /** - * secureCodeBox wide environment variable populated with name of the scan custom resource - */ - SCAN_NAME("SCAN_NAME"), - /** - * secureCodeBox wide environment variable populated with the Kubernetes namespace the scan is running in - */ - NAMESPACE("NAMESPACE"), - /** - * Whether low privilege mode is enabled - */ - LOW_PRIVILEGED_MODE("DEFECTDOJO_LOW_PRIVILEGED_MODE"), - /** - * Seconds to wait until re-fetching findings from DefectDojo - * * @deprecated see {@link EnvConfig#refetchWaitSeconds()} */ @Deprecated - REFETCH_WAIT_SECONDS("DEFECTDOJO_REFETCH_WAIT_SECONDS"); + REFETCH_WAIT_SECONDS("DEFECTDOJO_REFETCH_WAIT_SECONDS", "(deprecated) Seconds to wait until re-fetching findings from DefectDojo."); private final String literal; + private final String description; - EnvVarNames(String literal) { + EnvVarNames(String literal, String description) { this.literal = literal; + this.description = description; } } } diff --git a/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/config/PersistenceProviderConfig.java b/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/config/PersistenceProviderConfig.java index 74a0b07037..56a345d3ee 100644 --- a/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/config/PersistenceProviderConfig.java +++ b/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/config/PersistenceProviderConfig.java @@ -6,9 +6,9 @@ import io.securecodebox.persistence.exceptions.DefectDojoPersistenceException; import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; import lombok.extern.slf4j.Slf4j; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.time.ZoneId; import java.util.List; @@ -18,51 +18,94 @@ * the Hook is run in ReadOnly or ReadAndWrite mode based on the number of args. */ @Slf4j -public class PersistenceProviderConfig { - private final EnvConfig env = new EnvConfig(); - +@ToString +public final class PersistenceProviderConfig { private static final int RAW_RESULT_DOWNLOAD_ARG_POSITION = 0; private static final int FINDING_DOWNLOAD_ARG_POSITION = 1; - private static final int RAW_RESULT_UPLOAD_ARG_POSITION = 2; private static final int FINDING_UPLOAD_ARG_POSITION = 3; + public static final int NUMBER_OF_ARGS_READONLY = 2; + public static final int NUMBER_OF_ARGS_READWRITE = 4; - // DefectDojo does in contrast to secureCodeBox not pay attention to time zones - // to guarantee consistent results when converting back and forth a time zone - // has to be assumed for DefectDojo. It defaults to the Time Zone of the system clock + private final EnvConfig env = new EnvConfig(); + /** + * Assumed time zone of DefectDojo + *

+ * DefectDojo does in contrast to secureCodeBox not pay attention to time zones + * to guarantee consistent results when converting back and forth a time zone + * has to be assumed for DefectDojo. It defaults to the Time Zone of the system clock. + *

+ */ + @Getter + final ZoneId defectDojoTimezoneId = ZoneId.systemDefault(); @Getter - ZoneId defectDojoTimezoneId = ZoneId.systemDefault(); + final boolean readOnly; - // Download Urls + /** + * URL where to download the raw result file + */ @Getter final String rawResultDownloadUrl; + /** + * URL where to download the parsed finding file + */ @Getter final String findingDownloadUrl; - - // Upload Urls + /** + * URL where to upload the raw result file, maybe {@code null} + */ final String rawResultUploadUrl; + /** + * URL where to upload the parsed finding file, maybe {@code null} + */ final String findingUploadUrl; + /** + * Provider configuration + * + * @param args not {@code null}, hook args passed via command line flags + */ + public PersistenceProviderConfig(@NonNull final String[] args) { + if (args.length == NUMBER_OF_ARGS_READONLY) { + this.readOnly = true; + this.rawResultDownloadUrl = args[RAW_RESULT_DOWNLOAD_ARG_POSITION]; + this.findingDownloadUrl = args[FINDING_DOWNLOAD_ARG_POSITION]; + // Not set for ReadOnly hooks + this.rawResultUploadUrl = null; + this.findingUploadUrl = null; + } else if (args.length == NUMBER_OF_ARGS_READWRITE) { + this.readOnly = false; + this.rawResultDownloadUrl = args[RAW_RESULT_DOWNLOAD_ARG_POSITION]; + this.findingDownloadUrl = args[FINDING_DOWNLOAD_ARG_POSITION]; + this.rawResultUploadUrl = args[RAW_RESULT_UPLOAD_ARG_POSITION]; + this.findingUploadUrl = args[FINDING_UPLOAD_ARG_POSITION]; + } else { + final var msg = "Unexpected number of arguments given %d! Expected are either %d or %d arguments in array!"; + throw new DefectDojoPersistenceException( + String.format(msg, args.length, NUMBER_OF_ARGS_READONLY, NUMBER_OF_ARGS_READWRITE)); + } + } + + /** + * Throws {@link DefectDojoPersistenceException} if {@link #isReadOnly()} is {@code true} + */ public String getRawResultUploadUrl() { if (isReadOnly()) { - throw new DefectDojoPersistenceException("Cannot Access RawResult Upload URL as the hook is run is ReadOnly mode."); + throw new DefectDojoPersistenceException("Cannot access the RawResult Upload URL because the hook is executed in ReadOnly mode!"); } return rawResultUploadUrl; } + /** + * Throws {@link DefectDojoPersistenceException} if {@link #isReadOnly()} is {@code true} + */ public String getFindingUploadUrl() { if (isReadOnly()) { - throw new DefectDojoPersistenceException("Cannot Access Finding Upload URL as the hook is run is ReadOnly mode."); + throw new DefectDojoPersistenceException("Cannot access the Finding Upload URL because the hook is executed in ReadOnly mode!"); } return findingUploadUrl; } - final boolean readOnly; - - public boolean isReadOnly() { - return readOnly; - } - public boolean isReadAndWrite() { return !readOnly; } @@ -71,28 +114,4 @@ public boolean isInLowPrivilegedMode() { return env.lowPrivilegedMode(); } - public PersistenceProviderConfig(String[] args) { - // Parse Hook Args passed via command line flags - if (args == null) { - throw new DefectDojoPersistenceException("Received `null` as command line flags. Expected exactly four (RawResult & Finding Up/Download Urls)"); - } else if (args.length == 2) { - this.readOnly = true; - - this.rawResultDownloadUrl = args[RAW_RESULT_DOWNLOAD_ARG_POSITION]; - this.findingDownloadUrl = args[FINDING_DOWNLOAD_ARG_POSITION]; - // Not set for ReadOnly hooks - this.rawResultUploadUrl = null; - this.findingUploadUrl = null; - } else if (args.length == 4) { - this.readOnly = false; - - this.rawResultDownloadUrl = args[RAW_RESULT_DOWNLOAD_ARG_POSITION]; - this.findingDownloadUrl = args[FINDING_DOWNLOAD_ARG_POSITION]; - this.rawResultUploadUrl = args[RAW_RESULT_UPLOAD_ARG_POSITION]; - this.findingUploadUrl = args[FINDING_UPLOAD_ARG_POSITION]; - } else { - log.error("Received unexpected command line arguments: {}", List.of(args)); - throw new DefectDojoPersistenceException("DefectDojo Hook received a unexpected number of command line flags. Expected exactly two (for ReadOnly Mode) or four (for ReadAndWrite mode)"); - } - } } diff --git a/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/exceptions/DefectDojoPersistenceException.java b/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/exceptions/DefectDojoPersistenceException.java index 1a2c81346b..dedb3a2d25 100644 --- a/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/exceptions/DefectDojoPersistenceException.java +++ b/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/exceptions/DefectDojoPersistenceException.java @@ -7,10 +7,21 @@ * The base error type of this hook */ public class DefectDojoPersistenceException extends RuntimeException { + /** + * Creates an exception with a message + * + * @param message must not be {@code null} ar empty. Should be formatted to be directly printed to STDERR. + */ public DefectDojoPersistenceException(String message) { - super(message); + this(message, null); } + /** + * Dedicated constructor + * + * @param message see {@link #DefectDojoPersistenceException(String} + * @param cause may be {@code null} if context where the exception occurred is unnecessary. + */ public DefectDojoPersistenceException(String message, Throwable cause) { super(message, cause); } diff --git a/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/service/KubernetesService.java b/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/service/KubernetesService.java index 740b33d3b9..58194eefb5 100644 --- a/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/service/KubernetesService.java +++ b/hooks/persistence-defectdojo/hook/src/main/java/io/securecodebox/persistence/service/KubernetesService.java @@ -6,6 +6,7 @@ import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.util.ClientBuilder; +import io.kubernetes.client.util.Config; import io.kubernetes.client.util.KubeConfig; import io.kubernetes.client.util.generic.GenericKubernetesApi; import io.securecodebox.models.V1Scan; @@ -20,6 +21,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.HashMap; @@ -41,12 +43,29 @@ public void init() throws IOException { final ClientBuilder clientBuilder; if (env.isDev()) { + log.warn("Hook is executed in DEV MODE!"); // loading the out-of-cluster config, a kubeconfig from file-system // FIXME: Usage of reading system properties should be encapsulated in own class. - String kubeConfigPath = System.getProperty("user.home") + "/.kube/config"; - clientBuilder = ClientBuilder.kubeconfig(KubeConfig.loadKubeConfig(new FileReader(kubeConfigPath))); + final var kubeConfigPath = System.getProperty("user.home") + "/.kube/config"; + try (final var kubeConfigReader = new FileReader(kubeConfigPath)) { + clientBuilder = ClientBuilder.kubeconfig(KubeConfig.loadKubeConfig(kubeConfigReader)); + } catch (final IOException e) { + final var msg = String.format("Can't read Kubernetes configuration! Tried file path was '%s'.", kubeConfigPath); + throw new DefectDojoPersistenceException(msg); + } catch (final Exception e) { + final var msg = "Can't parse and create Kubernetes config! Reason: " + e.getMessage(); + throw new DefectDojoPersistenceException(msg, e); + } } else { - clientBuilder = ClientBuilder.cluster(); + try { + clientBuilder = ClientBuilder.cluster(); + } catch (final IllegalStateException e) { + final var msg = String.format( + "Could not create Kubernetes client config! Maybe the env var '%s' and/or '%s' is not set correct" + + "ly.", + Config.ENV_SERVICE_HOST,Config.ENV_SERVICE_PORT); + throw new DefectDojoPersistenceException(msg); + } } this.client = clientBuilder diff --git a/hooks/persistence-defectdojo/hook/src/test/java/io/securecodebox/persistence/DefectDojoPersistenceProviderTest.java b/hooks/persistence-defectdojo/hook/src/test/java/io/securecodebox/persistence/DefectDojoPersistenceProviderTest.java new file mode 100644 index 0000000000..c6c15de057 --- /dev/null +++ b/hooks/persistence-defectdojo/hook/src/test/java/io/securecodebox/persistence/DefectDojoPersistenceProviderTest.java @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: the secureCodeBox authors +// +// SPDX-License-Identifier: Apache-2.0 +package io.securecodebox.persistence; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class DefectDojoPersistenceProviderTest { + + private final DefectDojoPersistenceProvider sut = new DefectDojoPersistenceProvider(); + + private static Stream provideWrongNumberOfArgumentsFixtures() { + return Stream.of( + Arguments.of(new String[0], false), + Arguments.of(new String[1], false), + Arguments.of(new String[2], true), + Arguments.of(new String[3], false), + Arguments.of(new String[4], true), + Arguments.of(new String[5], false), + Arguments.of(new String[6], false), + Arguments.of(new String[7], false), + Arguments.of(new String[8], false), + Arguments.of(new String[9], false), + Arguments.of(new String[10], false) + ); + } + + @ParameterizedTest + @MethodSource("provideWrongNumberOfArgumentsFixtures") + void wrongNumberOfArguments(final String[] args, final boolean numberOfArgsCorrect) { + assertThat(sut.wrongNumberOfArguments(args), is(numberOfArgsCorrect)); + } + + private static Stream provideShouldShowHelpFixtures() { + return Stream.of( + Arguments.of(new String[]{}, false), + Arguments.of(new String[]{"foo"}, false), + Arguments.of(new String[]{"foo", "bar"}, false), + Arguments.of(new String[]{"foo", "bar", "baz"}, false), + Arguments.of(new String[]{"-h"}, true), + Arguments.of(new String[]{"--help"}, true), + Arguments.of(new String[]{"foo", "-h", "baz"}, true), + Arguments.of(new String[]{"foo", "bar", "--help"}, true) + ); + } + + @ParameterizedTest + @MethodSource("provideShouldShowHelpFixtures") + void shouldShowHelp(final String[] args, final boolean showHelp) { + assertThat(sut.shouldShowHelp(args), is(showHelp)); + } + +} diff --git a/hooks/persistence-defectdojo/hook/src/test/java/io/securecodebox/persistence/config/PersistenceProviderConfigTest.java b/hooks/persistence-defectdojo/hook/src/test/java/io/securecodebox/persistence/config/PersistenceProviderConfigTest.java new file mode 100644 index 0000000000..5d5655f4dd --- /dev/null +++ b/hooks/persistence-defectdojo/hook/src/test/java/io/securecodebox/persistence/config/PersistenceProviderConfigTest.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: the secureCodeBox authors +// +// SPDX-License-Identifier: Apache-2.0 +package io.securecodebox.persistence.config; + +import io.securecodebox.persistence.exceptions.DefectDojoPersistenceException; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.*; + +class PersistenceProviderConfigTest { + @Test + void constructorRequiresNonNullArgument() { + assertThrows(NullPointerException.class, () -> new PersistenceProviderConfig(null)); + } + + @Test + void constructorWithTwoArgsCreatesReadOnlyConfig() { + final var sut = new PersistenceProviderConfig(new String[]{"foo", "bar"}); + + assertAll( + () -> assertThat(sut.isReadOnly(), is(true)), + () -> assertThat(sut.isReadAndWrite(), is(false)), + () -> assertThat(sut.getRawResultDownloadUrl(), is("foo")), + () -> assertThat(sut.getFindingDownloadUrl(), is("bar")), + () -> assertThrows(DefectDojoPersistenceException.class, sut::getRawResultUploadUrl), + () -> assertThrows(DefectDojoPersistenceException.class, sut::getFindingUploadUrl) + ); + } + + @Test + void constructorWithFourArgsCreatesReadWriteConfig() { + final var sut = new PersistenceProviderConfig(new String[]{"foo", "bar", "baz", "snafu"}); + + assertAll( + () -> assertThat(sut.isReadOnly(), is(false)), + () -> assertThat(sut.isReadAndWrite(), is(true)), + () -> assertThat(sut.getRawResultDownloadUrl(), is("foo")), + () -> assertThat(sut.getFindingDownloadUrl(), is("bar")), + () -> assertThat(sut.getRawResultUploadUrl(), is("baz")), + () -> assertThat(sut.getFindingUploadUrl(), is("snafu")) + ); + } + + @Test + void constructorThrowsExceptionForWrongArgumentLength() { + assertAll( + () -> assertThrows(DefectDojoPersistenceException.class, () -> new PersistenceProviderConfig(new String[0])), + () -> assertThrows(DefectDojoPersistenceException.class, () -> new PersistenceProviderConfig(new String[]{"foo"})), + () -> assertThrows(DefectDojoPersistenceException.class, () -> new PersistenceProviderConfig(new String[]{"foo", "bar", "baz"})), + () -> assertThrows(DefectDojoPersistenceException.class, () -> new PersistenceProviderConfig(new String[]{"foo", "bar", "baz", "snafu", "shtf"})) + ); + } +}