diff --git a/api/src/main/openapi/paths/extension-points__name__extensions__name__config.yaml b/api/src/main/openapi/paths/extension-points__name__extensions__name__config.yaml index 4a0d55b3b5..e43cd88f8c 100644 --- a/api/src/main/openapi/paths/extension-points__name__extensions__name__config.yaml +++ b/api/src/main/openapi/paths/extension-points__name__extensions__name__config.yaml @@ -90,7 +90,13 @@ put: "304": description: Not Modified "400": - $ref: "../components/responses/json-schema-validation-error.yaml" + description: Bad Request + content: + application/problem+json: + schema: + anyOf: + - $ref: "../components/schemas/json-schema-validation-problem-details.yaml" + - $ref: "../components/schemas/problem-details.yaml" "401": $ref: "../components/responses/generic-unauthorized-error.yaml" "403": diff --git a/apiserver/src/main/java/org/dependencytrack/plugin/ConfigRegistryImpl.java b/apiserver/src/main/java/org/dependencytrack/plugin/ConfigRegistryImpl.java index 92a7e3c234..8b7160ca03 100644 --- a/apiserver/src/main/java/org/dependencytrack/plugin/ConfigRegistryImpl.java +++ b/apiserver/src/main/java/org/dependencytrack/plugin/ConfigRegistryImpl.java @@ -28,6 +28,7 @@ import org.eclipse.microprofile.config.Config; import org.jspecify.annotations.Nullable; +import java.util.Optional; import java.util.function.Function; import static java.util.Objects.requireNonNull; @@ -67,9 +68,9 @@ public DeploymentConfig getDeploymentConfig() { } @Override - public @Nullable RuntimeConfig getRuntimeConfig() { + public Optional getOptionalRuntimeConfig() { if (runtimeConfigSpec == null) { - return null; + return Optional.empty(); } requireNonNull(runtimeConfigMapper, "runtimeConfigMapper is not initialized"); requireNonNull(secretResolver, "secretResolver is not initialized"); @@ -78,14 +79,20 @@ public DeploymentConfig getDeploymentConfig() { handle -> handle.attach(ExtensionConfigDao.class).getConfig( extensionPointName, extensionName)); if (configJson == null) { - return null; + return Optional.empty(); } final JsonNode configJsonNode = runtimeConfigMapper.validateJson(configJson, runtimeConfigSpec); runtimeConfigMapper.resolveSecretRefs(configJsonNode, runtimeConfigSpec, secretResolver); - return runtimeConfigMapper.convert(configJsonNode, runtimeConfigSpec.configClass()); + final RuntimeConfig runtimeConfig = runtimeConfigMapper.convert(configJsonNode, runtimeConfigSpec.configClass()); + + if (runtimeConfigSpec.validator() != null) { + runtimeConfigSpec.validator().validate(runtimeConfig); + } + + return Optional.of(runtimeConfig); } @Override diff --git a/apiserver/src/main/java/org/dependencytrack/plugin/PluginManager.java b/apiserver/src/main/java/org/dependencytrack/plugin/PluginManager.java index 7306c130fc..abc13926d8 100644 --- a/apiserver/src/main/java/org/dependencytrack/plugin/PluginManager.java +++ b/apiserver/src/main/java/org/dependencytrack/plugin/PluginManager.java @@ -403,7 +403,7 @@ private void loadExtension( } LOGGER.debug("Creating runtime extension configs with defaults if necessary"); - if (configRegistry.getRuntimeConfig() == null) { + if (configRegistry.getOptionalRuntimeConfig().isEmpty()) { final boolean updated = configRegistry.setRuntimeConfig(defaultRuntimeConfig); if (updated) { LOGGER.debug("Created default runtime config"); diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/ExtensionsResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/ExtensionsResource.java index 24ddd9881e..67e1c01a82 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v2/ExtensionsResource.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/ExtensionsResource.java @@ -202,7 +202,12 @@ public Response updateExtensionConfig( final String configJson = Json.createObjectBuilder(request.getConfig()).build().toString(); // Throws when config is invalid or secrets cannot be resolved. - validateConfigAndResolveSecrets(configJson, runtimeConfigSpec); + final JsonNode configNode = validateConfigAndResolveSecrets(configJson, runtimeConfigSpec); + + final RuntimeConfig config = configMapper.convert(configNode, runtimeConfigSpec.configClass()); + if (runtimeConfigSpec.validator() != null) { + runtimeConfigSpec.validator().validate(config); + } final boolean updated = inJdbiTransaction( getAlpineRequest(), @@ -275,6 +280,9 @@ public Response testExtension( final String configJson = Json.createObjectBuilder(request.getConfig()).build().toString(); final JsonNode configNode = validateConfigAndResolveSecrets(configJson, runtimeConfigSpec); runtimeConfig = configMapper.convert(configNode, runtimeConfigSpec.configClass()); + if (runtimeConfigSpec.validator() != null) { + runtimeConfigSpec.validator().validate(runtimeConfig); + } } final ExtensionTestResult testResult; diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/InvalidRuntimeConfigExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/InvalidRuntimeConfigExceptionMapper.java new file mode 100644 index 0000000000..050f1b3770 --- /dev/null +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/InvalidRuntimeConfigExceptionMapper.java @@ -0,0 +1,40 @@ +/* + * 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.resources.v2.exception; + +import jakarta.ws.rs.ext.Provider; +import org.dependencytrack.api.v2.model.ProblemDetails; +import org.dependencytrack.plugin.api.config.InvalidRuntimeConfigException; + +/** + * @since 5.7.0 + */ +@Provider +public final class InvalidRuntimeConfigExceptionMapper extends ProblemDetailsExceptionMapper { + + @Override + ProblemDetails map(InvalidRuntimeConfigException exception) { + return ProblemDetails.builder() + .status(400) + .title("Config Validation Failed") + .detail(exception.getMessage()) + .build(); + } + +} diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/RuntimeConfigValidationExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/RuntimeConfigSchemaValidationExceptionMapper.java similarity index 88% rename from apiserver/src/main/java/org/dependencytrack/resources/v2/exception/RuntimeConfigValidationExceptionMapper.java rename to apiserver/src/main/java/org/dependencytrack/resources/v2/exception/RuntimeConfigSchemaValidationExceptionMapper.java index 2aa7b8b6a0..1d48185070 100644 --- a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/RuntimeConfigValidationExceptionMapper.java +++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/RuntimeConfigSchemaValidationExceptionMapper.java @@ -22,7 +22,7 @@ import jakarta.ws.rs.ext.Provider; import org.dependencytrack.api.v2.model.JsonSchemaValidationError; import org.dependencytrack.api.v2.model.JsonSchemaValidationProblemDetails; -import org.dependencytrack.plugin.runtime.config.RuntimeConfigValidationException; +import org.dependencytrack.plugin.runtime.config.RuntimeConfigSchemaValidationException; import java.util.ArrayList; @@ -30,11 +30,11 @@ * @since 5.7.0 */ @Provider -public final class RuntimeConfigValidationExceptionMapper - extends ProblemDetailsExceptionMapper { +public final class RuntimeConfigSchemaValidationExceptionMapper + extends ProblemDetailsExceptionMapper { @Override - public JsonSchemaValidationProblemDetails map(final RuntimeConfigValidationException exception) { + public JsonSchemaValidationProblemDetails map(final RuntimeConfigSchemaValidationException exception) { final var errors = new ArrayList(exception.getValidationMessages().size()); for (final ValidationMessage validationMessage : exception.getValidationMessages()) { diff --git a/apiserver/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java b/apiserver/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java index 456e4c7e8b..aad91e73e1 100644 --- a/apiserver/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java +++ b/apiserver/src/test/java/org/dependencytrack/event/kafka/processor/VulnerabilityScanResultProcessorTest.java @@ -479,24 +479,28 @@ void canUpdateExistingVulnerabilityTest( case GITHUB -> { final var configRegistry = pluginManager .getMutableConfigRegistry(VulnDataSource.class, "github"); - final var config = configRegistry.getRuntimeConfig(GitHubVulnDataSourceConfig.class); - config.setEnabled(mirrorSourceEnabled); - config.setApiToken("dummy"); - configRegistry.setRuntimeConfig(config); + configRegistry + .getOptionalRuntimeConfig(GitHubVulnDataSourceConfig.class) + .map(config -> config + .withEnabled(mirrorSourceEnabled) + .withApiToken("dummy")) + .ifPresent(configRegistry::setRuntimeConfig); } case NVD -> { final var configRegistry = pluginManager .getMutableConfigRegistry(VulnDataSource.class, "nvd"); - final var config = configRegistry.getRuntimeConfig(NvdVulnDataSourceConfig.class); - config.setEnabled(mirrorSourceEnabled); - configRegistry.setRuntimeConfig(config); + configRegistry + .getOptionalRuntimeConfig(NvdVulnDataSourceConfig.class) + .map(config -> config.withEnabled(mirrorSourceEnabled)) + .ifPresent(configRegistry::setRuntimeConfig); } case OSV -> { final var configRegistry = pluginManager .getMutableConfigRegistry(VulnDataSource.class, "osv"); - final var config = configRegistry.getRuntimeConfig(OsvVulnDataSourceConfig.class); - config.setEnabled(mirrorSourceEnabled); - configRegistry.setRuntimeConfig(config); + configRegistry + .getOptionalRuntimeConfig(OsvVulnDataSourceConfig.class) + .map(config -> config.withEnabled(mirrorSourceEnabled)) + .ifPresent(configRegistry::setRuntimeConfig); } } diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/ExtensionsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/ExtensionsResourceTest.java index cba7f456a7..ecd2cdde99 100644 --- a/apiserver/src/test/java/org/dependencytrack/resources/v2/ExtensionsResourceTest.java +++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/ExtensionsResourceTest.java @@ -31,6 +31,7 @@ import org.dependencytrack.plugin.api.ExtensionPoint; import org.dependencytrack.plugin.api.ExtensionPointSpec; import org.dependencytrack.plugin.api.ExtensionTestResult; +import org.dependencytrack.plugin.api.config.InvalidRuntimeConfigException; import org.dependencytrack.plugin.api.config.RuntimeConfig; import org.dependencytrack.plugin.api.config.RuntimeConfigSchemaSource; import org.dependencytrack.plugin.api.config.RuntimeConfigSpec; @@ -309,6 +310,35 @@ void updateExtensionConfigShouldReturnBadRequestWhenInvalid() { """); } + @Test + void updateExtensionConfigShouldReturnBadRequestWhenConfigValidationFails() { + pluginManager.loadPlugins(List.of( + () -> List.of(new ExtensionWithValidatorFactory()))); + + initializeWithPermissions(Permissions.SYSTEM_CONFIGURATION_UPDATE); + + final Response response = jersey + .target("/extension-points/dummy/extensions/extension-with-validator/config") + .request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "config": { + "outcome": "PASSED" + } + } + """)); + assertThat(response.getStatus()).isEqualTo(400); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 400, + "type": "about:blank", + "title": "Config Validation Failed", + "detail": "Boom!" + } + """); + } + @Test void getExtensionConfigSchemaShouldReturnConfigSchema() { pluginManager.loadPlugins(List.of( @@ -452,7 +482,7 @@ void testExtensionShouldReturnBadRequestWhenExtensionDoesNotSupportTesting() { } @Test - void testExtensionShouldReturnBadRequestWhenConfigIsInvalid() { + void testExtensionShouldReturnBadRequestWhenConfigSchemaValidationFails() { pluginManager.loadPlugins(List.of( () -> List.of(new TestableExtensionFactory()))); @@ -489,6 +519,35 @@ void testExtensionShouldReturnBadRequestWhenConfigIsInvalid() { """); } + @Test + void testExtensionShouldReturnBadRequestWhenConfigValidationFails() { + pluginManager.loadPlugins(List.of( + () -> List.of(new ExtensionWithValidatorFactory()))); + + initializeWithPermissions(Permissions.SYSTEM_CONFIGURATION_UPDATE); + + final Response response = jersey + .target("/extension-points/dummy/extensions/extension-with-validator/test") + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "config": { + "outcome": "PASSED" + } + } + """)); + assertThat(response.getStatus()).isEqualTo(400); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "status": 400, + "type": "about:blank", + "title": "Config Validation Failed", + "detail": "Boom!" + } + """); + } + @ExtensionPointSpec(name = "dummy", required = false) private interface DummyExtensionPoint extends ExtensionPoint { } @@ -521,7 +580,7 @@ public int priority() { @Override public RuntimeConfigSpec runtimeConfigSpec() { final var defaultConfig = new DummyRuntimeConfig("test", null); - return new RuntimeConfigSpec( + return RuntimeConfigSpec.of( defaultConfig, new RuntimeConfigSchemaSource.Literal(/* language=JSON */ """ { @@ -541,7 +600,8 @@ public RuntimeConfigSpec runtimeConfigSpec() { "requiredString" ] } - """)); + """), + null); } @Override @@ -625,7 +685,7 @@ public ExtensionTestResult test(@Nullable RuntimeConfig runtimeConfig) { @Override public @Nullable RuntimeConfigSpec runtimeConfigSpec() { final var defaultConfig = new TestableRuntimeConfig(null); - return new RuntimeConfigSpec( + return RuntimeConfigSpec.of( defaultConfig, new RuntimeConfigSchemaSource.Literal(/* language=JSON */ """ { @@ -641,7 +701,64 @@ public ExtensionTestResult test(@Nullable RuntimeConfig runtimeConfig) { } } } - """)); + """), + null); + } + + } + + private static class ExtensionWithValidatorFactory implements ExtensionFactory { + + @Override + public String extensionName() { + return "extension-with-validator"; + } + + @Override + public Class extensionClass() { + return DummyExtension.class; + } + + @Override + public int priority() { + return 0; + } + + @Override + public void init(ExtensionContext ctx) { + } + + @Override + public @Nullable RuntimeConfigSpec runtimeConfigSpec() { + final var defaultConfig = new TestableRuntimeConfig(null); + return RuntimeConfigSpec.of( + defaultConfig, + new RuntimeConfigSchemaSource.Literal(/* language=JSON */ """ + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "outcome": { + "type": "string", + "enum": [ + "PASSED", + "FAILED" + ] + } + } + } + """), + config -> { + final var actualConfig = (TestableRuntimeConfig) config; + if (actualConfig.outcome() != null) { + throw new InvalidRuntimeConfigException("Boom!"); + } + }); + } + + @Override + public DummyExtensionPoint create() { + return new DummyExtension(); } } diff --git a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/email/EmailNotificationPublisherFactory.java b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/email/EmailNotificationPublisherFactory.java index 3248f0df18..c665ef8004 100644 --- a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/email/EmailNotificationPublisherFactory.java +++ b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/email/EmailNotificationPublisherFactory.java @@ -66,7 +66,7 @@ public RuntimeConfigSpec ruleConfigSpec() { .withSenderAddress("dependencytrack@localhost") .withSubjectPrefix("[Dependency-Track]"); - return new RuntimeConfigSpec(defaultConfig); + return RuntimeConfigSpec.of(defaultConfig); } @Override diff --git a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/jira/JiraNotificationPublisherFactory.java b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/jira/JiraNotificationPublisherFactory.java index f3ca3c06f7..5ea80f0cab 100644 --- a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/jira/JiraNotificationPublisherFactory.java +++ b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/jira/JiraNotificationPublisherFactory.java @@ -74,7 +74,7 @@ public RuntimeConfigSpec ruleConfigSpec() { .withProjectKey("EXAMPLE") .withTicketType("TASK"); - return new RuntimeConfigSpec(defaultConfig); + return RuntimeConfigSpec.of(defaultConfig); } @Override diff --git a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/kafka/KafkaNotificationPublisherFactory.java b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/kafka/KafkaNotificationPublisherFactory.java index 7896db6a2a..c3a78e238e 100644 --- a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/kafka/KafkaNotificationPublisherFactory.java +++ b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/kafka/KafkaNotificationPublisherFactory.java @@ -86,7 +86,7 @@ public RuntimeConfigSpec ruleConfigSpec() { .withTopicName("dependencytrack-notifications") .withPublishProtobuf(true); - return new RuntimeConfigSpec(defaultConfig); + return RuntimeConfigSpec.of(defaultConfig); } @Override diff --git a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/mattermost/MattermostNotificationPublisherFactory.java b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/mattermost/MattermostNotificationPublisherFactory.java index c1a338f6a3..469e0ec068 100644 --- a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/mattermost/MattermostNotificationPublisherFactory.java +++ b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/mattermost/MattermostNotificationPublisherFactory.java @@ -72,7 +72,7 @@ public RuntimeConfigSpec ruleConfigSpec() { final var defaultConfig = new HttpNotificationRuleConfig() .withDestinationUrl(URI.create("https://mattermost.example.com")); - return new RuntimeConfigSpec(defaultConfig); + return RuntimeConfigSpec.of(defaultConfig); } @Override diff --git a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/msteams/MsTeamsNotificationPublisherFactory.java b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/msteams/MsTeamsNotificationPublisherFactory.java index 12437d501d..fc5e140ac6 100644 --- a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/msteams/MsTeamsNotificationPublisherFactory.java +++ b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/msteams/MsTeamsNotificationPublisherFactory.java @@ -72,7 +72,7 @@ public RuntimeConfigSpec ruleConfigSpec() { final var defaultConfig = new HttpNotificationRuleConfig() .withDestinationUrl(URI.create("https://msteams.example.com")); - return new RuntimeConfigSpec(defaultConfig); + return RuntimeConfigSpec.of(defaultConfig); } @Override diff --git a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/slack/SlackNotificationPublisherFactory.java b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/slack/SlackNotificationPublisherFactory.java index 22759ec07e..8f63b18afa 100644 --- a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/slack/SlackNotificationPublisherFactory.java +++ b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/slack/SlackNotificationPublisherFactory.java @@ -72,7 +72,7 @@ public RuntimeConfigSpec ruleConfigSpec() { final var defaultConfig = new HttpNotificationRuleConfig() .withDestinationUrl(URI.create("https://slack.example.com")); - return new RuntimeConfigSpec(defaultConfig); + return RuntimeConfigSpec.of(defaultConfig); } @Override diff --git a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/webex/WebexNotificationPublisherFactory.java b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/webex/WebexNotificationPublisherFactory.java index b5da0e0fe6..3f76843aec 100644 --- a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/webex/WebexNotificationPublisherFactory.java +++ b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/webex/WebexNotificationPublisherFactory.java @@ -72,7 +72,7 @@ public RuntimeConfigSpec ruleConfigSpec() { final var defaultConfig = new HttpNotificationRuleConfig() .withDestinationUrl(URI.create("https://webex.example.com")); - return new RuntimeConfigSpec(defaultConfig); + return RuntimeConfigSpec.of(defaultConfig); } @Override diff --git a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/webhook/WebhookNotificationPublisherFactory.java b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/webhook/WebhookNotificationPublisherFactory.java index b5e6f4fb88..8d37ed35ec 100644 --- a/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/webhook/WebhookNotificationPublisherFactory.java +++ b/notification/publishing/src/main/java/org/dependencytrack/notification/publishing/webhook/WebhookNotificationPublisherFactory.java @@ -72,7 +72,7 @@ public RuntimeConfigSpec ruleConfigSpec() { final var defaultConfig = new HttpNotificationRuleConfig() .withDestinationUrl(URI.create("https://example.com")); - return new RuntimeConfigSpec(defaultConfig); + return RuntimeConfigSpec.of(defaultConfig); } @Override diff --git a/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/ConfigRegistry.java b/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/ConfigRegistry.java index e627fb2d13..1afe8f1e8c 100644 --- a/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/ConfigRegistry.java +++ b/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/ConfigRegistry.java @@ -18,7 +18,8 @@ */ package org.dependencytrack.plugin.api.config; -import org.jspecify.annotations.Nullable; +import java.util.NoSuchElementException; +import java.util.Optional; /** * A read-only registry for accessing application configuration. @@ -28,22 +29,49 @@ public interface ConfigRegistry { /** + * Retrieve the deployment config. + * + * @return The deployment config. * @since 5.7.0 */ DeploymentConfig getDeploymentConfig(); /** + * Retrieve the runtime config. + * + * @return The runtime config. * @since 5.7.0 */ - @Nullable RuntimeConfig getRuntimeConfig(); + Optional getOptionalRuntimeConfig(); /** + * Retrieve the runtime config. + * + * @param configClass Class of the runtime config. + * @param Type of the runtime config. + * @return The runtime config. * @throws ClassCastException When the config object can not be cast to the provided {@code configClass}. - * @see #getRuntimeConfig() + * @see #getOptionalRuntimeConfig() * @since 5.7.0 */ - default @Nullable T getRuntimeConfig(Class configClass) { - return configClass.cast(getRuntimeConfig()); + default Optional getOptionalRuntimeConfig(Class configClass) { + return getOptionalRuntimeConfig().map(configClass::cast); + } + + /** + * Retrieve the runtime config, throwing if it doesn't exist. + * + * @param configClass Class of the runtime config. + * @param Type of the runtime config. + * @return The runtime config. + * @throws NoSuchElementException When no runtime config exists. + * @see #getOptionalRuntimeConfig(Class) + * @since 5.7.0 + */ + default T getRuntimeConfig(Class configClass) { + return getOptionalRuntimeConfig() + .map(configClass::cast) + .orElseThrow(() -> new NoSuchElementException("No runtime config found")); } } diff --git a/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/InvalidRuntimeConfigException.java b/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/InvalidRuntimeConfigException.java new file mode 100644 index 0000000000..fc5acdeba6 --- /dev/null +++ b/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/InvalidRuntimeConfigException.java @@ -0,0 +1,32 @@ +/* + * 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.plugin.api.config; + +import static java.util.Objects.requireNonNull; + +/** + * @since 5.7.0 + */ +public class InvalidRuntimeConfigException extends IllegalArgumentException { + + public InvalidRuntimeConfigException(String message) { + super(requireNonNull(message, "message must not be null")); + } + +} diff --git a/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/RuntimeConfigSpec.java b/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/RuntimeConfigSpec.java index 3a552e5489..493539074d 100644 --- a/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/RuntimeConfigSpec.java +++ b/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/RuntimeConfigSpec.java @@ -28,25 +28,46 @@ public final class RuntimeConfigSpec { private final Class configClass; + @SuppressWarnings("rawtypes") + private final @Nullable RuntimeConfigValidator validator; private final RuntimeConfig defaultConfig; private final String schema; - public RuntimeConfigSpec( + @SuppressWarnings("rawtypes") + private RuntimeConfigSpec( RuntimeConfig defaultConfig, - @Nullable RuntimeConfigSchemaSource schemaSource) { + @Nullable RuntimeConfigSchemaSource schemaSource, + @Nullable RuntimeConfigValidator validator) { this.defaultConfig = requireNonNull(defaultConfig, "defaultConfig must not be null"); this.configClass = defaultConfig.getClass(); this.schema = loadSchema(configClass, schemaSource); + this.validator = validator; + } + + public static RuntimeConfigSpec of( + T defaultConfig, + RuntimeConfigSchemaSource schemaSource, + RuntimeConfigValidator validator) { + return new RuntimeConfigSpec(defaultConfig, schemaSource, validator); } - public RuntimeConfigSpec(RuntimeConfig defaultConfig) { - this(defaultConfig, null); + public static RuntimeConfigSpec of(T defaultConfig, RuntimeConfigValidator validator) { + return new RuntimeConfigSpec(defaultConfig, null, validator); + } + + public static RuntimeConfigSpec of(T defaultConfig) { + return new RuntimeConfigSpec(defaultConfig, null, null); } public Class configClass() { return configClass; } + @SuppressWarnings("rawtypes") + public @Nullable RuntimeConfigValidator validator() { + return validator; + } + public RuntimeConfig defaultConfig() { return defaultConfig; } diff --git a/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/RuntimeConfigValidator.java b/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/RuntimeConfigValidator.java new file mode 100644 index 0000000000..ebff7879bc --- /dev/null +++ b/plugin/api/src/main/java/org/dependencytrack/plugin/api/config/RuntimeConfigValidator.java @@ -0,0 +1,37 @@ +/* + * 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.plugin.api.config; + +/** + * @since 5.7.0 + */ +public interface RuntimeConfigValidator { + + /** + * Validate the runtime config for semantic issues. + *

+ * When this method is invoked, the provided config has already been + * validated against its JSON schema. + * + * @param config The config to validate. + * @throws InvalidRuntimeConfigException When the config is invalid. + */ + void validate(T config); + +} diff --git a/plugin/runtime/src/main/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigMapper.java b/plugin/runtime/src/main/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigMapper.java index a2855b615e..299f7a29c2 100644 --- a/plugin/runtime/src/main/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigMapper.java +++ b/plugin/runtime/src/main/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigMapper.java @@ -108,27 +108,6 @@ public static RuntimeConfigMapper getInstance() { return INSTANCE; } - /** - * Deserialize a given config in JSON format. - * - * @param configJson The config in JSON format. - * @param configClass Class to deserialize the config into. - * @param Type of the config. - * @return The deserialized {@link RuntimeConfig}. - * @throws NullPointerException When either {@code configJson} or {@code configClass} are {@code null}. - * @throws UncheckedIOException When deserialization failed. - */ - public T deserialize(String configJson, Class configClass) { - requireNonNull(configJson, "configJson must not be null"); - requireNonNull(configClass, "configClass must not be null"); - - try { - return jsonMapper.readValue(configJson, configClass); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - public T convert(JsonNode configJsonNode, Class configClass) { requireNonNull(configJsonNode, "configJsonNode must not be null"); requireNonNull(configClass, "configClass must not be null"); @@ -163,7 +142,7 @@ public String serialize(RuntimeConfig config) { * @param runtimeConfigSpec The applicable config spec. * @throws NullPointerException When either {@code config} or {@code configSchemaJson} are {@code null}. * @throws UncheckedIOException When parsing the config JSON failed. - * @throws RuntimeConfigValidationException When the config failed validation. + * @throws RuntimeConfigSchemaValidationException When the config failed validation. */ public JsonNode validate(T config, RuntimeConfigSpec runtimeConfigSpec) { requireNonNull(config, "config must not be null"); @@ -174,7 +153,11 @@ public JsonNode validate(T config, RuntimeConfigSpec r final Set validationMessages = schema.jsonSchema().validate(configNode); if (!validationMessages.isEmpty()) { - throw new RuntimeConfigValidationException(validationMessages); + throw new RuntimeConfigSchemaValidationException(validationMessages); + } + + if (runtimeConfigSpec.validator() != null) { + runtimeConfigSpec.validator().validate(config); } return configNode; @@ -187,7 +170,7 @@ public JsonNode validate(T config, RuntimeConfigSpec r * @param runtimeConfigSpec The applicable config spec. * @throws NullPointerException When either {@code configJson} or {@code configSchemaJson} are {@code null}. * @throws UncheckedIOException When parsing the config JSON failed. - * @throws RuntimeConfigValidationException When the config failed validation. + * @throws RuntimeConfigSchemaValidationException When the config failed validation. */ public JsonNode validateJson(String configJson, RuntimeConfigSpec runtimeConfigSpec) { requireNonNull(configJson, "configJson must not be null"); @@ -204,7 +187,7 @@ public JsonNode validateJson(String configJson, RuntimeConfigSpec runtimeConfigS final Set validationMessages = schema.jsonSchema().validate(configNode); if (!validationMessages.isEmpty()) { - throw new RuntimeConfigValidationException(validationMessages); + throw new RuntimeConfigSchemaValidationException(validationMessages); } return configNode; diff --git a/plugin/runtime/src/main/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigValidationException.java b/plugin/runtime/src/main/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigSchemaValidationException.java similarity index 83% rename from plugin/runtime/src/main/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigValidationException.java rename to plugin/runtime/src/main/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigSchemaValidationException.java index e85a2e1853..aac180216a 100644 --- a/plugin/runtime/src/main/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigValidationException.java +++ b/plugin/runtime/src/main/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigSchemaValidationException.java @@ -19,6 +19,7 @@ package org.dependencytrack.plugin.runtime.config; import com.networknt.schema.ValidationMessage; +import org.dependencytrack.plugin.api.config.InvalidRuntimeConfigException; import java.util.Collection; import java.util.stream.Collectors; @@ -26,11 +27,11 @@ /** * @since 5.7.0 */ -public final class RuntimeConfigValidationException extends RuntimeException { +public final class RuntimeConfigSchemaValidationException extends InvalidRuntimeConfigException { private final Collection validationMessages; - public RuntimeConfigValidationException(Collection validationMessages) { + public RuntimeConfigSchemaValidationException(Collection validationMessages) { super("Runtime config is invalid: [%s]".formatted( validationMessages.stream() .map(ValidationMessage::getMessage) diff --git a/plugin/runtime/src/test/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigMapperTest.java b/plugin/runtime/src/test/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigMapperTest.java index ae7d6174a8..e0502c2bc6 100644 --- a/plugin/runtime/src/test/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigMapperTest.java +++ b/plugin/runtime/src/test/java/org/dependencytrack/plugin/runtime/config/RuntimeConfigMapperTest.java @@ -24,14 +24,13 @@ import org.junit.jupiter.api.Test; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; class RuntimeConfigMapperTest { private final RuntimeConfigMapper runtimeConfigMapper = new RuntimeConfigMapper(); - private final RuntimeConfigSpec configSpec = new RuntimeConfigSpec(new TestRuntimeConfig()); + private final RuntimeConfigSpec configSpec = RuntimeConfigSpec.of(new TestRuntimeConfig()); @Nested class SerializeTest { @@ -62,38 +61,6 @@ void shouldThrowWhenConfigIsNull() { } - @Nested - class DeserializeTest { - - @Test - void shouldDeserializeFromJson() { - final var config = runtimeConfigMapper.deserialize(/* language=JSON */ """ - { - "requiredString": "foo" - } - """, - TestRuntimeConfig.class); - - assertThat(config).isNotNull(); - assertThat(config.getRequiredString()).isEqualTo("foo"); - } - - @Test - void shouldThrowWhenConfigSpecIsNull() { - assertThatExceptionOfType(NullPointerException.class) - .isThrownBy(() -> runtimeConfigMapper.deserialize(null, TestRuntimeConfig.class)) - .withMessage("configJson must not be null"); - } - - @Test - void shouldThrowWhenConfigClassIsNull() { - assertThatExceptionOfType(NullPointerException.class) - .isThrownBy(() -> runtimeConfigMapper.deserialize("", null)) - .withMessage("configClass must not be null"); - } - - } - @Nested class ValidateTest { @@ -110,7 +77,7 @@ void shouldNotThrowWhenConfigIsValid() { void shouldThrowWhenConfigIsInvalid() { final var config = new TestRuntimeConfig(); - assertThatExceptionOfType(RuntimeConfigValidationException.class) + assertThatExceptionOfType(RuntimeConfigSchemaValidationException.class) .isThrownBy(() -> runtimeConfigMapper.validate(config, configSpec)); } @@ -147,7 +114,7 @@ void shouldNotThrowWhenConfigJsonIsValid() { @Test void shouldThrowWhenConfigJsonIsInvalid() { - assertThatExceptionOfType(RuntimeConfigValidationException.class) + assertThatExceptionOfType(RuntimeConfigSchemaValidationException.class) .isThrownBy(() -> runtimeConfigMapper.validateJson(/* language=JSON */ """ { "requiredString": null diff --git a/plugin/testing/src/main/java/org/dependencytrack/plugin/testing/MockConfigRegistry.java b/plugin/testing/src/main/java/org/dependencytrack/plugin/testing/MockConfigRegistry.java index dae70e62f7..067cfc1049 100644 --- a/plugin/testing/src/main/java/org/dependencytrack/plugin/testing/MockConfigRegistry.java +++ b/plugin/testing/src/main/java/org/dependencytrack/plugin/testing/MockConfigRegistry.java @@ -29,6 +29,7 @@ import java.util.Collections; import java.util.Map; import java.util.Objects; +import java.util.Optional; import static java.util.Objects.requireNonNull; @@ -82,8 +83,8 @@ public DeploymentConfig getDeploymentConfig() { } @Override - public @Nullable RuntimeConfig getRuntimeConfig() { - return runtimeConfig; + public Optional<@Nullable RuntimeConfig> getOptionalRuntimeConfig() { + return Optional.ofNullable(runtimeConfig); } @Override diff --git a/vuln-data-source/github/src/main/java/org/dependencytrack/vulndatasource/github/GitHubVulnDataSourceFactory.java b/vuln-data-source/github/src/main/java/org/dependencytrack/vulndatasource/github/GitHubVulnDataSourceFactory.java index b69ce78aa5..6125bfba7d 100644 --- a/vuln-data-source/github/src/main/java/org/dependencytrack/vulndatasource/github/GitHubVulnDataSourceFactory.java +++ b/vuln-data-source/github/src/main/java/org/dependencytrack/vulndatasource/github/GitHubVulnDataSourceFactory.java @@ -24,12 +24,11 @@ import org.apache.hc.client5.http.impl.async.HttpAsyncClients; import org.dependencytrack.plugin.api.ExtensionContext; import org.dependencytrack.plugin.api.config.ConfigRegistry; +import org.dependencytrack.plugin.api.config.InvalidRuntimeConfigException; import org.dependencytrack.plugin.api.config.RuntimeConfigSpec; import org.dependencytrack.plugin.api.storage.ExtensionKVStore; import org.dependencytrack.vulndatasource.api.VulnDataSource; import org.dependencytrack.vulndatasource.api.VulnDataSourceFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.net.URI; import java.time.Clock; @@ -43,8 +42,6 @@ */ final class GitHubVulnDataSourceFactory implements VulnDataSourceFactory { - private static final Logger LOGGER = LoggerFactory.getLogger(GitHubVulnDataSourceFactory.class); - private ConfigRegistry configRegistry; private ExtensionKVStore kvStore; private HttpAsyncClientSupplier httpClientSupplier; @@ -76,13 +73,13 @@ public void init(final ExtensionContext ctx) { @Override public boolean isDataSourceEnabled() { - return configRegistry.getRuntimeConfig(GitHubVulnDataSourceConfig.class).getEnabled(); + return configRegistry.getRuntimeConfig(GitHubVulnDataSourceConfig.class).isEnabled(); } @Override public VulnDataSource create() { final var config = configRegistry.getRuntimeConfig(GitHubVulnDataSourceConfig.class); - if (!config.getEnabled()) { + if (!config.isEnabled()) { throw new IllegalStateException("Vulnerability data source is disabled and cannot be created"); } @@ -108,7 +105,17 @@ public RuntimeConfigSpec runtimeConfigSpec() { .withAliasSyncEnabled(true) .withApiUrl(URI.create("https://api.github.com/graphql")); - return new RuntimeConfigSpec(defaultConfig); + return RuntimeConfigSpec.of(defaultConfig, config -> { + if (!config.isEnabled()) { + return; + } + if (config.getApiUrl() == null) { + throw new InvalidRuntimeConfigException("No API URL provided"); + } + if (config.getApiToken() == null) { + throw new InvalidRuntimeConfigException("No API Token provided"); + } + }); } } diff --git a/vuln-data-source/github/src/main/resources/org/dependencytrack/vulndatasource/github/GitHubVulnDataSourceConfig.schema.json b/vuln-data-source/github/src/main/resources/org/dependencytrack/vulndatasource/github/GitHubVulnDataSourceConfig.schema.json index 143cfafe37..0eea5a9034 100644 --- a/vuln-data-source/github/src/main/resources/org/dependencytrack/vulndatasource/github/GitHubVulnDataSourceConfig.schema.json +++ b/vuln-data-source/github/src/main/resources/org/dependencytrack/vulndatasource/github/GitHubVulnDataSourceConfig.schema.json @@ -8,7 +8,8 @@ "enabled": { "type": "boolean", "title": "Enabled", - "description": "Whether the GitHub Advisories data source should be enabled." + "description": "Whether the GitHub Advisories data source should be enabled.", + "existingJavaType": "boolean" }, "aliasSyncEnabled": { "type": "boolean", @@ -31,20 +32,6 @@ } }, "required": [ - "enabled", - "aliasSyncEnabled", - "apiUrl" - ], - "if": { - "properties": { - "enabled": { - "const": true - } - } - }, - "then": { - "required": [ - "apiToken" - ] - } + "enabled" + ] } \ No newline at end of file diff --git a/vuln-data-source/nvd/src/main/java/org/dependencytrack/vulndatasource/nvd/NvdVulnDataSourceFactory.java b/vuln-data-source/nvd/src/main/java/org/dependencytrack/vulndatasource/nvd/NvdVulnDataSourceFactory.java index 30743862f5..32deb5f0c2 100644 --- a/vuln-data-source/nvd/src/main/java/org/dependencytrack/vulndatasource/nvd/NvdVulnDataSourceFactory.java +++ b/vuln-data-source/nvd/src/main/java/org/dependencytrack/vulndatasource/nvd/NvdVulnDataSourceFactory.java @@ -25,6 +25,7 @@ import org.dependencytrack.plugin.api.ExtensionContext; import org.dependencytrack.plugin.api.ExtensionTestResult; import org.dependencytrack.plugin.api.config.ConfigRegistry; +import org.dependencytrack.plugin.api.config.InvalidRuntimeConfigException; import org.dependencytrack.plugin.api.config.RuntimeConfig; import org.dependencytrack.plugin.api.config.RuntimeConfigSpec; import org.dependencytrack.plugin.api.storage.ExtensionKVStore; @@ -94,18 +95,29 @@ public RuntimeConfigSpec runtimeConfigSpec() { .withEnabled(true) .withCveFeedsUrl(URI.create("https://nvd.nist.gov/feeds")); - return new RuntimeConfigSpec(defaultConfig); + return RuntimeConfigSpec.of(defaultConfig, config -> { + if (!config.isEnabled()) { + return; + } + if (config.getCveFeedsUrl() == null) { + throw new InvalidRuntimeConfigException("No CVE feeds URL provided"); + } + }); } @Override public boolean isDataSourceEnabled() { - return configRegistry.getRuntimeConfig(NvdVulnDataSourceConfig.class).getEnabled(); + requireNonNull(configRegistry, "configRegistry must not be null"); + return configRegistry.getRuntimeConfig(NvdVulnDataSourceConfig.class).isEnabled(); } @Override public VulnDataSource create() { + requireNonNull(configRegistry, "configRegistry must not be null"); + requireNonNull(kvStore, "kvStore must not be null"); + final var config = configRegistry.getRuntimeConfig(NvdVulnDataSourceConfig.class); - if (!config.getEnabled()) { + if (!config.isEnabled()) { throw new IllegalStateException("Vulnerability data source is disabled and cannot be created"); } @@ -118,14 +130,13 @@ public VulnDataSource create() { public ExtensionTestResult test(@Nullable RuntimeConfig runtimeConfig) { requireNonNull(configRegistry, "configRegistry has not been initialized"); requireNonNull(httpClient, "httpClient has not been initialized"); + requireNonNull(runtimeConfig, "runtimeConfig must not be null"); - if (!(runtimeConfig instanceof final NvdVulnDataSourceConfig nvdConfig)) { - throw new IllegalArgumentException(); - } + final var nvdConfig = (NvdVulnDataSourceConfig) runtimeConfig; final var testResult = ExtensionTestResult.ofChecks("connection", "feed_format"); - if (!nvdConfig.getEnabled()) { + if (!nvdConfig.isEnabled()) { return testResult; } @@ -133,7 +144,7 @@ public ExtensionTestResult test(@Nullable RuntimeConfig runtimeConfig) { ? URI.create(nvdConfig.getCveFeedsUrl().toString() + "/") : nvdConfig.getCveFeedsUrl(); final URI metadataUri = feedsUrl.resolve("json/cve/2.0/nvdcve-2.0-modified.meta"); - + if (!configRegistry .getDeploymentConfig() .getOptionalValue("allow-local-connections", boolean.class) diff --git a/vuln-data-source/nvd/src/main/resources/org/dependencytrack/vulndatasource/nvd/NvdVulnDataSourceConfig.schema.json b/vuln-data-source/nvd/src/main/resources/org/dependencytrack/vulndatasource/nvd/NvdVulnDataSourceConfig.schema.json index e9f34e59a4..29514fae47 100644 --- a/vuln-data-source/nvd/src/main/resources/org/dependencytrack/vulndatasource/nvd/NvdVulnDataSourceConfig.schema.json +++ b/vuln-data-source/nvd/src/main/resources/org/dependencytrack/vulndatasource/nvd/NvdVulnDataSourceConfig.schema.json @@ -8,7 +8,8 @@ "enabled": { "type": "boolean", "title": "Enabled", - "description": "Whether the NVD data source should be enabled." + "description": "Whether the NVD data source should be enabled.", + "existingJavaType": "boolean" }, "cveFeedsUrl": { "type": "string", @@ -19,7 +20,6 @@ } }, "required": [ - "enabled", - "cveFeedsUrl" + "enabled" ] } \ No newline at end of file diff --git a/vuln-data-source/osv/src/main/java/org/dependencytrack/vulndatasource/osv/OsvVulnDataSourceFactory.java b/vuln-data-source/osv/src/main/java/org/dependencytrack/vulndatasource/osv/OsvVulnDataSourceFactory.java index 9ac6715737..0df1d2a927 100644 --- a/vuln-data-source/osv/src/main/java/org/dependencytrack/vulndatasource/osv/OsvVulnDataSourceFactory.java +++ b/vuln-data-source/osv/src/main/java/org/dependencytrack/vulndatasource/osv/OsvVulnDataSourceFactory.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.dependencytrack.plugin.api.ExtensionContext; import org.dependencytrack.plugin.api.config.ConfigRegistry; +import org.dependencytrack.plugin.api.config.InvalidRuntimeConfigException; import org.dependencytrack.plugin.api.config.RuntimeConfigSpec; import org.dependencytrack.plugin.api.storage.ExtensionKVStore; import org.dependencytrack.vulndatasource.api.VulnDataSource; @@ -79,18 +80,28 @@ public RuntimeConfigSpec runtimeConfigSpec() { .withDataUrl(URI.create("https://storage.googleapis.com/osv-vulnerabilities")) .withEcosystems(Set.of("Go", "Maven", "npm", "NuGet", "PyPI")); - return new RuntimeConfigSpec(defaultConfig); + return RuntimeConfigSpec.of(defaultConfig, config -> { + if (!config.isEnabled()) { + return; + } + if (config.getDataUrl() == null) { + throw new InvalidRuntimeConfigException("No data URL provided"); + } + if (config.getEcosystems() == null || config.getEcosystems().isEmpty()) { + throw new InvalidRuntimeConfigException("At least one ecosystem must be specified"); + } + }); } @Override public boolean isDataSourceEnabled() { - return configRegistry.getRuntimeConfig(OsvVulnDataSourceConfig.class).getEnabled(); + return configRegistry.getRuntimeConfig(OsvVulnDataSourceConfig.class).isEnabled(); } @Override public VulnDataSource create() { final var config = configRegistry.getRuntimeConfig(OsvVulnDataSourceConfig.class); - if (!config.getEnabled()) { + if (!config.isEnabled()) { throw new IllegalStateException("Vulnerability data source is disabled and cannot be created"); } diff --git a/vuln-data-source/osv/src/main/resources/org/dependencytrack/vulndatasource/osv/OsvVulnDataSourceConfig.schema.json b/vuln-data-source/osv/src/main/resources/org/dependencytrack/vulndatasource/osv/OsvVulnDataSourceConfig.schema.json index 5e6fb66986..a1b5c49929 100644 --- a/vuln-data-source/osv/src/main/resources/org/dependencytrack/vulndatasource/osv/OsvVulnDataSourceConfig.schema.json +++ b/vuln-data-source/osv/src/main/resources/org/dependencytrack/vulndatasource/osv/OsvVulnDataSourceConfig.schema.json @@ -8,7 +8,8 @@ "enabled": { "type": "boolean", "title": "Enabled", - "description": "Whether the OSV data source should be enabled." + "description": "Whether the OSV data source should be enabled.", + "existingJavaType": "boolean" }, "aliasSyncEnabled": { "type": "boolean", @@ -26,7 +27,6 @@ "type": "array", "title": "Ecosystems", "description": "The ecosystems to mirror vulnerability data for. \nA list of available ecosystems can be found in the [OSV documentation](https://ossf.github.io/osv-schema/#defined-ecosystems).", - "minItems": 1, "uniqueItems": true, "items": { "type": "string", @@ -36,9 +36,6 @@ } }, "required": [ - "enabled", - "aliasSyncEnabled", - "dataUrl", - "ecosystems" + "enabled" ] } \ No newline at end of file diff --git a/vuln-data-source/osv/src/test/java/org/dependencytrack/vulndatasource/osv/OsvVulnDataSourceTest.java b/vuln-data-source/osv/src/test/java/org/dependencytrack/vulndatasource/osv/OsvVulnDataSourceTest.java index 4493747194..88a931de37 100644 --- a/vuln-data-source/osv/src/test/java/org/dependencytrack/vulndatasource/osv/OsvVulnDataSourceTest.java +++ b/vuln-data-source/osv/src/test/java/org/dependencytrack/vulndatasource/osv/OsvVulnDataSourceTest.java @@ -43,6 +43,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -147,7 +148,7 @@ void testCloseWithCompletedEcosystems() { @Test void testDownloadAndExtractEcosystemFiles() throws Exception { - var wireMockServer = new WireMockServer(); + var wireMockServer = new WireMockServer(options().dynamicPort()); wireMockServer.start(); WireMock.configureFor("localhost", wireMockServer.port()); final String ecosystem = "maven";