Skip to content

Commit

Permalink
Introduce plugin system to deal with provider config and lifecycle
Browse files Browse the repository at this point in the history
Signed-off-by: nscuro <nscuro@protonmail.com>
  • Loading branch information
nscuro committed Jul 27, 2024
1 parent 81013b4 commit 111bb27
Show file tree
Hide file tree
Showing 38 changed files with 1,866 additions and 260 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,12 @@
<version>${lib.json-unit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>minio</artifactId>
<version>${lib.testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>redpanda</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import alpine.event.framework.ChainableEvent;
import alpine.event.framework.Event;
import alpine.event.framework.EventService;
import alpine.model.ConfigProperty;
import alpine.notification.Notification;
import alpine.notification.NotificationLevel;
import org.apache.commons.collections4.MultiValuedMap;
Expand Down Expand Up @@ -64,6 +63,7 @@
import org.dependencytrack.notification.vo.BomConsumedOrProcessed;
import org.dependencytrack.notification.vo.BomProcessingFailed;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.plugin.PluginManager;
import org.dependencytrack.proto.event.v1alpha1.BomUploadedEvent;
import org.dependencytrack.storage.BomUploadStorageProvider;
import org.dependencytrack.util.InternalComponentIdentifier;
Expand Down Expand Up @@ -107,7 +107,6 @@
import static org.dependencytrack.common.MdcKeys.MDC_PROJECT_VERSION;
import static org.dependencytrack.event.kafka.componentmeta.RepoMetaConstants.SUPPORTED_PACKAGE_URLS_FOR_INTEGRITY_CHECK;
import static org.dependencytrack.event.kafka.componentmeta.RepoMetaConstants.TIME_SPAN;
import static org.dependencytrack.model.ConfigPropertyConstants.BOM_UPLOAD_STORAGE_PROVIDER;
import static org.dependencytrack.parser.cyclonedx.util.ModelConverter.convertComponents;
import static org.dependencytrack.parser.cyclonedx.util.ModelConverter.convertDependencyGraph;
import static org.dependencytrack.parser.cyclonedx.util.ModelConverter.convertServices;
Expand Down Expand Up @@ -157,6 +156,7 @@ private Context(final UUID token, final BomUploadedEvent.Project project) {
public BomUploadProcessor() {
this(new KafkaEventDispatcher(), Config.getInstance().getPropertyAsBoolean(ConfigKey.TMP_DELAY_BOM_PROCESSED_NOTIFICATION));
}

BomUploadProcessor(final KafkaEventDispatcher kafkaEventDispatcher, final boolean delayBomProcessedNotification) {
this.kafkaEventDispatcher = kafkaEventDispatcher;
this.delayBomProcessedNotification = delayBomProcessedNotification;
Expand All @@ -166,25 +166,16 @@ public BomUploadProcessor() {
public void process(final ConsumerRecord<UUID, BomUploadedEvent> record) throws ProcessingException {
final BomUploadedEvent event = record.value();

final BomUploadStorageProvider storageProvider;
try (final var qm = new QueryManager()) {
final ConfigProperty storageProviderProperty = qm.getConfigProperty(
BOM_UPLOAD_STORAGE_PROVIDER.getGroupName(),
BOM_UPLOAD_STORAGE_PROVIDER.getPropertyName()
);
final String storageProviderClassName = storageProviderProperty != null
? storageProviderProperty.getPropertyValue()
: BOM_UPLOAD_STORAGE_PROVIDER.getDefaultPropertyValue();
storageProvider = BomUploadStorageProvider.getForClassName(storageProviderClassName);
}
final var storageProviderFactory = PluginManager.getInstance().getFactory(BomUploadStorageProvider.class);

final var ctx = new Context(UUID.fromString(event.getToken()), event.getProject());
try (var ignoredMdcProjectUuid = MDC.putCloseable(MDC_PROJECT_UUID, ctx.project.getUuid().toString());
var ignoredMdcProjectName = MDC.putCloseable(MDC_PROJECT_NAME, ctx.project.getName());
var ignoredMdcProjectVersion = MDC.putCloseable(MDC_PROJECT_VERSION, ctx.project.getVersion());
var ignoredMdcBomUploadToken = MDC.putCloseable(MDC_BOM_UPLOAD_TOKEN, ctx.token.toString())) {
var ignoredMdcBomUploadToken = MDC.putCloseable(MDC_BOM_UPLOAD_TOKEN, ctx.token.toString());
final BomUploadStorageProvider storageProvider = storageProviderFactory.create()) {
processEvent(ctx, storageProvider);
} finally {

try {
storageProvider.deleteBomByToken(ctx.token);
} catch (IOException | RuntimeException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,10 @@

import alpine.model.IConfigProperty;
import alpine.model.IConfigProperty.PropertyType;
import com.github.luben.zstd.Zstd;
import org.apache.commons.lang3.SystemUtils;
import org.dependencytrack.storage.DatabaseBomUploadStorageProvider;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public enum ConfigPropertyConstants {

Expand Down Expand Up @@ -71,9 +68,6 @@ public enum ConfigPropertyConstants {
VULNERABILITY_SOURCE_EPSS_FEEDS_URL("vuln-source", "epss.feeds.url", "https://epss.cyentia.com", PropertyType.URL, "A base URL pointing to the hostname and path of the EPSS feeds", ConfigPropertyAccessMode.READ_WRITE),
ACCEPT_ARTIFACT_CYCLONEDX("artifact", "cyclonedx.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable the systems ability to accept CycloneDX uploads", ConfigPropertyAccessMode.READ_WRITE),
BOM_VALIDATION_ENABLED("artifact", "bom.validation.enabled", "true", PropertyType.BOOLEAN, "Flag to control bom validation", ConfigPropertyAccessMode.READ_WRITE),
BOM_UPLOAD_STORAGE_PROVIDER("artifact", "bom.upload.storage.provider", DatabaseBomUploadStorageProvider.class.getName(), PropertyType.STRING, "Class of the BOM upload storage provider", ConfigPropertyAccessMode.READ_WRITE),
BOM_UPLOAD_STORAGE_COMPRESSION_LEVEL("artifact", "bom.upload.storage.compression.level", String.valueOf(Zstd.defaultCompressionLevel()), PropertyType.INTEGER, "Compression level to use for storage of uploaded BOMs", ConfigPropertyAccessMode.READ_WRITE),
BOM_UPLOAD_STORAGE_RETENTION_MS("artifact", "bom.upload.storage.retention.ms", String.valueOf(TimeUnit.HOURS.toMillis(1)), PropertyType.INTEGER, "Maximum storage retention duration for uploaded BOMs in milliseconds", ConfigPropertyAccessMode.READ_WRITE),
FORTIFY_SSC_ENABLED("integrations", "fortify.ssc.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable Fortify SSC integration", ConfigPropertyAccessMode.READ_WRITE),
FORTIFY_SSC_SYNC_CADENCE("integrations", "fortify.ssc.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE),
FORTIFY_SSC_URL("integrations", "fortify.ssc.url", null, PropertyType.URL, "Base URL to Fortify SSC", ConfigPropertyAccessMode.READ_WRITE),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public interface BomDao {
@SqlUpdate("""
INSERT INTO "BOM_UPLOAD" ("TOKEN", "UPLOADED_AT", "BOM")
VALUES (:token, NOW(), :bomBytes)
ON CONFLICT ("TOKEN")
DO UPDATE
SET "UPLOADED_AT" = NOW()
, "BOM" = :bomBytes
""")
void createUpload(@Bind UUID token, @Bind byte[] bomBytes);

Expand Down
106 changes: 106 additions & 0 deletions src/main/java/org/dependencytrack/plugin/ConfigRegistry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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;

import alpine.Config;

import java.util.Optional;

import static java.util.Objects.requireNonNull;
import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle;

/**
* A read-only registry for accessing application configuration.
* <p>
* The registry enforces namespacing of property names,
* to prevent {@link Provider}s from accessing values
* belonging to the core application, or other plugins.
* <p>
* Namespacing is based on the plugin's, and the provider's name.
* Provider {@code foo} of plugin {@code bar} can access:
* <ul>
* <li>Runtime properties with {@code groupName} of {@code plugin} and {@code propertyName} starting with {@code bar.provider.foo}</li>
* <li>Deployment properties prefix {@code bar.provider.foo}</li>
* </ul>
* <p>
* Runtime properties are sourced from the {@code CONFIGPROPERTY} database table.
* Deployment properties are sourced from environment variables, and the {@code application.properties} file.
*
* @since 5.6.0
*/
public class ConfigRegistry {

private final String pluginName;
private final String providerName;

public ConfigRegistry(final String pluginName, final String providerName) {
this.pluginName = requireNonNull(pluginName);
this.providerName = requireNonNull(providerName);
}

/**
* @param propertyName Name of the runtime property.
* @return An {@link Optional} holding the property value, or {@link Optional#empty()}.
*/
public Optional<String> getRuntimeProperty(final String propertyName) {
final String namespacedPropertyName = "%s.provider.%s.%s".formatted(pluginName, providerName, propertyName);

return withJdbiHandle(handle -> handle.createQuery("""
SELECT "PROPERTYVALUE"
FROM "CONFIGPROPERTY"
WHERE "GROUPNAME" = 'plugin'
AND "PROPERTYNAME" = :propertyName
""")
.bind("propertyName", namespacedPropertyName)
.mapTo(String.class)
.findOne());
}

/**
* @param propertyName Name of the deployment property.
* @return An {@link Optional} holding the property value, or {@link Optional#empty()}.
*/
public Optional<String> getDeploymentProperty(final String propertyName) {
final var key = new DeploymentConfigKey(pluginName, providerName, propertyName);
return Optional.ofNullable(Config.getInstance().getProperty(key));
}

record DeploymentConfigKey(String pluginName, String providerName, String name) implements Config.Key {

DeploymentConfigKey(final String pluginName, final String name) {
this(pluginName, null, name);
}

@Override
public String getPropertyName() {
if (providerName == null) {
return "%s.%s".formatted(pluginName, name);
}

return "%s.provider.%s.%s".formatted(pluginName, providerName, name);
}

@Override
public Object getDefaultValue() {
return null;
}

}

}
46 changes: 46 additions & 0 deletions src/main/java/org/dependencytrack/plugin/Plugin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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;

/**
* @since 5.6.0
*/
public interface Plugin {

/**
* @return The name of the plugin. Can contain lowercase letters, numbers, and periods.
*/
String name();

/**
* @return Whether this plugin is required. Required plugins must have at least one active {@link Provider}.
*/
boolean required();

/**
* @return Class of the {@link ProviderFactory}
*/
Class<? extends ProviderFactory<? extends Provider>> providerFactoryClass();

/**
* @return Class of the {@link Provider}
*/
Class<? extends Provider> providerClass();

}
47 changes: 47 additions & 0 deletions src/main/java/org/dependencytrack/plugin/PluginInitializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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;

import alpine.common.logging.Logger;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
* @since 5.6.0
*/
public class PluginInitializer implements ServletContextListener {

private static final Logger LOGGER = Logger.getLogger(PluginInitializer.class);

private final PluginManager pluginManager = PluginManager.getInstance();

@Override
public void contextInitialized(final ServletContextEvent event) {
LOGGER.info("Loading plugins");
pluginManager.loadPlugins();
}

@Override
public void contextDestroyed(final ServletContextEvent event) {
LOGGER.info("Unloading plugins");
pluginManager.unloadPlugins();
}

}
Loading

0 comments on commit 111bb27

Please sign in to comment.