diff --git a/apiserver/src/main/java/org/dependencytrack/common/ConfigKey.java b/apiserver/src/main/java/org/dependencytrack/common/ConfigKey.java
index 58440d66a6..a11bbdbdac 100644
--- a/apiserver/src/main/java/org/dependencytrack/common/ConfigKey.java
+++ b/apiserver/src/main/java/org/dependencytrack/common/ConfigKey.java
@@ -52,17 +52,15 @@ public enum ConfigKey implements Config.Key {
VULNERABILITY_POLICY_S3_BUCKET_NAME("vulnerability.policy.s3.bucket.name", null),
VULNERABILITY_POLICY_S3_BUNDLE_NAME("vulnerability.policy.s3.bundle.name", null),
VULNERABILITY_POLICY_S3_REGION("vulnerability.policy.s3.region", null),
- DATABASE_MIGRATION_URL("database.migration.url", null),
- DATABASE_MIGRATION_USERNAME("database.migration.username", null),
- DATABASE_MIGRATION_PASSWORD("database.migration.password", null),
- DATABASE_RUN_MIGRATIONS("database.run.migrations", true),
- DATABASE_RUN_MIGRATIONS_ONLY("database.run.migrations.only", false),
INIT_TASKS_ENABLED("init.tasks.enabled", true),
+ INIT_TASKS_DATABASE_URL("init.tasks.database.url", null),
+ INIT_TASKS_DATABASE_USERNAME("init.tasks.database.username", null),
+ INIT_TASKS_DATABASE_PASSWORD("init.tasks.database.password", null),
INIT_AND_EXIT("init.and.exit", false),
DEV_SERVICES_ENABLED("dev.services.enabled", false),
DEV_SERVICES_IMAGE_FRONTEND("dev.services.image.frontend", "ghcr.io/dependencytrack/hyades-frontend:snapshot"),
- DEV_SERVICES_IMAGE_KAFKA("dev.services.image.kafka", "apache/kafka-native:3.9.0"),
+ DEV_SERVICES_IMAGE_KAFKA("dev.services.image.kafka", "apache/kafka-native:3.9.1"),
DEV_SERVICES_IMAGE_POSTGRES("dev.services.image.postgres", "postgres:13-alpine"),
DEV_SERVICES_PORT_FRONTEND("dev.services.port.frontend", 8081),
DEV_SERVICES_PORT_KAFKA("dev.services.port.kafka", 9092),
diff --git a/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsApplicationEventListener.java b/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsApplicationEventListener.java
index b580191d0a..cc8fa39014 100644
--- a/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsApplicationEventListener.java
+++ b/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsApplicationEventListener.java
@@ -23,12 +23,9 @@
import org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProvider;
import org.glassfish.jersey.micrometer.server.MetricsApplicationEventListener;
-import jakarta.ws.rs.ext.Provider;
-
/**
* @since 5.5.0
*/
-@Provider
public class JerseyMetricsApplicationEventListener extends MetricsApplicationEventListener {
public JerseyMetricsApplicationEventListener() {
diff --git a/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsFeature.java b/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsFeature.java
new file mode 100644
index 0000000000..4843a94c03
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/filters/JerseyMetricsFeature.java
@@ -0,0 +1,45 @@
+/*
+ * 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.filters;
+
+import alpine.Config;
+
+import jakarta.ws.rs.core.Feature;
+import jakarta.ws.rs.core.FeatureContext;
+import jakarta.ws.rs.ext.Provider;
+
+import static alpine.Config.AlpineKey.METRICS_ENABLED;
+
+/**
+ * @since 5.6.0
+ */
+@Provider
+public class JerseyMetricsFeature implements Feature {
+
+ @Override
+ public boolean configure(final FeatureContext context) {
+ if (Config.getInstance().getPropertyAsBoolean(METRICS_ENABLED)) {
+ context.register(JerseyMetricsApplicationEventListener.class);
+ return true;
+ }
+
+ return false;
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/health/HealthCheckInitializer.java b/apiserver/src/main/java/org/dependencytrack/health/HealthCheckInitializer.java
index 331b604bee..acb9fb7724 100644
--- a/apiserver/src/main/java/org/dependencytrack/health/HealthCheckInitializer.java
+++ b/apiserver/src/main/java/org/dependencytrack/health/HealthCheckInitializer.java
@@ -18,11 +18,9 @@
*/
package org.dependencytrack.health;
-import alpine.Config;
import alpine.common.logging.Logger;
import alpine.server.health.HealthCheckRegistry;
import alpine.server.health.checks.DatabaseHealthCheck;
-import org.dependencytrack.common.ConfigKey;
import org.dependencytrack.event.kafka.processor.ProcessorsHealthCheck;
import jakarta.servlet.ServletContextEvent;
@@ -34,12 +32,6 @@ public class HealthCheckInitializer implements ServletContextListener {
@Override
public void contextInitialized(final ServletContextEvent event) {
- if (Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) {
- LOGGER.debug("Not registering health checks because %s is enabled"
- .formatted(ConfigKey.INIT_AND_EXIT.getPropertyName()));
- return;
- }
-
LOGGER.info("Registering health checks");
HealthCheckRegistry.getInstance().register("database", new DatabaseHealthCheck());
HealthCheckRegistry.getInstance().register("kafka-processors", new ProcessorsHealthCheck());
diff --git a/apiserver/src/main/java/org/dependencytrack/init/InitTask.java b/apiserver/src/main/java/org/dependencytrack/init/InitTask.java
new file mode 100644
index 0000000000..cea6327f2c
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/init/InitTask.java
@@ -0,0 +1,51 @@
+/*
+ * 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.init;
+
+/**
+ * A task to be run on application startup.
+ *
+ * @since 5.6.0
+ */
+public interface InitTask {
+
+ int PRIORITY_HIGHEST = 100;
+ int PRIORITY_LOWEST = 0;
+
+ /**
+ * @return Priority of the task.
+ * @see #PRIORITY_HIGHEST
+ * @see #PRIORITY_LOWEST
+ */
+ int priority();
+
+ /**
+ * @return Name of the task. Must be globally unique.
+ */
+ String name();
+
+ /**
+ * Execute the task.
+ *
+ * @param ctx Context in which the task is executed.
+ * @throws Exception When the task execution failed.
+ */
+ void execute(InitTaskContext ctx) throws Exception;
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/init/InitTaskContext.java b/apiserver/src/main/java/org/dependencytrack/init/InitTaskContext.java
new file mode 100644
index 0000000000..be01f297fe
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/init/InitTaskContext.java
@@ -0,0 +1,36 @@
+/*
+ * 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.init;
+
+import alpine.Config;
+
+import javax.sql.DataSource;
+
+/**
+ * Context available to {@link InitTask}s.
+ *
+ * TODO: Introduce a tiny abstraction over {@link Config} such that
+ * Alpine specifics don't bleed through to {@link InitTask}s.
+ *
+ * @param config A {@link Config} instance to read application configuration.
+ * @param dataSource A {@link DataSource} which may be used for database interactions.
+ * @since 5.6.0
+ */
+public record InitTaskContext(Config config, DataSource dataSource) {
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/init/InitTaskExecutor.java b/apiserver/src/main/java/org/dependencytrack/init/InitTaskExecutor.java
new file mode 100644
index 0000000000..2ee6c5837a
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/init/InitTaskExecutor.java
@@ -0,0 +1,180 @@
+/*
+ * 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.init;
+
+import alpine.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jakarta.servlet.ServletContextListener;
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+import static java.util.Comparator.comparing;
+import static java.util.Comparator.reverseOrder;
+import static java.util.Objects.requireNonNull;
+import static org.dependencytrack.util.ConfigUtil.getPassThroughProperties;
+
+/**
+ * @since 5.6.0
+ */
+final class InitTaskExecutor implements ServletContextListener {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(InitTaskExecutor.class);
+ private static final long ADVISORY_LOCK_KEY = "dependency-track-init-tasks".hashCode();
+
+ private final Config config;
+ private final DataSource dataSource;
+ private final List tasks;
+
+ InitTaskExecutor(final Config config, final DataSource dataSource) {
+ this(config, dataSource, loadInitTasks());
+ }
+
+ InitTaskExecutor(final Config config, final DataSource dataSource, final List tasks) {
+ this.config = requireNonNull(config, "config must not be null");
+ this.dataSource = requireNonNull(dataSource, "dataSource must not be null");
+ this.tasks = requireNonNull(tasks, "tasks must not be null");
+ }
+
+ public void execute() {
+ final List orderedTasks = this.tasks.stream()
+ .peek(requireUniqueName())
+ .peek(requireValidPriority())
+ .filter(isTaskEnabled())
+ .sorted(comparing(InitTask::priority, reverseOrder())
+ .thenComparing(InitTask::name))
+ .toList();
+
+ final long startTimeNanos = System.nanoTime();
+
+ // We're using session-level advisory locks here,
+ // which won't work when using PgBouncer in "transaction" mode.
+ // We can't use transaction-level locking because that would
+ // block some DDL statements executed by database migrations,
+ // such as "CREATE INDEX CONCURRENTLY".
+ //
+ // This GitLab issue describes the problem well:
+ // https://gitlab.com/gitlab-com/support/support-training/-/issues/3823#locks-block-a-gitlab-database-migration
+ //
+ // The intended workaround is to use a separate set of connection
+ // details specifically for init tasks, which bypasses PgBouncer.
+ try (final Connection connection = dataSource.getConnection();
+ final PreparedStatement lockStatement = connection.prepareStatement("""
+ SELECT PG_ADVISORY_LOCK(?)
+ """);
+ final PreparedStatement unlockStatement = connection.prepareStatement("""
+ SELECT PG_ADVISORY_UNLOCK(?)
+ """)) {
+ LOGGER.debug("Trying to acquire lock {}", ADVISORY_LOCK_KEY);
+ lockStatement.setLong(1, ADVISORY_LOCK_KEY);
+ lockStatement.execute();
+ LOGGER.debug(
+ "Lock {} acquired after {}ms",
+ ADVISORY_LOCK_KEY,
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos));
+
+ final var taskContext = new InitTaskContext(config, dataSource);
+
+ try {
+ long taskStartTimeNanos;
+ for (final InitTask task : orderedTasks) {
+ taskStartTimeNanos = System.nanoTime();
+ LOGGER.info("Executing init task {}", task.name());
+ try {
+ task.execute(taskContext);
+ LOGGER.info(
+ "Completed init task {} in {}ms",
+ task.name(),
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - taskStartTimeNanos));
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to execute init task " + task.name(), e);
+ }
+ }
+ } finally {
+ LOGGER.debug("Releasing lock {}", ADVISORY_LOCK_KEY);
+ unlockStatement.setLong(1, ADVISORY_LOCK_KEY);
+ final ResultSet rs = unlockStatement.executeQuery();
+ if (!rs.next() || !rs.getBoolean(1)) {
+ LOGGER.warn("""
+ Lock {} could not be released, likely because a connection pooler \
+ in "transaction" mode is being used. Ensure that a direct database connection \
+ is provided when executing init tasks.""", ADVISORY_LOCK_KEY);
+ }
+ }
+ } catch (SQLException e) {
+ throw new IllegalStateException("Failed to acquire or release lock " + ADVISORY_LOCK_KEY, e);
+ }
+
+ LOGGER.info(
+ "All init tasks completed in {}ms",
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos));
+ }
+
+ private static List loadInitTasks() {
+ return ServiceLoader.load(InitTask.class).stream()
+ .map(ServiceLoader.Provider::get)
+ .toList();
+ }
+
+ private Consumer requireUniqueName() {
+ final var seenTaskClassesByTaskName =
+ new HashMap>(this.tasks.size());
+
+ return task -> {
+ final Class extends InitTask> previousClass =
+ seenTaskClassesByTaskName.put(task.name(), task.getClass());
+ if (previousClass != null) {
+ throw new IllegalStateException(
+ "Duplicate task name %s: Registered by %s and %s".formatted(
+ task.name(), previousClass.getName(), task.getClass().getName()));
+ }
+ };
+ }
+
+ private Consumer requireValidPriority() {
+ return task -> {
+ if (task.priority() < InitTask.PRIORITY_LOWEST
+ || task.priority() > InitTask.PRIORITY_HIGHEST) {
+ throw new IllegalStateException(
+ "Invalid priority of task %s: Must be within [%d..%d] but is %d".formatted(
+ task.name(), InitTask.PRIORITY_LOWEST, InitTask.PRIORITY_HIGHEST, task.priority()));
+ }
+ };
+ }
+
+ private Predicate isTaskEnabled() {
+ return task -> {
+ final String propertyPrefix = "init.task." + task.name();
+ final Map properties = getPassThroughProperties(config, propertyPrefix);
+ return !"false".equals(properties.get(propertyPrefix + ".enabled"));
+ };
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/init/InitTaskServletContextListener.java b/apiserver/src/main/java/org/dependencytrack/init/InitTaskServletContextListener.java
new file mode 100644
index 0000000000..5686412dfc
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/init/InitTaskServletContextListener.java
@@ -0,0 +1,99 @@
+/*
+ * 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.init;
+
+import alpine.Config;
+import org.dependencytrack.common.ConfigKey;
+import org.postgresql.ds.PGSimpleDataSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+import javax.sql.DataSource;
+
+import static alpine.Config.AlpineKey.DATABASE_PASSWORD;
+import static alpine.Config.AlpineKey.DATABASE_URL;
+import static alpine.Config.AlpineKey.DATABASE_USERNAME;
+import static java.util.Objects.requireNonNullElseGet;
+import static org.dependencytrack.common.ConfigKey.INIT_TASKS_DATABASE_PASSWORD;
+import static org.dependencytrack.common.ConfigKey.INIT_TASKS_DATABASE_URL;
+import static org.dependencytrack.common.ConfigKey.INIT_TASKS_DATABASE_USERNAME;
+
+/**
+ * @since 5.6.0
+ */
+public final class InitTaskServletContextListener implements ServletContextListener {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(InitTaskServletContextListener.class);
+
+ private final Config config;
+
+ @SuppressWarnings("unused")
+ public InitTaskServletContextListener() {
+ this(Config.getInstance());
+ }
+
+ InitTaskServletContextListener(final Config config) {
+ this.config = config;
+ }
+
+ @Override
+ public void contextInitialized(final ServletContextEvent event) {
+ if (!config.getPropertyAsBoolean(ConfigKey.INIT_TASKS_ENABLED)) {
+ LOGGER.debug(
+ "Not executing init tasks because {} is disabled",
+ ConfigKey.INIT_TASKS_ENABLED.getPropertyName());
+ return;
+ }
+
+ final DataSource dataSource;
+ try {
+ dataSource = createDataSource(config);
+ } catch (RuntimeException e) {
+ throw new IllegalStateException("Failed to create data source", e);
+ }
+
+ final var taskExecutor = new InitTaskExecutor(config, dataSource);
+ taskExecutor.execute();
+
+ if (config.getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) {
+ LOGGER.info(
+ "Exiting because {} is enabled",
+ ConfigKey.INIT_AND_EXIT.getPropertyName());
+ System.exit(0);
+ }
+ }
+
+ private DataSource createDataSource(final Config config) {
+ final var dataSource = new PGSimpleDataSource();
+ dataSource.setUrl(requireNonNullElseGet(
+ config.getProperty(INIT_TASKS_DATABASE_URL),
+ () -> config.getProperty(DATABASE_URL)));
+ dataSource.setUser(requireNonNullElseGet(
+ config.getProperty(INIT_TASKS_DATABASE_USERNAME),
+ () -> config.getProperty(DATABASE_USERNAME)));
+ dataSource.setPassword(requireNonNullElseGet(
+ config.getProperty(INIT_TASKS_DATABASE_PASSWORD),
+ () -> config.getPropertyOrFile(DATABASE_PASSWORD)));
+
+ return dataSource;
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/model/DefaultRepository.java b/apiserver/src/main/java/org/dependencytrack/model/DefaultRepository.java
new file mode 100644
index 0000000000..6a7df63fbb
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/model/DefaultRepository.java
@@ -0,0 +1,72 @@
+/*
+ * 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.model;
+
+/**
+ * @since 5.6.0
+ */
+public enum DefaultRepository {
+
+ CPAN_PUBLIC_REGISTRY(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", 1),
+ GEM_RUBYGEMS(RepositoryType.GEM, "rubygems.org", "https://rubygems.org/", 1),
+ HEX_HEX_PM(RepositoryType.HEX, "hex.pm", "https://hex.pm/", 1),
+ MAVEN_CENTRAL(RepositoryType.MAVEN, "central", "https://repo1.maven.org/maven2/", 1),
+ MAVEN_ATLASSIAN_PUBLIC(RepositoryType.MAVEN, "atlassian-public", "https://packages.atlassian.com/content/repositories/atlassian-public/", 2),
+ MAVEN_JBOSS_RELEASES(RepositoryType.MAVEN, "jboss-releases", "https://repository.jboss.org/nexus/content/repositories/releases/", 3),
+ MAVEN_CLOJARS(RepositoryType.MAVEN, "clojars", "https://repo.clojars.org/", 4),
+ MAVEN_GOOGLE_ANDROID(RepositoryType.MAVEN, "google-android", "https://maven.google.com/", 5),
+ NPM_PUBLIC_REGISTRY(RepositoryType.NPM, "npm-public-registry", "https://registry.npmjs.org/", 1),
+ PYPI_PYPI_ORG(RepositoryType.PYPI, "pypi.org", "https://pypi.org/", 1),
+ NUGET_GALLERY(RepositoryType.NUGET, "nuget-gallery", "https://api.nuget.org/", 1),
+ COMPOSER_PACKAGIST(RepositoryType.COMPOSER, "packagist", "https://repo.packagist.org/", 1),
+ CARGO_CRATES_IO(RepositoryType.CARGO, "crates.io", "https://crates.io", 1),
+ GO_PROXY_GOLANG_ORG(RepositoryType.GO_MODULES, "proxy.golang.org", "https://proxy.golang.org", 1),
+ GITHUB(RepositoryType.GITHUB, "github", "https://github.com", 1),
+ HACKAGE(RepositoryType.HACKAGE, "hackage.haskell", "https://hackage.haskell.org/", 1),
+ NIXPKGS_NIXOS_ORG(RepositoryType.NIXPKGS, "nixos.org", "https://channels.nixos.org/nixpkgs-unstable/packages.json.br", 1);
+
+ private final RepositoryType type;
+ private final String identifier;
+ private final String url;
+ private final int resolutionOrder;
+
+ DefaultRepository(final RepositoryType type, final String identifier, final String url, final int resolutionOrder) {
+ this.type = type;
+ this.identifier = identifier;
+ this.url = url;
+ this.resolutionOrder = resolutionOrder;
+ }
+
+ public RepositoryType getType() {
+ return type;
+ }
+
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public int getResolutionOrder() {
+ return resolutionOrder;
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/model/DependencyMetrics.java b/apiserver/src/main/java/org/dependencytrack/model/DependencyMetrics.java
index a775425a51..694842adea 100644
--- a/apiserver/src/main/java/org/dependencytrack/model/DependencyMetrics.java
+++ b/apiserver/src/main/java/org/dependencytrack/model/DependencyMetrics.java
@@ -21,7 +21,6 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
-import org.jdbi.v3.core.mapper.reflect.ColumnName;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
@@ -39,80 +38,37 @@ public class DependencyMetrics implements Serializable {
private static final long serialVersionUID = 5231823328085979791L;
@JsonIgnore
- @NotNull
private long projectId;
@JsonIgnore
- @NotNull
private long componentId;
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private int critical;
-
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private int high;
-
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private int medium;
-
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private int low;
-
- private Integer unassigned;
-
+ private int unassigned;
private int vulnerabilities;
-
private int suppressed;
-
- private Integer findingsTotal;
-
- private Integer findingsAudited;
-
- private Integer findingsUnaudited;
-
+ private int findingsTotal;
+ private int findingsAudited;
+ private int findingsUnaudited;
private double inheritedRiskScore;
-
- private Integer policyViolationsFail;
-
- private Integer policyViolationsWarn;
-
- private Integer policyViolationsInfo;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsTotal;
-
- // New column, must allow nulls on existing databases)
- private Integer policyViolationsAudited;
-
- // New column, must allow nulls on existing databases)
- private Integer policyViolationsUnaudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsSecurityTotal;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsSecurityAudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsSecurityUnaudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsLicenseTotal;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsLicenseAudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsLicenseUnaudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsOperationalTotal;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsOperationalAudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsOperationalUnaudited;
+ private int policyViolationsFail;
+ private int policyViolationsWarn;
+ private int policyViolationsInfo;
+ private int policyViolationsTotal;
+ private int policyViolationsAudited;
+ private int policyViolationsUnaudited;
+ private int policyViolationsSecurityTotal;
+ private int policyViolationsSecurityAudited;
+ private int policyViolationsSecurityUnaudited;
+ private int policyViolationsLicenseTotal;
+ private int policyViolationsLicenseAudited;
+ private int policyViolationsLicenseUnaudited;
+ private int policyViolationsOperationalTotal;
+ private int policyViolationsOperationalAudited;
+ private int policyViolationsOperationalUnaudited;
@NotNull
@Schema(type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED, description = "UNIX epoch timestamp in milliseconds")
@@ -171,7 +127,7 @@ public void setLow(int low) {
}
public int getUnassigned() {
- return unassigned != null ? unassigned : 0;
+ return unassigned;
}
public void setUnassigned(int unassigned) {
@@ -195,7 +151,7 @@ public void setSuppressed(int suppressed) {
}
public int getFindingsTotal() {
- return findingsTotal != null ? findingsTotal : 0;
+ return findingsTotal;
}
public void setFindingsTotal(int findingsTotal) {
@@ -203,7 +159,7 @@ public void setFindingsTotal(int findingsTotal) {
}
public int getFindingsAudited() {
- return findingsAudited != null ? findingsAudited : 0;
+ return findingsAudited;
}
public void setFindingsAudited(int findingsAudited) {
@@ -211,14 +167,13 @@ public void setFindingsAudited(int findingsAudited) {
}
public int getFindingsUnaudited() {
- return findingsUnaudited != null ? findingsUnaudited : 0;
+ return findingsUnaudited;
}
public void setFindingsUnaudited(int findingsUnaudited) {
this.findingsUnaudited = findingsUnaudited;
}
- @ColumnName("RISKSCORE")
public double getInheritedRiskScore() {
return inheritedRiskScore;
}
@@ -228,7 +183,7 @@ public void setInheritedRiskScore(double inheritedRiskScore) {
}
public int getPolicyViolationsFail() {
- return policyViolationsFail != null ? policyViolationsFail : 0;
+ return policyViolationsFail;
}
public void setPolicyViolationsFail(int policyViolationsFail) {
@@ -236,7 +191,7 @@ public void setPolicyViolationsFail(int policyViolationsFail) {
}
public int getPolicyViolationsWarn() {
- return policyViolationsWarn != null ? policyViolationsWarn : 0;
+ return policyViolationsWarn;
}
public void setPolicyViolationsWarn(int policyViolationsWarn) {
@@ -244,7 +199,7 @@ public void setPolicyViolationsWarn(int policyViolationsWarn) {
}
public int getPolicyViolationsInfo() {
- return policyViolationsInfo != null ? policyViolationsInfo : 0;
+ return policyViolationsInfo;
}
public void setPolicyViolationsInfo(int policyViolationsInfo) {
@@ -252,7 +207,7 @@ public void setPolicyViolationsInfo(int policyViolationsInfo) {
}
public int getPolicyViolationsTotal() {
- return policyViolationsTotal != null ? policyViolationsTotal : 0;
+ return policyViolationsTotal;
}
public void setPolicyViolationsTotal(int policyViolationsTotal) {
@@ -260,7 +215,7 @@ public void setPolicyViolationsTotal(int policyViolationsTotal) {
}
public int getPolicyViolationsAudited() {
- return policyViolationsAudited != null ? policyViolationsAudited : 0;
+ return policyViolationsAudited;
}
public void setPolicyViolationsAudited(int policyViolationsAudited) {
@@ -268,7 +223,7 @@ public void setPolicyViolationsAudited(int policyViolationsAudited) {
}
public int getPolicyViolationsUnaudited() {
- return policyViolationsUnaudited != null ? policyViolationsUnaudited : 0;
+ return policyViolationsUnaudited;
}
public void setPolicyViolationsUnaudited(int policyViolationsUnaudited) {
@@ -276,7 +231,7 @@ public void setPolicyViolationsUnaudited(int policyViolationsUnaudited) {
}
public int getPolicyViolationsSecurityTotal() {
- return policyViolationsSecurityTotal != null ? policyViolationsSecurityTotal : 0;
+ return policyViolationsSecurityTotal;
}
public void setPolicyViolationsSecurityTotal(int policyViolationsSecurityTotal) {
@@ -284,7 +239,7 @@ public void setPolicyViolationsSecurityTotal(int policyViolationsSecurityTotal)
}
public int getPolicyViolationsSecurityAudited() {
- return policyViolationsSecurityAudited != null ? policyViolationsSecurityAudited : 0;
+ return policyViolationsSecurityAudited;
}
public void setPolicyViolationsSecurityAudited(int policyViolationsSecurityAudited) {
@@ -292,7 +247,7 @@ public void setPolicyViolationsSecurityAudited(int policyViolationsSecurityAudit
}
public int getPolicyViolationsSecurityUnaudited() {
- return policyViolationsSecurityUnaudited != null ? policyViolationsSecurityUnaudited : 0;
+ return policyViolationsSecurityUnaudited;
}
public void setPolicyViolationsSecurityUnaudited(int policyViolationsSecurityUnaudited) {
@@ -300,7 +255,7 @@ public void setPolicyViolationsSecurityUnaudited(int policyViolationsSecurityUna
}
public int getPolicyViolationsLicenseTotal() {
- return policyViolationsLicenseTotal != null ? policyViolationsLicenseTotal : 0;
+ return policyViolationsLicenseTotal;
}
public void setPolicyViolationsLicenseTotal(int policyViolationsLicenseTotal) {
@@ -308,7 +263,7 @@ public void setPolicyViolationsLicenseTotal(int policyViolationsLicenseTotal) {
}
public int getPolicyViolationsLicenseAudited() {
- return policyViolationsLicenseAudited != null ? policyViolationsLicenseAudited : 0;
+ return policyViolationsLicenseAudited;
}
public void setPolicyViolationsLicenseAudited(int policyViolationsLicenseAudited) {
@@ -316,7 +271,7 @@ public void setPolicyViolationsLicenseAudited(int policyViolationsLicenseAudited
}
public int getPolicyViolationsLicenseUnaudited() {
- return policyViolationsLicenseUnaudited != null ? policyViolationsLicenseUnaudited : 0;
+ return policyViolationsLicenseUnaudited;
}
public void setPolicyViolationsLicenseUnaudited(int policyViolationsLicenseUnaudited) {
@@ -324,7 +279,7 @@ public void setPolicyViolationsLicenseUnaudited(int policyViolationsLicenseUnaud
}
public int getPolicyViolationsOperationalTotal() {
- return policyViolationsOperationalTotal != null ? policyViolationsOperationalTotal : 0;
+ return policyViolationsOperationalTotal;
}
public void setPolicyViolationsOperationalTotal(int policyViolationsOperationalTotal) {
@@ -332,7 +287,7 @@ public void setPolicyViolationsOperationalTotal(int policyViolationsOperationalT
}
public int getPolicyViolationsOperationalAudited() {
- return policyViolationsOperationalAudited != null ? policyViolationsOperationalAudited : 0;
+ return policyViolationsOperationalAudited;
}
public void setPolicyViolationsOperationalAudited(int policyViolationsOperationalAudited) {
@@ -340,7 +295,7 @@ public void setPolicyViolationsOperationalAudited(int policyViolationsOperationa
}
public int getPolicyViolationsOperationalUnaudited() {
- return policyViolationsOperationalUnaudited != null ? policyViolationsOperationalUnaudited : 0;
+ return policyViolationsOperationalUnaudited;
}
public void setPolicyViolationsOperationalUnaudited(int policyViolationsOperationalUnaudited) {
diff --git a/apiserver/src/main/java/org/dependencytrack/model/PortfolioMetrics.java b/apiserver/src/main/java/org/dependencytrack/model/PortfolioMetrics.java
index da6ebb857d..bf254e5bbc 100644
--- a/apiserver/src/main/java/org/dependencytrack/model/PortfolioMetrics.java
+++ b/apiserver/src/main/java/org/dependencytrack/model/PortfolioMetrics.java
@@ -18,12 +18,10 @@
*/
package org.dependencytrack.model;
-import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotNull;
-import org.jdbi.v3.core.mapper.reflect.ColumnName;
+import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
@@ -39,81 +37,36 @@ public class PortfolioMetrics implements Serializable {
private static final long serialVersionUID = -7690624184866776922L;
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private int critical;
-
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private int high;
-
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private int medium;
-
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private int low;
-
- private Integer unassigned;
-
+ private int unassigned;
private int vulnerabilities;
-
private int projects;
-
private int vulnerableProjects;
-
private int components;
-
private int vulnerableComponents;
-
private int suppressed;
-
- private Integer findingsTotal;
-
- private Integer findingsAudited;
-
- private Integer findingsUnaudited;
-
+ private int findingsTotal;
+ private int findingsAudited;
+ private int findingsUnaudited;
private double inheritedRiskScore;
-
- private Integer policyViolationsFail;
-
- private Integer policyViolationsWarn;
-
- private Integer policyViolationsInfo;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsTotal;
-
- // New column, must allow nulls on existing databases)
- private Integer policyViolationsAudited;
-
- // New column, must allow nulls on existing databases)
- private Integer policyViolationsUnaudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsSecurityTotal;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsSecurityAudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsSecurityUnaudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsLicenseTotal;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsLicenseAudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsLicenseUnaudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsOperationalTotal;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsOperationalAudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsOperationalUnaudited;
+ private int policyViolationsFail;
+ private int policyViolationsWarn;
+ private int policyViolationsInfo;
+ private int policyViolationsTotal;
+ private int policyViolationsAudited;
+ private int policyViolationsUnaudited;
+ private int policyViolationsSecurityTotal;
+ private int policyViolationsSecurityAudited;
+ private int policyViolationsSecurityUnaudited;
+ private int policyViolationsLicenseTotal;
+ private int policyViolationsLicenseAudited;
+ private int policyViolationsLicenseUnaudited;
+ private int policyViolationsOperationalTotal;
+ private int policyViolationsOperationalAudited;
+ private int policyViolationsOperationalUnaudited;
@NotNull
@Schema(type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED, description = "UNIX epoch timestamp in milliseconds")
@@ -156,7 +109,7 @@ public void setLow(int low) {
}
public int getUnassigned() {
- return unassigned != null ? unassigned : 0;
+ return unassigned;
}
public void setUnassigned(int unassigned) {
@@ -212,7 +165,7 @@ public void setSuppressed(int suppressed) {
}
public int getFindingsTotal() {
- return findingsTotal != null ? findingsTotal : 0;
+ return findingsTotal;
}
public void setFindingsTotal(int findingsTotal) {
@@ -220,7 +173,7 @@ public void setFindingsTotal(int findingsTotal) {
}
public int getFindingsAudited() {
- return findingsAudited != null ? findingsAudited : 0;
+ return findingsAudited;
}
public void setFindingsAudited(int findingsAudited) {
@@ -228,14 +181,13 @@ public void setFindingsAudited(int findingsAudited) {
}
public int getFindingsUnaudited() {
- return findingsUnaudited != null ? findingsUnaudited : 0;
+ return findingsUnaudited;
}
public void setFindingsUnaudited(int findingsUnaudited) {
this.findingsUnaudited = findingsUnaudited;
}
- @ColumnName("RISKSCORE")
public double getInheritedRiskScore() {
return inheritedRiskScore;
}
@@ -245,7 +197,7 @@ public void setInheritedRiskScore(double inheritedRiskScore) {
}
public int getPolicyViolationsFail() {
- return policyViolationsFail != null ? policyViolationsFail : 0;
+ return policyViolationsFail;
}
public void setPolicyViolationsFail(int policyViolationsFail) {
@@ -253,7 +205,7 @@ public void setPolicyViolationsFail(int policyViolationsFail) {
}
public int getPolicyViolationsWarn() {
- return policyViolationsWarn != null ? policyViolationsWarn : 0;
+ return policyViolationsWarn;
}
public void setPolicyViolationsWarn(int policyViolationsWarn) {
@@ -261,7 +213,7 @@ public void setPolicyViolationsWarn(int policyViolationsWarn) {
}
public int getPolicyViolationsInfo() {
- return policyViolationsInfo != null ? policyViolationsInfo : 0;
+ return policyViolationsInfo;
}
public void setPolicyViolationsInfo(int policyViolationsInfo) {
@@ -269,7 +221,7 @@ public void setPolicyViolationsInfo(int policyViolationsInfo) {
}
public int getPolicyViolationsTotal() {
- return policyViolationsTotal != null ? policyViolationsTotal : 0;
+ return policyViolationsTotal;
}
public void setPolicyViolationsTotal(int policyViolationsTotal) {
@@ -277,7 +229,7 @@ public void setPolicyViolationsTotal(int policyViolationsTotal) {
}
public int getPolicyViolationsAudited() {
- return policyViolationsAudited != null ? policyViolationsAudited : 0;
+ return policyViolationsAudited;
}
public void setPolicyViolationsAudited(int policyViolationsAudited) {
@@ -285,7 +237,7 @@ public void setPolicyViolationsAudited(int policyViolationsAudited) {
}
public int getPolicyViolationsUnaudited() {
- return policyViolationsUnaudited != null ? policyViolationsUnaudited : 0;
+ return policyViolationsUnaudited;
}
public void setPolicyViolationsUnaudited(int policyViolationsUnaudited) {
@@ -293,7 +245,7 @@ public void setPolicyViolationsUnaudited(int policyViolationsUnaudited) {
}
public int getPolicyViolationsSecurityTotal() {
- return policyViolationsSecurityTotal != null ? policyViolationsSecurityTotal : 0;
+ return policyViolationsSecurityTotal;
}
public void setPolicyViolationsSecurityTotal(int policyViolationsSecurityTotal) {
@@ -301,7 +253,7 @@ public void setPolicyViolationsSecurityTotal(int policyViolationsSecurityTotal)
}
public int getPolicyViolationsSecurityAudited() {
- return policyViolationsSecurityAudited != null ? policyViolationsSecurityAudited : 0;
+ return policyViolationsSecurityAudited;
}
public void setPolicyViolationsSecurityAudited(int policyViolationsSecurityAudited) {
@@ -309,7 +261,7 @@ public void setPolicyViolationsSecurityAudited(int policyViolationsSecurityAudit
}
public int getPolicyViolationsSecurityUnaudited() {
- return policyViolationsSecurityUnaudited != null ? policyViolationsSecurityUnaudited : 0;
+ return policyViolationsSecurityUnaudited;
}
public void setPolicyViolationsSecurityUnaudited(int policyViolationsSecurityUnaudited) {
@@ -317,7 +269,7 @@ public void setPolicyViolationsSecurityUnaudited(int policyViolationsSecurityUna
}
public int getPolicyViolationsLicenseTotal() {
- return policyViolationsLicenseTotal != null ? policyViolationsLicenseTotal : 0;
+ return policyViolationsLicenseTotal;
}
public void setPolicyViolationsLicenseTotal(int policyViolationsLicenseTotal) {
@@ -325,7 +277,7 @@ public void setPolicyViolationsLicenseTotal(int policyViolationsLicenseTotal) {
}
public int getPolicyViolationsLicenseAudited() {
- return policyViolationsLicenseAudited != null ? policyViolationsLicenseAudited : 0;
+ return policyViolationsLicenseAudited;
}
public void setPolicyViolationsLicenseAudited(int policyViolationsLicenseAudited) {
@@ -333,7 +285,7 @@ public void setPolicyViolationsLicenseAudited(int policyViolationsLicenseAudited
}
public int getPolicyViolationsLicenseUnaudited() {
- return policyViolationsLicenseUnaudited != null ? policyViolationsLicenseUnaudited : 0;
+ return policyViolationsLicenseUnaudited;
}
public void setPolicyViolationsLicenseUnaudited(int policyViolationsLicenseUnaudited) {
@@ -341,7 +293,7 @@ public void setPolicyViolationsLicenseUnaudited(int policyViolationsLicenseUnaud
}
public int getPolicyViolationsOperationalTotal() {
- return policyViolationsOperationalTotal != null ? policyViolationsOperationalTotal : 0;
+ return policyViolationsOperationalTotal;
}
public void setPolicyViolationsOperationalTotal(int policyViolationsOperationalTotal) {
@@ -349,7 +301,7 @@ public void setPolicyViolationsOperationalTotal(int policyViolationsOperationalT
}
public int getPolicyViolationsOperationalAudited() {
- return policyViolationsOperationalAudited != null ? policyViolationsOperationalAudited : 0;
+ return policyViolationsOperationalAudited;
}
public void setPolicyViolationsOperationalAudited(int policyViolationsOperationalAudited) {
@@ -357,7 +309,7 @@ public void setPolicyViolationsOperationalAudited(int policyViolationsOperationa
}
public int getPolicyViolationsOperationalUnaudited() {
- return policyViolationsOperationalUnaudited != null ? policyViolationsOperationalUnaudited : 0;
+ return policyViolationsOperationalUnaudited;
}
public void setPolicyViolationsOperationalUnaudited(int policyViolationsOperationalUnaudited) {
@@ -380,39 +332,4 @@ public void setLastOccurrence(Date lastOccurrence) {
this.lastOccurrence = lastOccurrence;
}
- @JsonIgnore
- public boolean hasChanged(final PortfolioMetrics comparedTo) {
- return comparedTo == null
- || comparedTo.getCritical() != this.critical
- || comparedTo.getHigh() != this.high
- || comparedTo.getMedium() != this.medium
- || comparedTo.getLow() != this.low
- || comparedTo.getUnassigned() != this.unassigned
- || comparedTo.getVulnerabilities() != this.vulnerabilities
- || comparedTo.getInheritedRiskScore() != this.inheritedRiskScore
- || comparedTo.getPolicyViolationsFail() != this.policyViolationsFail
- || comparedTo.getPolicyViolationsWarn() != this.policyViolationsWarn
- || comparedTo.getPolicyViolationsInfo() != this.policyViolationsInfo
- || comparedTo.getPolicyViolationsTotal() != this.policyViolationsTotal
- || comparedTo.getPolicyViolationsAudited() != this.policyViolationsAudited
- || comparedTo.getPolicyViolationsUnaudited() != this.policyViolationsUnaudited
- || comparedTo.getPolicyViolationsSecurityTotal() != this.policyViolationsSecurityTotal
- || comparedTo.getPolicyViolationsSecurityAudited() != this.policyViolationsSecurityAudited
- || comparedTo.getPolicyViolationsSecurityUnaudited() != this.policyViolationsSecurityUnaudited
- || comparedTo.getPolicyViolationsLicenseTotal() != this.policyViolationsLicenseTotal
- || comparedTo.getPolicyViolationsLicenseAudited() != this.policyViolationsLicenseAudited
- || comparedTo.getPolicyViolationsLicenseUnaudited() != this.policyViolationsLicenseUnaudited
- || comparedTo.getPolicyViolationsOperationalTotal() != this.policyViolationsOperationalTotal
- || comparedTo.getPolicyViolationsOperationalAudited() != this.policyViolationsOperationalAudited
- || comparedTo.getPolicyViolationsOperationalUnaudited() != this.policyViolationsOperationalUnaudited
- || comparedTo.getComponents() != this.components
- || comparedTo.getVulnerableComponents() != this.vulnerableComponents
- || comparedTo.getSuppressed() != this.suppressed
- || comparedTo.getFindingsTotal() != this.findingsTotal
- || comparedTo.getFindingsAudited() != this.findingsAudited
- || comparedTo.getFindingsUnaudited() != this.findingsUnaudited
- || comparedTo.getProjects() != this.projects
- || comparedTo.getVulnerableProjects() != this.vulnerableProjects;
- }
-
}
diff --git a/apiserver/src/main/java/org/dependencytrack/model/ProjectMetrics.java b/apiserver/src/main/java/org/dependencytrack/model/ProjectMetrics.java
index fbd9f321b8..0324911fcd 100644
--- a/apiserver/src/main/java/org/dependencytrack/model/ProjectMetrics.java
+++ b/apiserver/src/main/java/org/dependencytrack/model/ProjectMetrics.java
@@ -21,7 +21,6 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
-import org.jdbi.v3.core.mapper.reflect.ColumnName;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
@@ -39,80 +38,36 @@ public class ProjectMetrics implements Serializable {
private static final long serialVersionUID = 8741534340846353210L;
@JsonIgnore
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private long projectId;
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private int critical;
-
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private int high;
-
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private int medium;
-
- @NotNull
private int low;
-
- private Integer unassigned;
-
+ private int unassigned;
private int vulnerabilities;
-
private int vulnerableComponents;
-
private int components;
-
private int suppressed;
-
- private Integer findingsTotal;
-
- private Integer findingsAudited;
-
- private Integer findingsUnaudited;
-
+ private int findingsTotal;
+ private int findingsAudited;
+ private int findingsUnaudited;
private double inheritedRiskScore;
-
- private Integer policyViolationsFail;
-
- private Integer policyViolationsWarn;
-
- private Integer policyViolationsInfo;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsTotal;
-
- // New column, must allow nulls on existing databases)
- private Integer policyViolationsAudited;
-
- // New column, must allow nulls on existing databases)
- private Integer policyViolationsUnaudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsSecurityTotal;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsSecurityAudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsSecurityUnaudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsLicenseTotal;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsLicenseAudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsLicenseUnaudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsOperationalTotal;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsOperationalAudited;
-
- // New column, must allow nulls on existing data bases)
- private Integer policyViolationsOperationalUnaudited;
+ private int policyViolationsFail;
+ private int policyViolationsWarn;
+ private int policyViolationsInfo;
+ private int policyViolationsTotal;
+ private int policyViolationsAudited;
+ private int policyViolationsUnaudited;
+ private int policyViolationsSecurityTotal;
+ private int policyViolationsSecurityAudited;
+ private int policyViolationsSecurityUnaudited;
+ private int policyViolationsLicenseTotal;
+ private int policyViolationsLicenseAudited;
+ private int policyViolationsLicenseUnaudited;
+ private int policyViolationsOperationalTotal;
+ private int policyViolationsOperationalAudited;
+ private int policyViolationsOperationalUnaudited;
@NotNull
@Schema(type = "integer", format = "int64", requiredMode = Schema.RequiredMode.REQUIRED, description = "UNIX epoch timestamp in milliseconds")
@@ -163,7 +118,7 @@ public void setLow(int low) {
}
public int getUnassigned() {
- return unassigned != null ? unassigned : 0;
+ return unassigned;
}
public void setUnassigned(int unassigned) {
@@ -203,7 +158,7 @@ public void setSuppressed(int suppressed) {
}
public int getFindingsTotal() {
- return findingsTotal != null ? findingsTotal : 0;
+ return findingsTotal;
}
public void setFindingsTotal(int findingsTotal) {
@@ -211,7 +166,7 @@ public void setFindingsTotal(int findingsTotal) {
}
public int getFindingsAudited() {
- return findingsAudited != null ? findingsAudited : 0;
+ return findingsAudited;
}
public void setFindingsAudited(int findingsAudited) {
@@ -219,14 +174,13 @@ public void setFindingsAudited(int findingsAudited) {
}
public int getFindingsUnaudited() {
- return findingsUnaudited != null ? findingsUnaudited : 0;
+ return findingsUnaudited;
}
public void setFindingsUnaudited(int findingsUnaudited) {
this.findingsUnaudited = findingsUnaudited;
}
- @ColumnName("RISKSCORE")
public double getInheritedRiskScore() {
return inheritedRiskScore;
}
@@ -236,7 +190,7 @@ public void setInheritedRiskScore(double inheritedRiskScore) {
}
public int getPolicyViolationsFail() {
- return policyViolationsFail != null ? policyViolationsFail : 0;
+ return policyViolationsFail;
}
public void setPolicyViolationsFail(int policyViolationsFail) {
@@ -244,7 +198,7 @@ public void setPolicyViolationsFail(int policyViolationsFail) {
}
public int getPolicyViolationsWarn() {
- return policyViolationsWarn != null ? policyViolationsWarn : 0;
+ return policyViolationsWarn;
}
public void setPolicyViolationsWarn(int policyViolationsWarn) {
@@ -252,7 +206,7 @@ public void setPolicyViolationsWarn(int policyViolationsWarn) {
}
public int getPolicyViolationsInfo() {
- return policyViolationsInfo != null ? policyViolationsInfo : 0;
+ return policyViolationsInfo;
}
public void setPolicyViolationsInfo(int policyViolationsInfo) {
@@ -260,7 +214,7 @@ public void setPolicyViolationsInfo(int policyViolationsInfo) {
}
public int getPolicyViolationsTotal() {
- return policyViolationsTotal != null ? policyViolationsTotal : 0;
+ return policyViolationsTotal;
}
public void setPolicyViolationsTotal(int policyViolationsTotal) {
@@ -268,7 +222,7 @@ public void setPolicyViolationsTotal(int policyViolationsTotal) {
}
public int getPolicyViolationsAudited() {
- return policyViolationsAudited != null ? policyViolationsAudited : 0;
+ return policyViolationsAudited;
}
public void setPolicyViolationsAudited(int policyViolationsAudited) {
@@ -276,7 +230,7 @@ public void setPolicyViolationsAudited(int policyViolationsAudited) {
}
public int getPolicyViolationsUnaudited() {
- return policyViolationsUnaudited != null ? policyViolationsUnaudited : 0;
+ return policyViolationsUnaudited;
}
public void setPolicyViolationsUnaudited(int policyViolationsUnaudited) {
@@ -284,7 +238,7 @@ public void setPolicyViolationsUnaudited(int policyViolationsUnaudited) {
}
public int getPolicyViolationsSecurityTotal() {
- return policyViolationsSecurityTotal != null ? policyViolationsSecurityTotal : 0;
+ return policyViolationsSecurityTotal;
}
public void setPolicyViolationsSecurityTotal(int policyViolationsSecurityTotal) {
@@ -292,7 +246,7 @@ public void setPolicyViolationsSecurityTotal(int policyViolationsSecurityTotal)
}
public int getPolicyViolationsSecurityAudited() {
- return policyViolationsSecurityAudited != null ? policyViolationsSecurityAudited : 0;
+ return policyViolationsSecurityAudited;
}
public void setPolicyViolationsSecurityAudited(int policyViolationsSecurityAudited) {
@@ -300,7 +254,7 @@ public void setPolicyViolationsSecurityAudited(int policyViolationsSecurityAudit
}
public int getPolicyViolationsSecurityUnaudited() {
- return policyViolationsSecurityUnaudited != null ? policyViolationsSecurityUnaudited : 0;
+ return policyViolationsSecurityUnaudited;
}
public void setPolicyViolationsSecurityUnaudited(int policyViolationsSecurityUnaudited) {
@@ -308,7 +262,7 @@ public void setPolicyViolationsSecurityUnaudited(int policyViolationsSecurityUna
}
public int getPolicyViolationsLicenseTotal() {
- return policyViolationsLicenseTotal != null ? policyViolationsLicenseTotal : 0;
+ return policyViolationsLicenseTotal;
}
public void setPolicyViolationsLicenseTotal(int policyViolationsLicenseTotal) {
@@ -316,7 +270,7 @@ public void setPolicyViolationsLicenseTotal(int policyViolationsLicenseTotal) {
}
public int getPolicyViolationsLicenseAudited() {
- return policyViolationsLicenseAudited != null ? policyViolationsLicenseAudited : 0;
+ return policyViolationsLicenseAudited;
}
public void setPolicyViolationsLicenseAudited(int policyViolationsLicenseAudited) {
@@ -324,7 +278,7 @@ public void setPolicyViolationsLicenseAudited(int policyViolationsLicenseAudited
}
public int getPolicyViolationsLicenseUnaudited() {
- return policyViolationsLicenseUnaudited != null ? policyViolationsLicenseUnaudited : 0;
+ return policyViolationsLicenseUnaudited;
}
public void setPolicyViolationsLicenseUnaudited(int policyViolationsLicenseUnaudited) {
@@ -332,7 +286,7 @@ public void setPolicyViolationsLicenseUnaudited(int policyViolationsLicenseUnaud
}
public int getPolicyViolationsOperationalTotal() {
- return policyViolationsOperationalTotal != null ? policyViolationsOperationalTotal : 0;
+ return policyViolationsOperationalTotal;
}
public void setPolicyViolationsOperationalTotal(int policyViolationsOperationalTotal) {
@@ -340,7 +294,7 @@ public void setPolicyViolationsOperationalTotal(int policyViolationsOperationalT
}
public int getPolicyViolationsOperationalAudited() {
- return policyViolationsOperationalAudited != null ? policyViolationsOperationalAudited : 0;
+ return policyViolationsOperationalAudited;
}
public void setPolicyViolationsOperationalAudited(int policyViolationsOperationalAudited) {
@@ -348,7 +302,7 @@ public void setPolicyViolationsOperationalAudited(int policyViolationsOperationa
}
public int getPolicyViolationsOperationalUnaudited() {
- return policyViolationsOperationalUnaudited != null ? policyViolationsOperationalUnaudited : 0;
+ return policyViolationsOperationalUnaudited;
}
public void setPolicyViolationsOperationalUnaudited(int policyViolationsOperationalUnaudited) {
@@ -371,37 +325,4 @@ public void setLastOccurrence(Date lastOccurrence) {
this.lastOccurrence = lastOccurrence;
}
- @JsonIgnore
- public boolean hasChanged(final ProjectMetrics comparedTo) {
- return comparedTo == null
- || comparedTo.getCritical() != this.critical
- || comparedTo.getHigh() != this.high
- || comparedTo.getMedium() != this.medium
- || comparedTo.getLow() != this.low
- || comparedTo.getUnassigned() != this.unassigned
- || comparedTo.getVulnerabilities() != this.vulnerabilities
- || comparedTo.getSuppressed() != this.suppressed
- || comparedTo.getFindingsTotal() != this.findingsTotal
- || comparedTo.getFindingsAudited() != this.findingsAudited
- || comparedTo.getFindingsUnaudited() != this.findingsUnaudited
- || comparedTo.getInheritedRiskScore() != this.inheritedRiskScore
- || comparedTo.getPolicyViolationsFail() != this.policyViolationsFail
- || comparedTo.getPolicyViolationsWarn() != this.policyViolationsWarn
- || comparedTo.getPolicyViolationsInfo() != this.policyViolationsInfo
- || comparedTo.getPolicyViolationsTotal() != this.policyViolationsTotal
- || comparedTo.getPolicyViolationsAudited() != this.policyViolationsAudited
- || comparedTo.getPolicyViolationsUnaudited() != this.policyViolationsUnaudited
- || comparedTo.getPolicyViolationsSecurityTotal() != this.policyViolationsSecurityTotal
- || comparedTo.getPolicyViolationsSecurityAudited() != this.policyViolationsSecurityAudited
- || comparedTo.getPolicyViolationsSecurityUnaudited() != this.policyViolationsSecurityUnaudited
- || comparedTo.getPolicyViolationsLicenseTotal() != this.policyViolationsLicenseTotal
- || comparedTo.getPolicyViolationsLicenseAudited() != this.policyViolationsLicenseAudited
- || comparedTo.getPolicyViolationsLicenseUnaudited() != this.policyViolationsLicenseUnaudited
- || comparedTo.getPolicyViolationsOperationalTotal() != this.policyViolationsOperationalTotal
- || comparedTo.getPolicyViolationsOperationalAudited() != this.policyViolationsOperationalAudited
- || comparedTo.getPolicyViolationsOperationalUnaudited() != this.policyViolationsOperationalUnaudited
- || comparedTo.getComponents() != this.components
- || comparedTo.getVulnerableComponents() != this.vulnerableComponents;
- }
-
}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseMigrationInitTask.java b/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseMigrationInitTask.java
new file mode 100644
index 0000000000..83c10e87c8
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseMigrationInitTask.java
@@ -0,0 +1,58 @@
+/*
+ * 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.persistence;
+
+import alpine.server.util.DbUtil;
+import org.dependencytrack.init.InitTask;
+import org.dependencytrack.init.InitTaskContext;
+import org.dependencytrack.support.liquibase.MigrationExecutor;
+
+import java.sql.Connection;
+
+/**
+ * @since 5.6.0
+ */
+public final class DatabaseMigrationInitTask implements InitTask {
+
+ @Override
+ public int priority() {
+ return PRIORITY_HIGHEST;
+ }
+
+ @Override
+ public String name() {
+ return "database.migration";
+ }
+
+ @Override
+ public void execute(InitTaskContext ctx) throws Exception {
+ try (final Connection connection = ctx.dataSource().getConnection()) {
+ // Ensure that DbUtil#isPostgreSQL will work as expected.
+ // Some legacy code ported over from v4 still uses this.
+ //
+ // NB: This was previously done in alpine.server.upgrade.UpgradeExecutor.
+ //
+ // TODO: Remove once DbUtil#isPostgreSQL is no longer used.
+ DbUtil.initPlatformName(connection);
+ }
+
+ new MigrationExecutor(ctx.dataSource(), "migration/changelog-main.xml").executeMigration();
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTask.java b/apiserver/src/main/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTask.java
new file mode 100644
index 0000000000..f391fafdfb
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTask.java
@@ -0,0 +1,54 @@
+/*
+ * 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.persistence;
+
+import org.dependencytrack.init.InitTask;
+import org.dependencytrack.init.InitTaskContext;
+import org.dependencytrack.persistence.jdbi.JdbiFactory;
+import org.dependencytrack.persistence.jdbi.MetricsDao;
+
+import java.time.LocalDate;
+
+/**
+ * @since 5.6.0
+ */
+public final class DatabasePartitionMaintenanceInitTask implements InitTask {
+
+ @Override
+ public int priority() {
+ return PRIORITY_HIGHEST - 10;
+ }
+
+ @Override
+ public String name() {
+ return "database.partition.maintenance";
+ }
+
+ @Override
+ public void execute(final InitTaskContext ctx) throws Exception {
+ final var jdbi = JdbiFactory.createLocalJdbi(ctx.dataSource());
+
+ jdbi.useTransaction(handle -> {
+ var metricsDao = handle.attach(MetricsDao.class);
+ metricsDao.createMetricsPartitionsForDate(LocalDate.now().toString(), LocalDate.now().plusDays(1).toString());
+ metricsDao.createMetricsPartitionsForDate(LocalDate.now().plusDays(1).toString(), LocalDate.now().plusDays(2).toString());
+ });
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseSeedingInitTask.java b/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseSeedingInitTask.java
new file mode 100644
index 0000000000..a9dbe19fc4
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/DatabaseSeedingInitTask.java
@@ -0,0 +1,502 @@
+/*
+ * 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.persistence;
+
+import alpine.server.auth.PasswordService;
+import org.apache.commons.lang3.SerializationUtils;
+import org.dependencytrack.auth.Permissions;
+import org.dependencytrack.init.InitTask;
+import org.dependencytrack.init.InitTaskContext;
+import org.dependencytrack.model.ConfigPropertyConstants;
+import org.dependencytrack.model.DefaultRepository;
+import org.dependencytrack.model.License;
+import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
+import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser;
+import org.dependencytrack.persistence.jdbi.ConfigPropertyDao;
+import org.dependencytrack.persistence.jdbi.JdbiFactory;
+import org.jdbi.v3.core.Handle;
+import org.jdbi.v3.core.statement.PreparedBatch;
+import org.jdbi.v3.core.statement.Update;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jakarta.json.Json;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static org.dependencytrack.model.ConfigPropertyConstants.INTERNAL_DEFAULT_OBJECTS_VERSION;
+import static org.dependencytrack.model.ConfigPropertyConstants.NOTIFICATION_TEMPLATE_BASE_DIR;
+import static org.dependencytrack.model.ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED;
+
+/**
+ * @since 5.6.0
+ */
+public final class DatabaseSeedingInitTask implements InitTask {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseSeedingInitTask.class);
+
+ private static final Map> DEFAULT_TEAM_PERMISSIONS = Map.of(
+ "Administrators", Stream.of(Permissions.values())
+ .map(Permissions::name)
+ .toList(),
+ "Portfolio Managers", List.of(
+ Permissions.Constants.VIEW_PORTFOLIO,
+ Permissions.Constants.PORTFOLIO_MANAGEMENT,
+ Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE,
+ Permissions.Constants.PORTFOLIO_MANAGEMENT_READ,
+ Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE,
+ Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE),
+ "Automation", List.of(
+ Permissions.Constants.VIEW_PORTFOLIO,
+ Permissions.Constants.BOM_UPLOAD),
+ "Badge Viewers", List.of(
+ Permissions.Constants.VIEW_BADGES));
+
+ private static final Map> DEFAULT_ROLE_PERMISSIONS = Map.of(
+ "Project Admin", List.of(
+ Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE,
+ Permissions.Constants.PORTFOLIO_MANAGEMENT_READ,
+ Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE,
+ Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE,
+ Permissions.Constants.VULNERABILITY_ANALYSIS,
+ Permissions.Constants.VULNERABILITY_ANALYSIS_CREATE,
+ Permissions.Constants.VULNERABILITY_ANALYSIS_READ,
+ Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE,
+ Permissions.Constants.POLICY_MANAGEMENT,
+ Permissions.Constants.POLICY_MANAGEMENT_CREATE,
+ Permissions.Constants.POLICY_MANAGEMENT_READ,
+ Permissions.Constants.POLICY_MANAGEMENT_UPDATE,
+ Permissions.Constants.POLICY_MANAGEMENT_DELETE),
+ "Project Auditor", List.of(
+ Permissions.Constants.VIEW_PORTFOLIO,
+ Permissions.Constants.VIEW_VULNERABILITY,
+ Permissions.Constants.VIEW_POLICY_VIOLATION,
+ Permissions.Constants.VULNERABILITY_ANALYSIS_READ),
+ "Project Editor", List.of(
+ Permissions.Constants.BOM_UPLOAD,
+ Permissions.Constants.VIEW_PORTFOLIO,
+ Permissions.Constants.PORTFOLIO_MANAGEMENT_READ,
+ Permissions.Constants.VIEW_VULNERABILITY,
+ Permissions.Constants.VULNERABILITY_ANALYSIS_READ,
+ Permissions.Constants.PROJECT_CREATION_UPLOAD),
+ "Project Viewer", List.of(
+ Permissions.Constants.VIEW_PORTFOLIO,
+ Permissions.Constants.VIEW_VULNERABILITY,
+ Permissions.Constants.VIEW_BADGES));
+
+ @Override
+ public int priority() {
+ return PRIORITY_HIGHEST - 10;
+ }
+
+ @Override
+ public String name() {
+ return "database.seeding";
+ }
+
+ @Override
+ public void execute(final InitTaskContext ctx) throws Exception {
+ final var jdbi = JdbiFactory.createLocalJdbi(ctx.dataSource());
+
+ jdbi.useTransaction(handle -> {
+ final var configPropertyDao = handle.attach(ConfigPropertyDao.class);
+
+ final String defaultObjectsVersion = configPropertyDao
+ .getOptionalValue(INTERNAL_DEFAULT_OBJECTS_VERSION)
+ .orElse(null);
+ if (ctx.config().getApplicationBuildUuid().equals(defaultObjectsVersion)) {
+ LOGGER.info(
+ "Default objects already populated for build {} (timestamp: {}); Skipping",
+ ctx.config().getApplicationBuildUuid(),
+ ctx.config().getApplicationBuildTimestamp());
+ return;
+ }
+
+ seedDefaultConfigProperties(handle);
+ seedDefaultPermissions(handle);
+ seedDefaultLicenses(handle);
+ seedDefaultNotificationPublishers(handle);
+ seedDefaultRepositories(handle);
+
+ final boolean isFirstExecution = defaultObjectsVersion == null;
+ if (isFirstExecution) {
+ seedDefaultTeams(handle);
+ seedDefaultRoles(handle);
+ seedDefaultUsers(handle);
+ seedDefaultLicenseGroups(handle);
+ }
+
+ configPropertyDao.setValue(
+ INTERNAL_DEFAULT_OBJECTS_VERSION,
+ ctx.config().getApplicationBuildUuid());
+ });
+ }
+
+ public static void seedDefaultConfigProperties(final Handle jdbiHandle) {
+ final PreparedBatch preparedBatch = jdbiHandle.prepareBatch("""
+ INSERT INTO "CONFIGPROPERTY" ("GROUPNAME", "PROPERTYNAME", "PROPERTYTYPE", "PROPERTYVALUE", "DESCRIPTION")
+ VALUES (:groupName, :propertyName, :propertyType, :defaultPropertyValue, :description)
+ ON CONFLICT ("GROUPNAME", "PROPERTYNAME") DO NOTHING
+ """);
+
+ for (final ConfigPropertyConstants configProperty : ConfigPropertyConstants.values()) {
+ preparedBatch.bindBean(configProperty);
+ preparedBatch.add();
+ }
+
+ final int configPropertiesCreated = Arrays.stream(preparedBatch.execute()).sum();
+ LOGGER.debug("Created {} config properties", configPropertiesCreated);
+ }
+
+ public static void seedDefaultPermissions(final Handle jdbiHandle) {
+ final PreparedBatch preparedBatch = jdbiHandle.prepareBatch("""
+ INSERT INTO "PERMISSION" ("NAME", "DESCRIPTION")
+ VALUES (:name, :description)
+ ON CONFLICT ("NAME") DO NOTHING
+ """);
+
+ for (final Permissions permission : Permissions.values()) {
+ preparedBatch.bind("name", permission.name());
+ preparedBatch.bind("description", permission.getDescription());
+ preparedBatch.add();
+ }
+
+ final int permissionsCreated = Arrays.stream(preparedBatch.execute()).sum();
+ LOGGER.debug("Created {} permissions", permissionsCreated);
+ }
+
+ public static void seedDefaultTeams(final Handle jdbiHandle) {
+ final Update update = jdbiHandle.createUpdate("""
+ WITH cte_team_permission AS (
+ SELECT *
+ FROM UNNEST(:teamNames, :permissionNames) AS t(team_name, permission_name)
+ ),
+ cte_created_team AS (
+ INSERT INTO "TEAM" ("NAME", "UUID")
+ SELECT DISTINCT ON (team_name)
+ team_name
+ , GEN_RANDOM_UUID()
+ FROM cte_team_permission
+ RETURNING "ID" AS id
+ , "NAME" AS name
+ )
+ INSERT INTO "TEAMS_PERMISSIONS" ("TEAM_ID", "PERMISSION_ID")
+ SELECT cte_created_team.id
+ , (SELECT "ID" FROM "PERMISSION" WHERE "NAME" = cte_team_permission.permission_name)
+ FROM cte_team_permission
+ INNER JOIN cte_created_team
+ ON cte_created_team.name = cte_team_permission.team_name
+ """);
+
+ final var teamNames = new ArrayList();
+ final var permissionNames = new ArrayList();
+
+ for (final Map.Entry> entry : DEFAULT_TEAM_PERMISSIONS.entrySet()) {
+ for (final String permissionName : entry.getValue()) {
+ teamNames.add(entry.getKey());
+ permissionNames.add(permissionName);
+ }
+ }
+
+ update
+ .bindArray("teamNames", String.class, teamNames)
+ .bindArray("permissionNames", String.class, permissionNames)
+ .execute();
+ }
+
+ public static void seedDefaultRoles(final Handle jdbiHandle) {
+ final Update update = jdbiHandle.createUpdate("""
+ WITH cte_role_permission AS (
+ SELECT *
+ FROM UNNEST(:roleNames, :permissionNames) AS t(role_name, permission_name)
+ ),
+ cte_created_role AS (
+ INSERT INTO "ROLE" ("NAME", "UUID")
+ SELECT DISTINCT ON (role_name)
+ role_name
+ , GEN_RANDOM_UUID()
+ FROM cte_role_permission
+ RETURNING "ID" AS id
+ , "NAME" AS name
+ )
+ INSERT INTO "ROLES_PERMISSIONS" ("ROLE_ID", "PERMISSION_ID")
+ SELECT cte_created_role.id
+ , (SELECT "ID" FROM "PERMISSION" WHERE "NAME" = cte_role_permission.permission_name)
+ FROM cte_role_permission
+ INNER JOIN cte_created_role
+ ON cte_created_role.name = cte_role_permission.role_name
+ """);
+
+ final var roleNames = new ArrayList();
+ final var permissionNames = new ArrayList();
+
+ for (final Map.Entry> entry : DEFAULT_ROLE_PERMISSIONS.entrySet()) {
+ for (final String permissionName : entry.getValue()) {
+ roleNames.add(entry.getKey());
+ permissionNames.add(permissionName);
+ }
+ }
+
+ update
+ .bindArray("roleNames", String.class, roleNames)
+ .bindArray("permissionNames", String.class, permissionNames)
+ .execute();
+ }
+
+ public static void seedDefaultUsers(final Handle jdbiHandle) {
+ final long adminUserId = jdbiHandle.createUpdate("""
+ INSERT INTO "USER" (
+ "TYPE", "USERNAME", "EMAIL", "PASSWORD", "LAST_PASSWORD_CHANGE"
+ , "FORCE_PASSWORD_CHANGE", "NON_EXPIRY_PASSWORD", "SUSPENDED")
+ VALUES ('MANAGED', 'admin', 'admin@localhost', :password, NOW(), TRUE, TRUE, FALSE)
+ RETURNING "ID"
+ """)
+ .bind("password", new String(PasswordService.createHash("admin".toCharArray())))
+ .executeAndReturnGeneratedKeys()
+ .mapTo(Long.class)
+ .one();
+
+ jdbiHandle.createUpdate("""
+ INSERT INTO "USERS_TEAMS" ("USER_ID", "TEAM_ID")
+ SELECT :adminUserId, (SELECT "ID" FROM "TEAM" WHERE "NAME" = 'Administrators')
+ """)
+ .bind("adminUserId", adminUserId)
+ .execute();
+
+ jdbiHandle.createUpdate("""
+ INSERT INTO "USERS_PERMISSIONS" ("USER_ID", "PERMISSION_ID")
+ SELECT :adminUserId, "PERMISSION"."ID" FROM "PERMISSION"
+ """)
+ .bind("adminUserId", adminUserId)
+ .execute();
+ }
+
+ public static void seedDefaultLicenses(final Handle jdbiHandle) {
+ final List licenses;
+ try {
+ licenses = new SpdxLicenseDetailParser().getLicenseDefinitions();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to load license details", e);
+ }
+
+ // We have hundreds of licenses, the majority of which is *very* unlikely to change between executions
+ // of this init task. In the future, we should store the version of the SPDX license list,
+ // and then only sync licenses when that version has changed.
+ final PreparedBatch preparedBatch = jdbiHandle.prepareBatch("""
+ INSERT INTO "LICENSE" (
+ "LICENSEID", "NAME", "HEADER", "TEXT", "TEMPLATE", "ISDEPRECATED"
+ , "FSFLIBRE", "ISOSIAPPROVED", "COMMENT", "SEEALSO", "UUID"
+ )
+ VALUES (
+ :licenseId, :name, :header, :text, :template, :deprecatedLicenseId
+ , :fsfLibre, :osiApproved, :comment, :seeAlsoSerialized, GEN_RANDOM_UUID()
+ )
+ ON CONFLICT ("LICENSEID") DO UPDATE
+ SET "NAME" = EXCLUDED."NAME"
+ , "HEADER" = EXCLUDED."HEADER"
+ , "TEXT" = EXCLUDED."TEXT"
+ , "TEMPLATE" = EXCLUDED."TEMPLATE"
+ , "ISDEPRECATED" = EXCLUDED."ISDEPRECATED"
+ , "FSFLIBRE" = EXCLUDED."FSFLIBRE"
+ , "ISOSIAPPROVED" = EXCLUDED."ISOSIAPPROVED"
+ , "COMMENT" = EXCLUDED."COMMENT"
+ , "SEEALSO" = EXCLUDED."SEEALSO"
+ -- Only update when at least one relevant field has changed.
+ WHERE "LICENSE"."NAME" IS DISTINCT FROM EXCLUDED."NAME"
+ OR "LICENSE"."HEADER" IS DISTINCT FROM EXCLUDED."HEADER"
+ OR "LICENSE"."TEXT" IS DISTINCT FROM EXCLUDED."TEXT"
+ OR "LICENSE"."TEMPLATE" IS DISTINCT FROM EXCLUDED."TEMPLATE"
+ OR "LICENSE"."ISDEPRECATED" IS DISTINCT FROM EXCLUDED."ISDEPRECATED"
+ OR "LICENSE"."FSFLIBRE" IS DISTINCT FROM EXCLUDED."FSFLIBRE"
+ OR "LICENSE"."ISOSIAPPROVED" IS DISTINCT FROM EXCLUDED."ISOSIAPPROVED"
+ OR "LICENSE"."COMMENT" IS DISTINCT FROM EXCLUDED."COMMENT"
+ OR "LICENSE"."SEEALSO" IS DISTINCT FROM EXCLUDED."SEEALSO"
+ """);
+
+ for (final License license : licenses) {
+ preparedBatch.bindBean(license);
+ preparedBatch.bind(
+ "seeAlsoSerialized",
+ license.getSeeAlso() != null
+ ? SerializationUtils.serialize(license.getSeeAlso())
+ : null);
+ preparedBatch.add();
+ }
+
+ int licensesCreatedOrUpdated = Arrays.stream(preparedBatch.execute()).sum();
+ LOGGER.debug("Created or updated {} licenses", licensesCreatedOrUpdated);
+ }
+
+ public static void seedDefaultLicenseGroups(final Handle jdbiHandle) {
+ final Update update = jdbiHandle.createUpdate("""
+ WITH cte_group_license AS (
+ SELECT *
+ FROM UNNEST(:groupNames, :groupRiskWeights, :licenseIds) AS t(group_name, group_risk_weight, license_id)
+ ),
+ cte_created_group AS (
+ INSERT INTO "LICENSEGROUP" ("NAME", "RISKWEIGHT", "UUID")
+ SELECT DISTINCT ON (group_name)
+ group_name
+ , group_risk_weight
+ , GEN_RANDOM_UUID()
+ FROM cte_group_license
+ RETURNING "ID" AS id, "NAME" AS name
+ )
+ INSERT INTO "LICENSEGROUP_LICENSE" ("LICENSEGROUP_ID", "LICENSE_ID")
+ SELECT cte_created_group.id
+ , (SELECT "ID" FROM "LICENSE" WHERE "LICENSEID" = cte_group_license.license_id)
+ FROM cte_group_license
+ INNER JOIN cte_created_group
+ ON cte_created_group.name = cte_group_license.group_name
+ """);
+
+ final JsonArray groupDefsJson;
+ try (final InputStream inputStream = DatabaseSeedingInitTask.class.getResourceAsStream("/default-objects/licenseGroups.json");
+ final JsonReader jsonReader = Json.createReader(inputStream)) {
+ groupDefsJson = jsonReader.readArray();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to parse license group definition", e);
+ }
+
+ final var groupNames = new ArrayList();
+ final var groupRiskWeights = new ArrayList();
+ final var licenseIds = new ArrayList();
+
+ for (int i = 0; i < groupDefsJson.size(); i++) {
+ final JsonObject groupDefJson = groupDefsJson.getJsonObject(i);
+ final String groupName = groupDefJson.getString("name");
+ final int riskWeight = groupDefJson.getInt("riskWeight");
+
+ final JsonArray licenseIdsJson = groupDefJson.getJsonArray("licenses");
+ for (int j = 0; j < licenseIdsJson.size(); j++) {
+ groupNames.add(groupName);
+ groupRiskWeights.add(riskWeight);
+ licenseIds.add(licenseIdsJson.getString(j));
+ }
+ }
+
+ update
+ .bindArray("groupNames", String.class, groupNames)
+ .bindArray("groupRiskWeights", Integer.class, groupRiskWeights)
+ .bindArray("licenseIds", String.class, licenseIds)
+ .execute();
+ }
+
+ public static void seedDefaultNotificationPublishers(final Handle jdbiHandle) {
+ final PreparedBatch preparedBatch = jdbiHandle.prepareBatch("""
+ INSERT INTO "NOTIFICATIONPUBLISHER" (
+ "NAME", "PUBLISHER_CLASS", "DEFAULT_PUBLISHER", "DESCRIPTION"
+ , "TEMPLATE", "TEMPLATE_MIME_TYPE", "UUID")
+ VALUES (
+ :publisherName, :publisherClass, TRUE, :publisherDescription
+ , :templateContent, :templateMimeType, GEN_RANDOM_UUID())
+ ON CONFLICT ("NAME") DO UPDATE
+ SET "PUBLISHER_CLASS" = EXCLUDED."PUBLISHER_CLASS"
+ , "DESCRIPTION" = EXCLUDED."DESCRIPTION"
+ , "TEMPLATE" = EXCLUDED."TEMPLATE"
+ , "TEMPLATE_MIME_TYPE" = EXCLUDED."TEMPLATE_MIME_TYPE"
+ -- Only update when at least one relevant field has changed.
+ WHERE "NOTIFICATIONPUBLISHER"."PUBLISHER_CLASS" IS DISTINCT FROM EXCLUDED."PUBLISHER_CLASS"
+ OR "NOTIFICATIONPUBLISHER"."DESCRIPTION" IS DISTINCT FROM EXCLUDED."DESCRIPTION"
+ OR "NOTIFICATIONPUBLISHER"."TEMPLATE" IS DISTINCT FROM EXCLUDED."TEMPLATE"
+ OR "NOTIFICATIONPUBLISHER"."TEMPLATE_MIME_TYPE" IS DISTINCT FROM EXCLUDED."TEMPLATE_MIME_TYPE"
+ """);
+
+ final var configPropertyDao = jdbiHandle.attach(ConfigPropertyDao.class);
+ final var templateOverrideEnabled = configPropertyDao.getOptionalValue(
+ NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED, Boolean.class).orElse(false);
+ final var templateOverrideBaseDir = configPropertyDao.getOptionalValue(
+ NOTIFICATION_TEMPLATE_BASE_DIR).orElse(null);
+ if (templateOverrideEnabled && templateOverrideBaseDir == null) {
+ throw new IllegalStateException("%s is enabled but %s is not configured".formatted(
+ NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getPropertyName(),
+ NOTIFICATION_TEMPLATE_BASE_DIR.getPropertyName()));
+ }
+
+ for (final DefaultNotificationPublishers publisher : DefaultNotificationPublishers.values()) {
+ final URL templateFileUrl = DatabaseSeedingInitTask.class.getResource(publisher.getPublisherTemplateFile());
+ if (templateFileUrl == null) {
+ throw new IllegalStateException("Template file %s of default publisher %s does not exist".formatted(
+ publisher.getPublisherTemplateFile(), publisher.getPublisherName()));
+ }
+
+ Path templateFilePath;
+ try {
+ templateFilePath = Paths.get(templateFileUrl.toURI());
+ } catch (URISyntaxException e) {
+ throw new IllegalStateException("Failed to construct path for template file: " + templateFileUrl, e);
+ }
+
+ if (templateOverrideEnabled) {
+ final Path customTemplateFilePath = Paths.get(templateOverrideBaseDir, publisher.getPublisherTemplateFile());
+ if (Files.exists(customTemplateFilePath)) {
+ templateFilePath = customTemplateFilePath;
+ }
+ }
+
+ final String templateContent;
+ try {
+ templateContent = Files.readString(templateFilePath);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to read template file: " + templateFilePath, e);
+ }
+
+ preparedBatch.bindBean(publisher);
+ preparedBatch.bind("templateContent", templateContent);
+ preparedBatch.add();
+ }
+
+ final int publishersCreatedOrUpdated = Arrays.stream(preparedBatch.execute()).sum();
+ LOGGER.debug("Created or updated {} publishers", publishersCreatedOrUpdated);
+ }
+
+ public static void seedDefaultRepositories(final Handle jdbiHandle) {
+ final PreparedBatch preparedBatch = jdbiHandle.prepareBatch("""
+ INSERT INTO "REPOSITORY"(
+ "TYPE", "IDENTIFIER", "URL", "INTERNAL", "RESOLUTION_ORDER"
+ , "ENABLED", "AUTHENTICATIONREQUIRED", "UUID")
+ VALUES (
+ :type, :identifier, :url, FALSE, :resolutionOrder
+ , TRUE, FALSE, GEN_RANDOM_UUID())
+ ON CONFLICT ("TYPE", "IDENTIFIER") DO NOTHING
+ """);
+
+ for (final DefaultRepository repository : DefaultRepository.values()) {
+ preparedBatch.bindBean(repository);
+ preparedBatch.add();
+ }
+
+ final int reposCreated = Arrays.stream(preparedBatch.execute()).sum();
+ LOGGER.debug("Created {} repositories", reposCreated);
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java
deleted file mode 100644
index fc4ddcc31c..0000000000
--- a/apiserver/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java
+++ /dev/null
@@ -1,436 +0,0 @@
-/*
- * This file is part of Dependency-Track.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * SPDX-License-Identifier: Apache-2.0
- * Copyright (c) OWASP Foundation. All Rights Reserved.
- */
-package org.dependencytrack.persistence;
-
-import alpine.Config;
-import alpine.common.logging.Logger;
-import alpine.model.ConfigProperty;
-import alpine.model.ManagedUser;
-import alpine.model.Permission;
-import alpine.server.auth.PasswordService;
-
-import jakarta.servlet.ServletContextEvent;
-import jakarta.servlet.ServletContextListener;
-
-import org.dependencytrack.auth.Permissions;
-import org.dependencytrack.common.ConfigKey;
-import org.dependencytrack.model.ConfigPropertyConstants;
-import org.dependencytrack.model.License;
-import org.dependencytrack.model.RepositoryType;
-import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser;
-import org.dependencytrack.persistence.defaults.DefaultLicenseGroupImporter;
-import org.dependencytrack.persistence.jdbi.MetricsDao;
-import org.dependencytrack.util.NotificationUtil;
-import org.dependencytrack.util.WaitingLockConfiguration;
-
-import java.io.IOException;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Collections;
-import java.time.LocalDate;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Stream;
-
-import static net.javacrumbs.shedlock.core.LockAssert.assertLocked;
-import static org.dependencytrack.model.ConfigPropertyConstants.INTERNAL_DEFAULT_OBJECTS_VERSION;
-import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle;
-import static org.dependencytrack.util.LockProvider.executeWithLockWaiting;
-
-/**
- * Creates default objects on an empty database.
- *
- * @author Steve Springett
- * @since 3.0.0
- */
-public class DefaultObjectGenerator implements ServletContextListener {
-
- private static final Logger LOGGER = Logger.getLogger(DefaultObjectGenerator.class);
-
- private static final Map> DEFAULT_TEAM_PERMISSIONS = Map.of(
- "Administrators", Stream.of(Permissions.values())
- .map(Permissions::name)
- .toList(),
- "Portfolio Managers", List.of(
- Permissions.Constants.VIEW_PORTFOLIO,
- Permissions.Constants.PORTFOLIO_MANAGEMENT,
- Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE,
- Permissions.Constants.PORTFOLIO_MANAGEMENT_READ,
- Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE,
- Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE),
- "Automation", List.of(
- Permissions.Constants.VIEW_PORTFOLIO,
- Permissions.Constants.BOM_UPLOAD),
- "Badge Viewers", List.of(
- Permissions.Constants.VIEW_BADGES));
-
- private static final Map> DEFAULT_ROLE_PERMISSIONS = Map.of(
- "Project Admin", List.of(
- Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE,
- Permissions.Constants.PORTFOLIO_MANAGEMENT_READ,
- Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE,
- Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE,
- Permissions.Constants.VULNERABILITY_ANALYSIS,
- Permissions.Constants.VULNERABILITY_ANALYSIS_CREATE,
- Permissions.Constants.VULNERABILITY_ANALYSIS_READ,
- Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE,
- Permissions.Constants.POLICY_MANAGEMENT,
- Permissions.Constants.POLICY_MANAGEMENT_CREATE,
- Permissions.Constants.POLICY_MANAGEMENT_READ,
- Permissions.Constants.POLICY_MANAGEMENT_UPDATE,
- Permissions.Constants.POLICY_MANAGEMENT_DELETE),
- "Project Auditor", List.of(
- Permissions.Constants.VIEW_PORTFOLIO,
- Permissions.Constants.VIEW_VULNERABILITY,
- Permissions.Constants.VIEW_POLICY_VIOLATION,
- Permissions.Constants.VULNERABILITY_ANALYSIS_READ),
- "Project Editor", List.of(
- Permissions.Constants.BOM_UPLOAD,
- Permissions.Constants.VIEW_PORTFOLIO,
- Permissions.Constants.PORTFOLIO_MANAGEMENT_READ,
- Permissions.Constants.VIEW_VULNERABILITY,
- Permissions.Constants.VULNERABILITY_ANALYSIS_READ,
- Permissions.Constants.PROJECT_CREATION_UPLOAD),
- "Project Viewer", List.of(
- Permissions.Constants.VIEW_PORTFOLIO,
- Permissions.Constants.VIEW_VULNERABILITY,
- Permissions.Constants.VIEW_BADGES));
-
- private final Map persistentPermissionByName = new HashMap<>();
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void contextInitialized(final ServletContextEvent event) {
- if (!Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_TASKS_ENABLED)) {
- LOGGER.info("Not populating database with default objects because %s is disabled"
- .formatted(ConfigKey.INIT_TASKS_ENABLED.getPropertyName()));
- return;
- }
-
- // Ensure that this task is only executed by a single instance at once.
- // Wait for lock acquisition rather than simply skipping execution,
- // since application logic may depend on default objects being present.
- final var lockConfig = new WaitingLockConfiguration(
- /* createdAt */ Instant.now(),
- /* name */ getClass().getName(),
- /* lockAtMostFor */ Duration.ofMinutes(5),
- /* lockAtLeastFor */ Duration.ZERO,
- /* pollInterval */ Duration.ofSeconds(1),
- /* waitTimeout */ Duration.ofMinutes(5));
-
- try {
- executeWithLockWaiting(lockConfig, this::executeLocked);
- } catch (Throwable t) {
- if (Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) {
- // Make absolutely sure that we exit with non-zero code so
- // the container orchestrator knows to restart the container.
- LOGGER.error("Failed to populate database with default objects", t);
- System.exit(1);
- }
-
- throw new RuntimeException("Failed to populate database with default objects", t);
- }
-
- if (Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) {
- LOGGER.info("Exiting because %s is enabled".formatted(ConfigKey.INIT_AND_EXIT.getPropertyName()));
- System.exit(0);
- }
- }
-
- private void executeLocked() {
- assertLocked();
-
- if (!shouldExecute()) {
- LOGGER.info("Default objects already populated for build %s (timestamp: %s); Skipping".formatted(
- Config.getInstance().getApplicationBuildUuid(),
- Config.getInstance().getApplicationBuildTimestamp()));
- return;
- }
-
- // TODO: Make population transactional with recordDefaultObjectsVersion().
-
- LOGGER.info("Initializing default object generator");
- try (final var qm = new QueryManager()) {
- loadDefaultPermissions(qm);
- loadDefaultPersonas(qm);
- loadDefaultLicenses(qm);
- loadDefaultLicenseGroups(qm);
- loadDefaultRepositories(qm);
- loadDefaultRoles(qm);
- loadDefaultConfigProperties(qm);
- loadDefaultNotificationPublishers(qm);
- recordDefaultObjectsVersion(qm);
- }
-
- LOGGER.info("Ensuring the metrics partitions for today and tomorrow exist.");
- ensureMetricsPartitions();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void contextDestroyed(final ServletContextEvent event) {
- /* Intentionally blank to satisfy interface */
- }
-
- private boolean shouldExecute() {
- try (final var qm = new QueryManager()) {
- final ConfigProperty configProperty = qm.getConfigProperty(
- INTERNAL_DEFAULT_OBJECTS_VERSION.getGroupName(),
- INTERNAL_DEFAULT_OBJECTS_VERSION.getPropertyName());
-
- return configProperty == null
- || configProperty.getPropertyValue() == null
- || !Config.getInstance().getApplicationBuildUuid().equals(configProperty.getPropertyValue());
- }
- }
-
- private void recordDefaultObjectsVersion(final QueryManager qm) {
- qm.runInTransaction(() -> {
- final ConfigProperty configProperty = qm.getConfigProperty(
- INTERNAL_DEFAULT_OBJECTS_VERSION.getGroupName(),
- INTERNAL_DEFAULT_OBJECTS_VERSION.getPropertyName());
-
- configProperty.setPropertyValue(Config.getInstance().getApplicationBuildUuid());
- });
- }
-
- public static void loadDefaultLicenses() {
- try (final var qm = new QueryManager()) {
- loadDefaultLicenses(qm);
- }
- }
-
- /**
- * Loads the default licenses into the database if no license data exists.
- */
- private static void loadDefaultLicenses(final QueryManager qm) {
- LOGGER.info("Synchronizing SPDX license definitions to datastore");
-
- final SpdxLicenseDetailParser parser = new SpdxLicenseDetailParser();
- try {
- final List licenses = parser.getLicenseDefinitions();
- for (final License license : licenses) {
- LOGGER.debug("Synchronizing: " + license.getName());
- qm.synchronizeLicense(license, false);
- }
- } catch (IOException e) {
- LOGGER.error("An error occurred during the parsing SPDX license definitions");
- LOGGER.error(e.getMessage());
- }
- }
-
- /**
- * Loads the default license groups into the database if no license groups exists.
- */
- private void loadDefaultLicenseGroups(final QueryManager qm) {
- final DefaultLicenseGroupImporter importer = new DefaultLicenseGroupImporter(qm);
- if (!importer.shouldImport()) {
- return;
- }
- LOGGER.info("Adding default license group definitions to datastore");
- try {
- importer.loadDefaults();
- } catch (IOException e) {
- LOGGER.error("An error occurred loading default license group definitions");
- LOGGER.error(e.getMessage());
- }
- }
-
- public void loadDefaultPermissions() {
- try (final var qm = new QueryManager()) {
- loadDefaultPermissions(qm);
- }
- }
-
- /**
- * Loads the default permissions
- */
- private void loadDefaultPermissions(final QueryManager qm) {
- LOGGER.info("Synchronizing permissions to datastore");
-
- List existing = Objects.requireNonNullElse(qm.getPermissions(), Collections.emptyList())
- .stream()
- .map(Permission::getName)
- .toList();
-
- for (final Permissions value : Permissions.values())
- if (!existing.contains(value.name())) {
- LOGGER.debug("Creating permission: " + value.name());
- persistentPermissionByName.put(value.name(), qm.createPermission(value.name(), value.getDescription()));
- }
- }
-
- @SuppressWarnings("unused")
- void loadDefaultPersonas() {
- try (final var qm = new QueryManager()) {
- loadDefaultPersonas(qm);
- }
- }
-
- /**
- * Loads the default users and teams
- */
- private void loadDefaultPersonas(final QueryManager qm) {
- if (!qm.getManagedUsers().isEmpty() && qm.getTeams().getTotal() != 0)
- return;
-
- LOGGER.info("Adding default users and teams to datastore");
-
- LOGGER.debug("Creating user: admin");
- ManagedUser admin = qm.createManagedUser("admin", "Administrator", "admin@localhost",
- new String(PasswordService.createHash("admin".toCharArray())), true, true, false);
-
- for (var name : DEFAULT_TEAM_PERMISSIONS.keySet()) {
- LOGGER.debug("Creating team: " + name);
- var team = qm.createTeam(name);
-
- LOGGER.debug("Assigning default permissions for team: " + name);
- team.setPermissions(getPermissionsByName(DEFAULT_TEAM_PERMISSIONS.get(name)));
-
- qm.persist(team);
- }
-
- LOGGER.debug("Adding admin user to System Administrators");
- qm.addUserToTeam(admin, qm.getTeam("Administrators"));
-
- admin = qm.getObjectById(ManagedUser.class, admin.getId());
- admin.setPermissions(qm.getPermissions());
- qm.persist(admin);
- }
-
- /**
- * Perform a lookup of {@link Permission}s for specified name(s).
- *
- * @param names permission names
- * @return list of {@link Permission}s
- */
- private List getPermissionsByName(List names) {
- return names.stream().map(persistentPermissionByName::get).filter(Objects::nonNull).toList();
- }
-
- public void loadDefaultRoles() {
- try (final var qm = new QueryManager()) {
- loadDefaultRoles(qm);
- }
- }
-
- /**
- * Loads the default Roles
- */
- private void loadDefaultRoles(final QueryManager qm) {
- if (!qm.getRoles().isEmpty())
- return;
-
- LOGGER.info("Adding default roles to datastore");
-
- for (var name : DEFAULT_ROLE_PERMISSIONS.keySet()) {
- LOGGER.debug("Creating role: " + name);
- qm.createRole(name, getPermissionsByName(DEFAULT_ROLE_PERMISSIONS.get(name)));
- }
- }
-
- public void loadDefaultRepositories() {
- try (final var qm = new QueryManager()) {
- loadDefaultRepositories(qm);
- }
- }
-
- /**
- * Loads the default repositories
- */
- private void loadDefaultRepositories(final QueryManager qm) {
- LOGGER.info("Synchronizing default repositories to datastore");
- // @formatter:off
- qm.createRepository(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", true, false, false, null, null);
- qm.createRepository(RepositoryType.GEM, "rubygems.org", "https://rubygems.org/", true, false, false,null, null);
- qm.createRepository(RepositoryType.HEX, "hex.pm", "https://hex.pm/", true, false, false, null, null);
- qm.createRepository(RepositoryType.MAVEN, "central", "https://repo1.maven.org/maven2/", true, false, false, null, null);
- qm.createRepository(RepositoryType.MAVEN, "atlassian-public", "https://packages.atlassian.com/content/repositories/atlassian-public/", true, false, false, null, null);
- qm.createRepository(RepositoryType.MAVEN, "jboss-releases", "https://repository.jboss.org/nexus/content/repositories/releases/", true, false, false, null, null);
- qm.createRepository(RepositoryType.MAVEN, "clojars", "https://repo.clojars.org/", true, false, false, null, null);
- qm.createRepository(RepositoryType.MAVEN, "google-android", "https://maven.google.com/", true, false, false, null, null);
- qm.createRepository(RepositoryType.NPM, "npm-public-registry", "https://registry.npmjs.org/", true, false, false, null, null);
- qm.createRepository(RepositoryType.PYPI, "pypi.org", "https://pypi.org/", true, false, false, null, null);
- qm.createRepository(RepositoryType.NUGET, "nuget-gallery", "https://api.nuget.org/", true, false, false, null, null);
- qm.createRepository(RepositoryType.COMPOSER, "packagist", "https://repo.packagist.org/", true, false, false, null, null);
- qm.createRepository(RepositoryType.CARGO, "crates.io", "https://crates.io", true, false, false, null, null);
- qm.createRepository(RepositoryType.GO_MODULES, "proxy.golang.org", "https://proxy.golang.org", true, false, false, null, null);
- qm.createRepository(RepositoryType.GITHUB, "github.com", "https://github.com", true, false, false, null, null);
- qm.createRepository(RepositoryType.HACKAGE, "hackage.haskell", "https://hackage.haskell.org/", true, false, false, null, null);
- qm.createRepository(RepositoryType.NIXPKGS, "nixos.org", "https://channels.nixos.org/nixpkgs-unstable/packages.json.br", true, false, false, null, null);
- // @formatter:on
- }
-
- @SuppressWarnings("unused")
- void loadDefaultConfigProperties() {
- try (final var qm = new QueryManager()) {
- loadDefaultConfigProperties(qm);
- }
- }
-
- /**
- * Loads the default ConfigProperty objects
- */
- private void loadDefaultConfigProperties(final QueryManager qm) {
- LOGGER.info("Synchronizing config properties to datastore");
- for (final ConfigPropertyConstants cpc : ConfigPropertyConstants.values()) {
- LOGGER.debug("Creating config property: " + cpc.getGroupName() + " / " + cpc.getPropertyName());
- if (qm.getConfigProperty(cpc.getGroupName(), cpc.getPropertyName()) == null) {
- qm.createConfigProperty(cpc.getGroupName(), cpc.getPropertyName(), cpc.getDefaultPropertyValue(),
- cpc.getPropertyType(), cpc.getDescription());
- }
- }
- }
-
- public void loadDefaultNotificationPublishers() {
- try (final var qm = new QueryManager()) {
- loadDefaultNotificationPublishers(qm);
- }
- }
-
- /**
- * Loads the default notification publishers
- */
- private void loadDefaultNotificationPublishers(final QueryManager qm) {
- LOGGER.info("Synchronizing notification publishers to datastore");
- try {
- NotificationUtil.loadDefaultNotificationPublishers(qm);
- } catch (IOException e) {
- LOGGER.error("An error occurred while synchronizing a default notification publisher", e);
- }
- }
-
- /**
- * Create metrics partitions for today and tomorrow if they don't exist.
- */
- void ensureMetricsPartitions() {
- useJdbiHandle(handle -> {
- var metricsHandle = handle.attach(MetricsDao.class);
- metricsHandle.createMetricsPartitionsForDate(LocalDate.now().toString(), LocalDate.now().plusDays(1).toString());
- metricsHandle.createMetricsPartitionsForDate(LocalDate.now().plusDays(1).toString(), LocalDate.now().plusDays(2).toString());
- });
- }
-}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/MigrationInitializer.java b/apiserver/src/main/java/org/dependencytrack/persistence/MigrationInitializer.java
deleted file mode 100644
index 05347dcb2c..0000000000
--- a/apiserver/src/main/java/org/dependencytrack/persistence/MigrationInitializer.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * This file is part of Dependency-Track.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * SPDX-License-Identifier: Apache-2.0
- * Copyright (c) OWASP Foundation. All Rights Reserved.
- */
-package org.dependencytrack.persistence;
-
-import alpine.Config;
-import alpine.common.logging.Logger;
-import alpine.server.util.DbUtil;
-import com.zaxxer.hikari.HikariConfig;
-import com.zaxxer.hikari.HikariDataSource;
-import org.dependencytrack.common.ConfigKey;
-import org.dependencytrack.support.liquibase.MigrationExecutor;
-
-import jakarta.servlet.ServletContextEvent;
-import jakarta.servlet.ServletContextListener;
-import java.sql.Connection;
-import java.util.Optional;
-
-public class MigrationInitializer implements ServletContextListener {
-
- private static final Logger LOGGER = Logger.getLogger(MigrationInitializer.class);
-
- private final Config config;
-
- @SuppressWarnings("unused")
- public MigrationInitializer() {
- this(Config.getInstance());
- }
-
- MigrationInitializer(final Config config) {
- this.config = config;
- }
-
- @Override
- public void contextInitialized(final ServletContextEvent event) {
- if (!config.getPropertyAsBoolean(ConfigKey.INIT_TASKS_ENABLED)) {
- LOGGER.debug("Not running migrations because %s is disabled"
- .formatted(ConfigKey.INIT_TASKS_ENABLED.getPropertyName()));
- return;
- }
- if (!config.getPropertyAsBoolean(ConfigKey.DATABASE_RUN_MIGRATIONS)) {
- LOGGER.debug("Not running migrations because %s is disabled"
- .formatted(ConfigKey.DATABASE_RUN_MIGRATIONS.getPropertyName()));
- return;
- }
-
- LOGGER.info("Running migrations");
- try (final HikariDataSource dataSource = createDataSource()) {
- try (final Connection connection = dataSource.getConnection()) {
- // Ensure that DbUtil#isPostgreSQL will work as expected.
- // Some legacy code ported over from v4 still uses this.
- //
- // NB: This was previously done in alpine.server.upgrade.UpgradeExecutor.
- //
- // TODO: Remove once DbUtil#isPostgreSQL is no longer used.
- DbUtil.initPlatformName(connection);
- }
-
- new MigrationExecutor(dataSource, "migration/changelog-main.xml").executeMigration();
- } catch (Exception e) {
- if (config.getPropertyAsBoolean(ConfigKey.DATABASE_RUN_MIGRATIONS_ONLY)
- || config.getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) {
- // Make absolutely sure that we exit with non-zero code so
- // the container orchestrator knows to restart the container.
- LOGGER.error("Failed to execute migrations", e);
- System.exit(1);
- }
-
- throw new RuntimeException("Failed to execute migrations", e);
- }
-
- if (config.getPropertyAsBoolean(ConfigKey.DATABASE_RUN_MIGRATIONS_ONLY)) {
- LOGGER.info("Exiting because %s is enabled".formatted(ConfigKey.DATABASE_RUN_MIGRATIONS.getPropertyName()));
- System.exit(0);
- }
- }
-
- private HikariDataSource createDataSource() {
- final String jdbcUrl = Optional.ofNullable(config.getProperty(ConfigKey.DATABASE_MIGRATION_URL))
- .orElseGet(() -> config.getProperty(Config.AlpineKey.DATABASE_URL));
- final String username = Optional.ofNullable(config.getProperty(ConfigKey.DATABASE_MIGRATION_USERNAME))
- .orElseGet(() -> config.getProperty(Config.AlpineKey.DATABASE_USERNAME));
- final String password = Optional.ofNullable(config.getProperty(ConfigKey.DATABASE_MIGRATION_PASSWORD))
- .orElseGet(() -> config.getProperty(Config.AlpineKey.DATABASE_PASSWORD));
-
- final var hikariCfg = new HikariConfig();
- hikariCfg.setJdbcUrl(jdbcUrl);
- hikariCfg.setDriverClassName(config.getProperty(Config.AlpineKey.DATABASE_DRIVER));
- hikariCfg.setUsername(username);
- hikariCfg.setPassword(password);
- hikariCfg.setMaximumPoolSize(1);
- hikariCfg.setMinimumIdle(1);
-
- return new HikariDataSource(hikariCfg);
- }
-}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/defaults/DefaultLicenseGroupImporter.java b/apiserver/src/main/java/org/dependencytrack/persistence/defaults/DefaultLicenseGroupImporter.java
deleted file mode 100644
index a52727c63c..0000000000
--- a/apiserver/src/main/java/org/dependencytrack/persistence/defaults/DefaultLicenseGroupImporter.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * This file is part of Dependency-Track.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * SPDX-License-Identifier: Apache-2.0
- * Copyright (c) OWASP Foundation. All Rights Reserved.
- */
-package org.dependencytrack.persistence.defaults;
-
-import alpine.common.logging.Logger;
-import org.dependencytrack.model.License;
-import org.dependencytrack.model.LicenseGroup;
-import org.dependencytrack.persistence.QueryManager;
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.URLDecoder;
-import java.util.ArrayList;
-import java.util.List;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * Imports default LicenseGroup objects into the datastore.
- *
- * @author Steve Springett
- * @since 4.0.0
- */
-public class DefaultLicenseGroupImporter implements IDefaultObjectImporter {
-
- private static final Logger LOGGER = Logger.getLogger(DefaultLicenseGroupImporter.class);
-
- private QueryManager qm;
-
- public DefaultLicenseGroupImporter(final QueryManager qm) {
- this.qm = qm;
- }
-
- public boolean shouldImport() {
- if (qm.getLicenseGroups().getTotal() > 0) {
- return false;
- }
- return true;
- }
-
- public void loadDefaults() throws IOException {
- final File defaultsFile = new File(URLDecoder.decode(getClass().getProtectionDomain().getCodeSource().getLocation().getPath(), UTF_8.name()) + "default-objects/licenseGroups.json");
- final JSONArray licenseGroups = readFile(defaultsFile);
- for (int i = 0; i < licenseGroups.length(); i++) {
- final JSONObject json = licenseGroups.getJSONObject(i);
- final LicenseGroup licenseGroup = new LicenseGroup();
- licenseGroup.setName(json.getString("name"));
- licenseGroup.setRiskWeight(json.getInt("riskWeight"));
- LOGGER.debug("Adding " + licenseGroup.getName());
- final List licenses = new ArrayList<>();
- for (int k=0; k T getValue(final ConfigPropertyConstants property, final Class cl
return getOptionalValue(property, clazz).orElseThrow(NoSuchElementException::new);
}
+ @SqlUpdate("""
+ UPDATE "CONFIGPROPERTY"
+ SET "PROPERTYVALUE" = :value
+ WHERE "GROUPNAME" = :groupName
+ AND "PROPERTYNAME" = :propertyName
+ """)
+ void setValue(@BindBean ConfigPropertyConstants property, @Bind String value);
+
}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java
index 25b387ed53..916eff5952 100644
--- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/JdbiFactory.java
@@ -154,6 +154,10 @@ public static Jdbi createLocalJdbi(final QueryManager qm) {
return createLocalJdbi(qm.getPersistenceManager());
}
+ public static Jdbi createLocalJdbi(final DataSource dataSource) {
+ return customizeJdbi(Jdbi.create(dataSource));
+ }
+
private static Jdbi createLocalJdbi(final PersistenceManager pm) {
if (!pm.currentTransaction().isActive()) {
throw new IllegalStateException("""
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/MetricsDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/MetricsDao.java
index 21ac7a32d2..a58ad468dc 100644
--- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/MetricsDao.java
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/MetricsDao.java
@@ -21,6 +21,9 @@
import org.dependencytrack.model.DependencyMetrics;
import org.dependencytrack.model.PortfolioMetrics;
import org.dependencytrack.model.ProjectMetrics;
+import org.dependencytrack.persistence.pagination.Page;
+import org.jdbi.v3.core.mapper.reflect.ConstructorMapper;
+import org.jdbi.v3.core.statement.Query;
import org.jdbi.v3.sqlobject.SqlObject;
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
import org.jdbi.v3.sqlobject.customizer.Bind;
@@ -35,21 +38,170 @@
import java.util.Collection;
import java.util.List;
+import static org.dependencytrack.persistence.pagination.PageUtil.decodePageToken;
+import static org.dependencytrack.persistence.pagination.PageUtil.encodePageToken;
+
/**
* @since 5.6.0
*/
public interface MetricsDao extends SqlObject {
+ record ListVulnerabilityMetricsPageToken(int year, int month) {
+ }
+
+ record ListVulnerabilityMetricsRow(int year, int month, int count, Instant measuredAt) {
+ }
+
+ default Page getVulnerabilityMetrics(final int limit, final String pageToken) {
+ final var decodedPageToken = decodePageToken(getHandle(), pageToken, ListVulnerabilityMetricsPageToken.class);
+
+ final Query query = getHandle().createQuery(/* language=InjectedFreeMarker */ """
+ <#-- @ftlvariable name="year" type="Boolean" -->
+ <#-- @ftlvariable name="month" type="Boolean" -->
+ SELECT *
+ FROM "VULNERABILITYMETRICS"
+ WHERE TRUE
+ <#if year && month>
+ AND ("YEAR", "MONTH") > (:year, :month)
+ #if>
+ ORDER BY "YEAR" ASC, "MONTH" ASC
+ LIMIT :limit
+ """);
+
+ final List rows = query
+ .bind("year", decodedPageToken != null
+ ? decodedPageToken.year()
+ : null)
+ .bind("month", decodedPageToken != null
+ ? decodedPageToken.month()
+ : null)
+ .bind("limit", limit + 1)
+ .defineNamedBindings()
+ .map(ConstructorMapper.of(ListVulnerabilityMetricsRow.class))
+ .list();
+
+ final List resultRows = rows.size() > 1
+ ? rows.subList(0, Math.min(rows.size(), limit))
+ : rows;
+
+ final ListVulnerabilityMetricsPageToken nextPageToken = rows.size() > limit
+ ? new ListVulnerabilityMetricsPageToken(resultRows.getLast().year, resultRows.getLast().month)
+ : null;
+
+ return new Page<>(resultRows, encodePageToken(getHandle(), nextPageToken));
+ }
+
+ /**
+ * Note that generate_series is invoked with integers rather
+ * than dates, because the query planner tends to overestimate
+ * rows with the latter approach.
+ *
+ * @see generate_series quirk
+ */
@SqlQuery("""
- SELECT * FROM "PORTFOLIOMETRICS"
- WHERE "LAST_OCCURRENCE" >= :since
- ORDER BY "LAST_OCCURRENCE" ASC
+ WITH
+ date_range AS(
+ SELECT DATE_TRUNC('day', CURRENT_DATE - (INTERVAL '1 day' * day)) AS metrics_date
+ FROM GENERATE_SERIES(0, GREATEST(:days - 1, 0)) day
+ ),
+ projects_in_scope AS(
+ SELECT "ID"
+ FROM "PROJECT"
+ WHERE "INACTIVE_SINCE" IS NULL
+ AND ${apiProjectAclCondition}
+ ),
+ latest_daily_project_metrics AS(
+ SELECT date_range.metrics_date
+ , latest_metrics.*
+ FROM date_range
+ LEFT JOIN LATERAL (
+ SELECT DISTINCT ON (pm."PROJECT_ID")
+ pm.*
+ FROM projects_in_scope
+ INNER JOIN "PROJECTMETRICS" pm
+ ON pm."PROJECT_ID" = projects_in_scope."ID"
+ WHERE pm."LAST_OCCURRENCE" < date_range.metrics_date + INTERVAL '1 day'
+ AND pm."LAST_OCCURRENCE" >= DATE_TRUNC('day', CURRENT_DATE - (INTERVAL '1 day' * GREATEST(:days - 1, 0)))
+ ORDER BY pm."PROJECT_ID", pm."LAST_OCCURRENCE" DESC
+ ) AS latest_metrics ON TRUE
+ ),
+ daily_metrics AS(
+ SELECT COUNT(DISTINCT "PROJECT_ID") AS projects
+ , SUM("COMPONENTS") AS components
+ , SUM("CRITICAL") AS critical
+ , metrics_date
+ , SUM("FINDINGS_AUDITED") AS findings_audited
+ , SUM("FINDINGS_TOTAL") AS findings_total
+ , SUM("FINDINGS_UNAUDITED") AS findings_unaudited
+ , SUM("HIGH") AS high
+ , SUM("RISKSCORE") as inherited_risk_score
+ , SUM("LOW") AS low
+ , SUM("MEDIUM") AS medium
+ , SUM("POLICYVIOLATIONS_AUDITED") AS policy_violations_audited
+ , SUM("POLICYVIOLATIONS_FAIL") AS policy_violations_fail
+ , SUM("POLICYVIOLATIONS_INFO") AS policy_violations_info
+ , SUM("POLICYVIOLATIONS_LICENSE_AUDITED") AS policy_violations_license_audited
+ , SUM("POLICYVIOLATIONS_LICENSE_TOTAL") AS policy_violations_license_total
+ , SUM("POLICYVIOLATIONS_LICENSE_UNAUDITED") AS policy_violations_license_unaudited
+ , SUM("POLICYVIOLATIONS_OPERATIONAL_AUDITED") AS policy_violations_operational_audited
+ , SUM("POLICYVIOLATIONS_OPERATIONAL_TOTAL") AS policy_violations_operational_total
+ , SUM("POLICYVIOLATIONS_OPERATIONAL_UNAUDITED") AS policy_violations_operational_unaudited
+ , SUM("POLICYVIOLATIONS_SECURITY_AUDITED") AS policy_violations_security_audited
+ , SUM("POLICYVIOLATIONS_SECURITY_TOTAL") AS policy_violations_security_total
+ , SUM("POLICYVIOLATIONS_SECURITY_UNAUDITED") AS policy_violations_security_unaudited
+ , SUM("POLICYVIOLATIONS_TOTAL") AS policy_violations_total
+ , SUM("POLICYVIOLATIONS_UNAUDITED") AS policy_violations_unaudited
+ , SUM("POLICYVIOLATIONS_WARN") AS policy_violations_warn
+ , SUM("SUPPRESSED") AS suppressed
+ , SUM("UNASSIGNED_SEVERITY") AS unassigned
+ , SUM("VULNERABILITIES") AS vulnerabilities
+ , SUM("VULNERABLECOMPONENTS") AS vulnerable_components
+ , SUM(CASE WHEN "VULNERABLECOMPONENTS" > 0 THEN 1 ELSE 0 END) AS vulnerable_projects
+ FROM latest_daily_project_metrics
+ GROUP BY metrics_date
+ )
+ SELECT COALESCE(dm.components, 0) AS components
+ , COALESCE(dm.critical, 0) AS critical
+ , COALESCE(dm.findings_audited, 0) AS findings_audited
+ , COALESCE(dm.findings_total, 0) AS findings_total
+ , COALESCE(dm.findings_unaudited, 0) AS findings_unaudited
+ , date_range.metrics_date AS first_occurrence
+ , COALESCE(dm.high, 0) AS high
+ , COALESCE(dm.inherited_risk_score, 0) AS inherited_risk_score
+ , date_range.metrics_date AS last_occurrence
+ , COALESCE(dm.low, 0) AS low
+ , COALESCE(dm.medium, 0) AS medium
+ , COALESCE(dm.policy_violations_audited, 0) AS policy_violations_audited
+ , COALESCE(dm.policy_violations_fail, 0) AS policy_violations_fail
+ , COALESCE(dm.policy_violations_info, 0) AS policy_violations_info
+ , COALESCE(dm.policy_violations_license_audited, 0) AS policy_violations_license_audited
+ , COALESCE(dm.policy_violations_license_total, 0) AS policy_violations_license_total
+ , COALESCE(dm.policy_violations_license_unaudited, 0) AS policy_violations_license_unaudited
+ , COALESCE(dm.policy_violations_operational_audited, 0) AS policy_violations_operational_audited
+ , COALESCE(dm.policy_violations_operational_total, 0) AS policy_violations_operational_total
+ , COALESCE(dm.policy_violations_operational_unaudited, 0) AS policy_violations_operational_unaudited
+ , COALESCE(dm.policy_violations_security_audited, 0) AS policy_violations_security_audited
+ , COALESCE(dm.policy_violations_security_total, 0) AS policy_violations_security_total
+ , COALESCE(dm.policy_violations_security_unaudited, 0) AS policy_violations_security_unaudited
+ , COALESCE(dm.policy_violations_total, 0) AS policy_violations_total
+ , COALESCE(dm.policy_violations_unaudited, 0) AS policy_violations_unaudited
+ , COALESCE(dm.policy_violations_warn, 0) AS policy_violations_warn
+ , COALESCE(dm.projects, 0) AS projects
+ , COALESCE(dm.suppressed, 0) AS suppressed
+ , COALESCE(dm.unassigned, 0) AS unassigned
+ , COALESCE(dm.vulnerabilities, 0) AS vulnerabilities
+ , COALESCE(dm.vulnerable_components, 0) AS vulnerable_components
+ , COALESCE(dm.vulnerable_projects, 0) AS vulnerable_projects
+ FROM date_range
+ LEFT JOIN daily_metrics AS dm
+ ON date_range.metrics_date = dm.metrics_date
+ ORDER BY date_range.metrics_date;
""")
@RegisterBeanMapper(PortfolioMetrics.class)
- List getPortfolioMetricsSince(@Bind Instant since);
+ List getPortfolioMetricsForDays(@Bind int days);
@SqlQuery("""
- SELECT * FROM "PROJECTMETRICS"
+ SELECT *, "RISKSCORE" AS inherited_risk_score FROM "PROJECTMETRICS"
WHERE "PROJECT_ID" = :projectId
AND "LAST_OCCURRENCE" >= :since
ORDER BY "LAST_OCCURRENCE" ASC
@@ -58,7 +210,7 @@ public interface MetricsDao extends SqlObject {
List getProjectMetricsSince(@Bind long projectId, @Bind Instant since);
@SqlQuery("""
- SELECT * FROM "DEPENDENCYMETRICS"
+ SELECT *, "RISKSCORE" AS inherited_risk_score FROM "DEPENDENCYMETRICS"
WHERE "COMPONENT_ID" = :componentId
AND "LAST_OCCURRENCE" >= :since
ORDER BY "LAST_OCCURRENCE" ASC
@@ -66,17 +218,14 @@ public interface MetricsDao extends SqlObject {
@RegisterBeanMapper(DependencyMetrics.class)
List getDependencyMetricsSince(@Bind long componentId, @Bind Instant since);
- @SqlQuery("""
- SELECT *
- FROM "PORTFOLIOMETRICS"
- ORDER BY "LAST_OCCURRENCE" DESC
- LIMIT 1
- """)
- @RegisterBeanMapper(PortfolioMetrics.class)
- PortfolioMetrics getMostRecentPortfolioMetrics();
+ default PortfolioMetrics getMostRecentPortfolioMetrics() {
+ // Request metrics since yesterday, such that we cater for projects that do
+ // not have fresh metrics from today yet.
+ return getPortfolioMetricsForDays(2).getLast();
+ }
@SqlQuery("""
- SELECT *
+ SELECT *, "RISKSCORE" AS inherited_risk_score
FROM "PROJECTMETRICS"
WHERE "PROJECT_ID" = :projectId
ORDER BY "LAST_OCCURRENCE" DESC
@@ -86,7 +235,7 @@ public interface MetricsDao extends SqlObject {
ProjectMetrics getMostRecentProjectMetrics(@Bind final long projectId);
@SqlQuery("""
- SELECT metrics.*
+ SELECT metrics.*, metrics."RISKSCORE" AS inherited_risk_score
FROM UNNEST(:projectIds) AS project(id)
INNER JOIN LATERAL (
SELECT *
@@ -100,7 +249,7 @@ INNER JOIN LATERAL (
List getMostRecentProjectMetrics(@Bind Collection projectIds);
@SqlQuery("""
- SELECT *
+ SELECT *, "RISKSCORE" AS inherited_risk_score
FROM "DEPENDENCYMETRICS"
WHERE "COMPONENT_ID" = :componentId
ORDER BY "LAST_OCCURRENCE" DESC
@@ -110,7 +259,7 @@ INNER JOIN LATERAL (
DependencyMetrics getMostRecentDependencyMetrics(@Bind long componentId);
@SqlQuery("""
- SELECT metrics.*
+ SELECT metrics.*, metrics."RISKSCORE" AS inherited_risk_score
FROM UNNEST(:componentIds) AS component(id)
INNER JOIN LATERAL (
SELECT *
@@ -126,21 +275,16 @@ INNER JOIN LATERAL (
@SqlQuery("""
SELECT inhrelid::regclass AS partition_name
FROM pg_inherits
- WHERE inhparent = '"PORTFOLIOMETRICS"'::regclass;
- """)
- List getPortfolioMetricsPartitions();
-
- @SqlQuery("""
- SELECT inhrelid::regclass AS partition_name
- FROM pg_inherits
- WHERE inhparent = '"PROJECTMETRICS"'::regclass;
+ WHERE inhparent = '"PROJECTMETRICS"'::regclass
+ ORDER BY partition_name;
""")
List getProjectMetricsPartitions();
@SqlQuery("""
SELECT inhrelid::regclass AS partition_name
FROM pg_inherits
- WHERE inhparent = '"DEPENDENCYMETRICS"'::regclass;
+ WHERE inhparent = '"DEPENDENCYMETRICS"'::regclass
+ ORDER BY partition_name;
""")
List getDependencyMetricsPartitions();
@@ -153,7 +297,7 @@ INNER JOIN LATERAL (
partition_name TEXT;
partition_exists BOOLEAN;
table_name TEXT;
- metric_tables TEXT[] := ARRAY['PORTFOLIOMETRICS', 'PROJECTMETRICS', 'DEPENDENCYMETRICS'];
+ metric_tables TEXT[] := ARRAY['PROJECTMETRICS', 'DEPENDENCYMETRICS'];
BEGIN
FOREACH table_name IN ARRAY metric_tables
LOOP
@@ -182,11 +326,6 @@ EXECUTE format(
""")
void createMetricsPartitionsForDate(@Define("targetDate") String targetDate, @Define("nextDate") String nextDate);
- default int deletePortfolioMetricsForRetentionDuration(Duration retentionDuration) {
- List metricsPartitions = getPortfolioMetricsPartitions();
- return dropOldPartitions(metricsPartitions, retentionDuration);
- }
-
default int deleteProjectMetricsForRetentionDuration(Duration retentionDuration) {
List metricsPartitions = getProjectMetricsPartitions();
return dropOldPartitions(metricsPartitions, retentionDuration);
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java
index de8994cc23..60b9dd0726 100644
--- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/ProjectDao.java
@@ -22,7 +22,6 @@
import alpine.persistence.PaginatedResult;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.core.type.TypeReference;
-import jakarta.annotation.Nullable;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.ProjectMetrics;
import org.dependencytrack.model.Tag;
@@ -44,6 +43,7 @@
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
+import jakarta.annotation.Nullable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Instant;
@@ -66,6 +66,7 @@ public interface ProjectDao {
@SqlQuery(/* language=InjectedFreeMarker */ """
<#-- @ftlvariable name="nameFilter" type="Boolean" -->
+ <#-- @ftlvariable name="versionFilter" type="Boolean" -->
<#-- @ftlvariable name="classifierFilter" type="Boolean" -->
<#-- @ftlvariable name="tagFilter" type="Boolean" -->
<#-- @ftlvariable name="teamFilter" type="Boolean" -->
@@ -103,37 +104,39 @@ public interface ProjectDao {
FROM "PROJECT" AS "CHILD_PROJECT"
WHERE "CHILD_PROJECT"."PARENT_PROJECT_ID" = "PROJECT"."ID")) AS "hasChildren"
<#if includeMetrics>
- , TO_JSONB("metrics") AS "metrics"
+ , (SELECT TO_JSONB(m)
+ FROM (
+ SELECT "COMPONENTS"
+ , "CRITICAL"
+ , "HIGH"
+ , "LOW"
+ , "MEDIUM"
+ , "POLICYVIOLATIONS_FAIL"
+ , "POLICYVIOLATIONS_INFO"
+ , "POLICYVIOLATIONS_LICENSE_TOTAL"
+ , "POLICYVIOLATIONS_OPERATIONAL_TOTAL"
+ , "POLICYVIOLATIONS_SECURITY_TOTAL"
+ , "POLICYVIOLATIONS_TOTAL"
+ , "POLICYVIOLATIONS_WARN"
+ , "RISKSCORE"
+ , "UNASSIGNED_SEVERITY"
+ , "VULNERABILITIES"
+ FROM "PROJECTMETRICS"
+ WHERE "PROJECTMETRICS"."PROJECT_ID" = "PROJECT"."ID"
+ ORDER BY "PROJECTMETRICS"."LAST_OCCURRENCE" DESC
+ LIMIT 1
+ ) AS m
+ ) AS "metrics"
#if>
, COUNT(*) OVER() AS "totalCount"
FROM "PROJECT"
- <#if includeMetrics>
- LEFT JOIN LATERAL (
- SELECT "COMPONENTS"
- , "CRITICAL"
- , "HIGH"
- , "LOW"
- , "MEDIUM"
- , "POLICYVIOLATIONS_FAIL"
- , "POLICYVIOLATIONS_INFO"
- , "POLICYVIOLATIONS_LICENSE_TOTAL"
- , "POLICYVIOLATIONS_OPERATIONAL_TOTAL"
- , "POLICYVIOLATIONS_SECURITY_TOTAL"
- , "POLICYVIOLATIONS_TOTAL"
- , "POLICYVIOLATIONS_WARN"
- , "RISKSCORE"
- , "UNASSIGNED_SEVERITY"
- , "VULNERABILITIES"
- FROM "PROJECTMETRICS"
- WHERE "PROJECTMETRICS"."PROJECT_ID" = "PROJECT"."ID"
- ORDER BY "PROJECTMETRICS"."LAST_OCCURRENCE" DESC
- LIMIT 1
- ) AS "metrics" ON TRUE
- #if>
WHERE ${apiProjectAclCondition}
<#if nameFilter>
AND "PROJECT"."NAME" = :nameFilter
#if>
+ <#if versionFilter>
+ AND "PROJECT"."VERSION" = :versionFilter
+ #if>
<#if classifierFilter>
AND "PROJECT"."CLASSIFIER" = :classifierFilter
#if>
@@ -194,13 +197,10 @@ OR EXISTS (SELECT 1 FROM "TAG" WHERE "TAG"."NAME" = ${apiFilterParameter}))
@AllowApiOrdering.Column(name = "isLatest"),
@AllowApiOrdering.Column(name = "lastBomImport"),
@AllowApiOrdering.Column(name = "lastBomImportFormat"),
- @AllowApiOrdering.Column(name = "metrics.components", queryName = "\"metrics\".\"COMPONENTS\""),
- @AllowApiOrdering.Column(name = "metrics.inheritedRiskScore", queryName = "\"metrics\".\"RISKSCORE\""),
- @AllowApiOrdering.Column(name = "metrics.policyViolationsTotal", queryName = "\"metrics\".\"POLICYVIOLATIONS_TOTAL\""),
- @AllowApiOrdering.Column(name = "metrics.vulnerabilities", queryName = "\"metrics\".\"VULNERABILITIES\"")
})
List getPageConcise(
@Bind String nameFilter,
+ @Bind String versionFilter,
@Bind String classifierFilter,
@Bind String tagFilter,
@Bind String teamFilter,
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java
index dced37e427..9a054557d7 100644
--- a/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/jdbi/TeamDao.java
@@ -18,10 +18,138 @@
*/
package org.dependencytrack.persistence.jdbi;
+import org.dependencytrack.persistence.pagination.Page;
+import org.jdbi.v3.core.mapper.reflect.ConstructorMapper;
+import org.jdbi.v3.core.statement.Query;
+import org.jdbi.v3.core.statement.Update;
+import org.jdbi.v3.sqlobject.SqlObject;
import org.jdbi.v3.sqlobject.customizer.Bind;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
-public interface TeamDao {
+import java.util.Collection;
+import java.util.List;
+
+import static org.dependencytrack.persistence.pagination.PageUtil.decodePageToken;
+import static org.dependencytrack.persistence.pagination.PageUtil.encodePageToken;
+
+public interface TeamDao extends SqlObject {
+
+ record ListTeamsPageToken(String lastName) {
+ }
+
+ record ListTeamsRow(String name, int apiKeys, int members) {
+ }
+
+ default Page listTeams(final int limit, final String pageToken) {
+ final var decodedPageToken = decodePageToken(getHandle(), pageToken, ListTeamsPageToken.class);
+
+ final Query query = getHandle().createQuery(/* language=InjectedFreeMarker */ """
+ <#-- @ftlvariable name="lastName" type="Boolean" -->
+ SELECT "NAME" AS name
+ , (SELECT COUNT(*) FROM "APIKEYS_TEAMS" WHERE "TEAM_ID" = "TEAM"."ID") AS api_keys
+ , (SELECT COUNT(*) FROM "USERS_TEAMS" WHERE "TEAM_ID" = "TEAM"."ID") AS members
+ FROM "TEAM"
+ WHERE TRUE
+ <#if lastName>
+ AND "NAME" > :lastName
+ #if>
+ ORDER BY "NAME"
+ LIMIT :limit
+ """);
+
+ final List rows = query
+ .bind("lastName", decodedPageToken != null
+ ? decodedPageToken.lastName()
+ : null)
+ .bind("limit", limit + 1)
+ .defineNamedBindings()
+ .map(ConstructorMapper.of(ListTeamsRow.class))
+ .list();
+
+ final List resultRows = rows.size() > 1
+ ? rows.subList(0, Math.min(rows.size(), limit))
+ : rows;
+
+ final ListTeamsPageToken nextPageToken = rows.size() > limit
+ ? new ListTeamsPageToken(resultRows.getLast().name())
+ : null;
+
+ return new Page<>(resultRows, encodePageToken(getHandle(), nextPageToken));
+ }
+
+ record ListTeamMembershipsPageToken(String lastTeamName, String lastUsername) {
+ }
+
+ record ListTeamMembershipsRow(String teamName, String username) {
+ }
+
+ default Page listTeamMembers(
+ final String teamName,
+ final String username,
+ final int limit,
+ final String pageToken) {
+ final var decodedPageToken = decodePageToken(getHandle(), pageToken, ListTeamMembershipsPageToken.class);
+
+ final Query query = getHandle().createQuery(/* language=InjectedFreeMarker */ """
+ <#-- @ftlvariable name="teamName" type="Boolean" -->
+ <#-- @ftlvariable name="username" type="Boolean" -->
+ <#-- @ftlvariable name="lastTeamName" type="Boolean" -->
+ <#-- @ftlvariable name="lastUsername" type="Boolean" -->
+ SELECT t."NAME" AS team_name
+ , u."USERNAME" AS username
+ FROM "USERS_TEAMS" AS ut
+ INNER JOIN "TEAM" AS t
+ ON t."ID" = ut."TEAM_ID"
+ INNER JOIN "USER" AS u
+ ON u."ID" = ut."USER_ID"
+ WHERE TRUE
+ <#if teamName>
+ AND t."NAME" = :teamName
+ #if>
+ <#if username>
+ AND u."USERNAME" = :username
+ #if>
+ <#if lastTeamName && lastUsername>
+ AND (t."NAME", u."USERNAME") > (:lastTeamName, :lastUsername)
+ #if>
+ ORDER BY t."NAME", u."USERNAME"
+ LIMIT :limit
+ """);
+
+ final List rows = query
+ .bind("teamName", teamName)
+ .bind("username", username)
+ .bind("lastTeamName", decodedPageToken != null
+ ? decodedPageToken.lastTeamName()
+ : null)
+ .bind("lastUsername", decodedPageToken != null
+ ? decodedPageToken.lastUsername()
+ : null)
+ .bind("limit", limit + 1)
+ .defineNamedBindings()
+ .map(ConstructorMapper.of(ListTeamMembershipsRow.class))
+ .list();
+
+ final List resultRows = rows.size() > 1
+ ? rows.subList(0, Math.min(rows.size(), limit))
+ : rows;
+
+ final ListTeamMembershipsPageToken nextPageToken = rows.size() > limit
+ ? new ListTeamMembershipsPageToken(
+ resultRows.getLast().teamName(),
+ resultRows.getLast().username())
+ : null;
+
+ return new Page<>(resultRows, encodePageToken(getHandle(), nextPageToken));
+ }
+
+ @SqlUpdate("""
+ DELETE
+ FROM "USERS_TEAMS"
+ WHERE "TEAM_ID" = (SELECT "ID" FROM "TEAM" WHERE "NAME" = :teamName)
+ AND "USER_ID" = (SELECT "ID" FROM "USER" WHERE "USERNAME" = :username)
+ """)
+ boolean deleteTeamMembership(@Bind String teamName, @Bind String username);
@SqlUpdate("""
DELETE
@@ -29,4 +157,19 @@ public interface TeamDao {
WHERE "ID" = :teamId
""")
int deleteTeam(@Bind final long teamId);
+
+ default List deleteTeamsByName(final Collection names) {
+ final Update update = getHandle().createUpdate("""
+ DELETE
+ FROM "TEAM"
+ WHERE "NAME" = ANY(:names)
+ RETURNING "NAME"
+ """);
+
+ return update
+ .bindArray("names", String.class, names)
+ .executeAndReturnGeneratedKeys()
+ .mapTo(String.class)
+ .list();
+ }
}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/pagination/InvalidPageTokenException.java b/apiserver/src/main/java/org/dependencytrack/persistence/pagination/InvalidPageTokenException.java
new file mode 100644
index 0000000000..359722fd4a
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/pagination/InvalidPageTokenException.java
@@ -0,0 +1,27 @@
+/*
+ * 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.persistence.pagination;
+
+public class InvalidPageTokenException extends IllegalArgumentException {
+
+ public InvalidPageTokenException(final Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/defaults/IDefaultObjectImporter.java b/apiserver/src/main/java/org/dependencytrack/persistence/pagination/Page.java
similarity index 78%
rename from apiserver/src/main/java/org/dependencytrack/persistence/defaults/IDefaultObjectImporter.java
rename to apiserver/src/main/java/org/dependencytrack/persistence/pagination/Page.java
index f2971a7ff9..dd62e1c52e 100644
--- a/apiserver/src/main/java/org/dependencytrack/persistence/defaults/IDefaultObjectImporter.java
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/pagination/Page.java
@@ -16,14 +16,9 @@
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/
-package org.dependencytrack.persistence.defaults;
+package org.dependencytrack.persistence.pagination;
-import java.io.IOException;
-
-public interface IDefaultObjectImporter {
-
- boolean shouldImport();
-
- void loadDefaults() throws IOException;
+import java.util.List;
+public record Page(List items, String nextPageToken) {
}
diff --git a/apiserver/src/main/java/org/dependencytrack/persistence/pagination/PageUtil.java b/apiserver/src/main/java/org/dependencytrack/persistence/pagination/PageUtil.java
new file mode 100644
index 0000000000..02377594df
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/persistence/pagination/PageUtil.java
@@ -0,0 +1,88 @@
+/*
+ * 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.persistence.pagination;
+
+import alpine.security.crypto.DataEncryption;
+import org.dependencytrack.api.v2.model.PaginationLinks;
+import org.dependencytrack.api.v2.model.PaginationMetadata;
+import org.jdbi.v3.core.Handle;
+import org.jdbi.v3.json.JsonConfig;
+import org.jdbi.v3.json.JsonMapper;
+
+import jakarta.ws.rs.core.UriInfo;
+import java.util.Base64;
+
+/**
+ * @since 5.6.0
+ */
+public final class PageUtil {
+
+ private PageUtil() {}
+
+ public static T decodePageToken(final Handle handle, final String encodedToken, final Class tokenClass) {
+ if (encodedToken == null) {
+ return null;
+ }
+
+ final JsonMapper.TypedJsonMapper jsonMapper = handle
+ .getConfig(JsonConfig.class)
+ .getJsonMapper()
+ .forType(tokenClass, handle.getConfig());
+
+ try {
+ final byte[] encryptedTokenBytes = Base64.getUrlDecoder().decode(encodedToken);
+ final byte[] decryptedToken = DataEncryption.decryptAsBytes(encryptedTokenBytes);
+ return (T) jsonMapper.fromJson(new String(decryptedToken), handle.getConfig());
+ } catch (Exception e) {
+ throw new InvalidPageTokenException(e);
+ }
+ }
+
+ public static String encodePageToken(final Handle handle, final T pageToken) {
+ if (pageToken == null) {
+ return null;
+ }
+
+ final JsonMapper.TypedJsonMapper jsonMapper = handle
+ .getConfig(JsonConfig.class)
+ .getJsonMapper()
+ .forType(Object.class, handle.getConfig());
+
+ try {
+ final String tokenJson = jsonMapper.toJson(pageToken, handle.getConfig());
+ final byte[] encryptedTokenBytes = DataEncryption.encryptAsBytes(tokenJson);
+ return Base64.getUrlEncoder().encodeToString(encryptedTokenBytes);
+ } catch (Exception e) {
+ throw new InvalidPageTokenException(e);
+ }
+ }
+
+ public static PaginationMetadata createPaginationMetadata(final UriInfo uriInfo, final Page> page) {
+ return PaginationMetadata.builder()
+ .links(PaginationLinks.builder()
+ .self(uriInfo.getRequestUri())
+ .next(page.nextPageToken() != null ?
+ uriInfo.getRequestUriBuilder()
+ .replaceQueryParam("page_token", page.nextPageToken())
+ .build()
+ : null)
+ .build())
+ .build();
+ }
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/plugin/PluginInitializer.java b/apiserver/src/main/java/org/dependencytrack/plugin/PluginInitializer.java
index 5449f8f313..5751f325ff 100644
--- a/apiserver/src/main/java/org/dependencytrack/plugin/PluginInitializer.java
+++ b/apiserver/src/main/java/org/dependencytrack/plugin/PluginInitializer.java
@@ -18,9 +18,7 @@
*/
package org.dependencytrack.plugin;
-import alpine.Config;
import alpine.common.logging.Logger;
-import org.dependencytrack.common.ConfigKey;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
@@ -36,12 +34,6 @@ public class PluginInitializer implements ServletContextListener {
@Override
public void contextInitialized(final ServletContextEvent event) {
- if (Config.getInstance().getPropertyAsBoolean(ConfigKey.INIT_AND_EXIT)) {
- LOGGER.debug("Not loading plugins because %s is enabled"
- .formatted(ConfigKey.INIT_AND_EXIT.getPropertyName()));
- return;
- }
-
LOGGER.info("Loading plugins");
pluginManager.loadPlugins();
}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java
index 38602a65f4..e0865ab75a 100644
--- a/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/BomResource.java
@@ -24,13 +24,14 @@
import alpine.notification.Notification;
import alpine.notification.NotificationLevel;
import alpine.server.auth.PermissionRequired;
+import alpine.server.filters.ResourceAccessRequired;
+import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.SignatureException;
-import alpine.server.filters.ResourceAccessRequired;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
@@ -40,23 +41,6 @@
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.json.Json;
-import jakarta.json.JsonArray;
-import jakarta.json.JsonReader;
-import jakarta.json.JsonString;
-import jakarta.validation.Validator;
-import jakarta.ws.rs.Consumes;
-import jakarta.ws.rs.DefaultValue;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.PUT;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.QueryParam;
-import jakarta.ws.rs.WebApplicationException;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BOMInputStream;
import org.apache.commons.lang3.StringUtils;
@@ -93,8 +77,23 @@
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.owasp.security.logging.SecurityMarkers;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
+import jakarta.json.Json;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonReader;
+import jakarta.json.JsonString;
+import jakarta.validation.Validator;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
@@ -107,6 +106,8 @@
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
+
+import static java.util.Objects.requireNonNull;
import static java.util.function.Predicate.not;
import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_MODE;
import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE;
@@ -314,13 +315,14 @@ public Response exportComponentAsCycloneDx(
@ResourceAccessRequired
public Response uploadBom(@Parameter(required = true) BomSubmitRequest request) {
final Validator validator = getValidator();
+ final ProcessingResult processingResult;
if (request.getProject() != null) { // behavior in v3.0.0
failOnValidationError(
validator.validateProperty(request, "project"),
validator.validateProperty(request, "bom")
);
try (QueryManager qm = new QueryManager()) {
- return qm.callInTransaction(() -> {
+ processingResult = qm.callInTransaction(() -> {
final Project project = qm.getObjectByUuid(Project.class, request.getProject());
return process(qm, project, request.getBom());
});
@@ -332,7 +334,7 @@ public Response uploadBom(@Parameter(required = true) BomSubmitRequest request)
validator.validateProperty(request, "bom")
);
try (final var qm = new QueryManager()) {
- return qm.callInTransaction(() -> {
+ processingResult = qm.callInTransaction(() -> {
Project project = qm.getProject(request.getProjectName(), request.getProjectVersion());
if (project == null && request.isAutoCreate()) {
if (hasPermission(Permissions.Constants.PORTFOLIO_MANAGEMENT) || hasPermission(Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE) || hasPermission(Permissions.Constants.PROJECT_CREATION_UPLOAD)) {
@@ -352,20 +354,27 @@ public Response uploadBom(@Parameter(required = true) BomSubmitRequest request)
}
if (parent == null) { // if parent project is specified but not found
- return Response.status(Response.Status.NOT_FOUND).entity("The parent project could not be found.").build();
+ final var response = Response.status(Response.Status.NOT_FOUND).entity("The parent project could not be found.").build();
+ return new ProcessingResult(response, null);
}
requireAccess(qm, parent, "Access to the specified parent project is forbidden");
}
createNewProject(request.getProjectName(), request.getProjectVersion(), request.getProjectTags(), parent, request.isLatestProjectVersion(), null);
} else {
- return Response.status(Response.Status.UNAUTHORIZED)
- .entity("The principal does not have permission to create project.").build();
+ final var response = Response.status(Response.Status.UNAUTHORIZED).entity("The principal does not have permission to create project.").build();
+ return new ProcessingResult(response, null);
}
}
return process(qm, project, request.getBom());
});
}
}
+
+ if (processingResult.event() != null) {
+ Event.dispatch(processingResult.event());
+ }
+
+ return processingResult.response();
}
@POST
@@ -540,16 +549,17 @@ public Response uploadBom(
@DefaultValue("false") @FormDataParam("isLatest") boolean isLatest,
@Parameter(schema = @Schema(type = "string")) @FormDataParam("bom") final List artifactParts
) {
+ final ProcessingResult processingResult;
if (projectUuid != null) { // behavior in v3.0.0
try (QueryManager qm = new QueryManager()) {
- return qm.callInTransaction(() -> {
+ processingResult = qm.callInTransaction(() -> {
final Project project = qm.getObjectByUuid(Project.class, projectUuid);
return process(qm, project, artifactParts);
});
}
} else { // additional behavior added in v3.1.0
try (QueryManager qm = new QueryManager()) {
- return qm.callInTransaction(() -> {
+ processingResult = qm.callInTransaction(() -> {
final String trimmedProjectName = StringUtils.trimToNull(projectName);
final String trimmedProjectVersion = StringUtils.trimToNull(projectVersion);
Project project = qm.getProject(trimmedProjectName, trimmedProjectVersion);
@@ -567,7 +577,8 @@ public Response uploadBom(
}
if (parent == null) { // if parent project is specified but not found
- return Response.status(Response.Status.NOT_FOUND).entity("The parent project could not be found.").build();
+ final var response = Response.status(Response.Status.NOT_FOUND).entity("The parent project could not be found.").build();
+ return new ProcessingResult(response, null);
}
requireAccess(qm, parent, "Access to the specified parent project is forbidden");
}
@@ -576,20 +587,34 @@ public Response uploadBom(
: null;
createNewProject(projectName, projectVersion, tags, parent, isLatest, null);
} else {
- return Response.status(Response.Status.UNAUTHORIZED)
- .entity("The principal does not have permission to create project.").build();
+ final var response = Response.status(Response.Status.UNAUTHORIZED).entity("The principal does not have permission to create project.").build();
+ return new ProcessingResult(response, null);
}
}
return process(qm, project, artifactParts);
});
}
}
+
+ if (processingResult.event() != null) {
+ Event.dispatch(processingResult.event());
+ }
+
+ return processingResult.response();
+ }
+
+ private record ProcessingResult(Response response, BomUploadEvent event) {
+
+ private ProcessingResult {
+ requireNonNull(response, "response must not be null");
+ }
+
}
/**
* Common logic that processes a BOM given a project and encoded payload.
*/
- private Response process(QueryManager qm, Project project, String encodedBomData) {
+ private ProcessingResult process(QueryManager qm, Project project, String encodedBomData) {
if (project != null) {
requireAccess(qm, project);
@@ -600,25 +625,28 @@ private Response process(QueryManager qm, Project project, String encodedBomData
bomFileMetadata = validateAndStoreBom(IOUtils.toByteArray(byteOrderMarkInputStream), project);
} catch (IOException e) {
LOGGER.error("An unexpected error occurred while validating or storing a BOM uploaded to project: " + project.getUuid(), e);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
+ final var response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
+ return new ProcessingResult(response, null);
}
final BomUploadEvent bomUploadEvent = new BomUploadEvent(qm.detach(Project.class, project.getId()), bomFileMetadata);
qm.createWorkflowSteps(bomUploadEvent.getChainIdentifier());
- Event.dispatch(bomUploadEvent);
BomUploadResponse bomUploadResponse = new BomUploadResponse();
bomUploadResponse.setToken(bomUploadEvent.getChainIdentifier());
- return Response.ok(bomUploadResponse).build();
+ final var response = Response.ok(bomUploadResponse).build();
+
+ return new ProcessingResult(response, bomUploadEvent);
} else {
- return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build();
+ final var response = Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build();
+ return new ProcessingResult(response, null);
}
}
/**
* Common logic that processes a BOM given a project and list of multi-party form objects containing decoded payloads.
*/
- private Response process(QueryManager qm, Project project, List artifactParts) {
+ private ProcessingResult process(QueryManager qm, Project project, List artifactParts) {
for (final FormDataBodyPart artifactPart : artifactParts) {
final BodyPartEntity bodyPartEntity = (BodyPartEntity) artifactPart.getEntity();
if (project != null) {
@@ -630,7 +658,8 @@ private Response process(QueryManager qm, Project project, List
- handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics());
+ PortfolioMetrics metrics = withJdbiHandle(
+ getAlpineRequest(),
+ handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics());
return Response.ok(metrics).build();
}
@@ -145,13 +152,25 @@ public Response getPortfolioCurrentMetrics() {
public Response getPortfolioMetricsSince(
@Parameter(description = "The start date to retrieve metrics for", required = true)
@PathParam("date") String date) {
-
- final Date since = DateUtil.parseShortDate(date);
- if (since == null) {
+ final LocalDate since;
+ try {
+ since = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyyMMdd"));
+ } catch (DateTimeParseException e) {
return Response.status(Response.Status.BAD_REQUEST).entity("The specified date format is incorrect.").build();
}
- List metrics = withJdbiHandle(handle ->
- handle.attach(MetricsDao.class).getPortfolioMetricsSince(since.toInstant()));
+ List metrics = withJdbiHandle(getAlpineRequest(), handle -> {
+ final int retentionDays = handle.attach(ConfigPropertyDao.class)
+ .getOptionalValue(MAINTENANCE_METRICS_RETENTION_DAYS, Integer.class)
+ .orElseGet(() -> Integer.parseInt(MAINTENANCE_METRICS_RETENTION_DAYS.getDefaultPropertyValue()));
+
+ // NB: Calculate days between the given date and *tomorrow*,
+ // because LocalDate#until's end date is exclusive,
+ // and we want to include data for *today*.
+ final var sincePeriod = since.until(LocalDate.now().plusDays(1));
+ final int sinceDays = sincePeriod.getDays();
+
+ return handle.attach(MetricsDao.class).getPortfolioMetricsForDays(Math.min(retentionDays, sinceDays));
+ });
return Response.ok(metrics).build();
}
@@ -174,10 +193,14 @@ public Response getPortfolioMetricsSince(
@ResourceAccessRequired
public Response getPortfolioMetricsXDays(
@Parameter(description = "The number of days back to retrieve metrics for", required = true)
- @PathParam("days") int days) {
- final Date since = DateUtils.addDays(new Date(), -days);
- List metrics = withJdbiHandle(handle ->
- handle.attach(MetricsDao.class).getPortfolioMetricsSince(since.toInstant()));
+ @PathParam("days") @Positive int days) {
+ List metrics = withJdbiHandle(getAlpineRequest(), handle -> {
+ final int retentionDays = handle.attach(ConfigPropertyDao.class)
+ .getOptionalValue(MAINTENANCE_METRICS_RETENTION_DAYS, Integer.class)
+ .orElseGet(() -> Integer.parseInt(MAINTENANCE_METRICS_RETENTION_DAYS.getDefaultPropertyValue()));
+
+ return handle.attach(MetricsDao.class).getPortfolioMetricsForDays(Math.min(days, retentionDays));
+ });
return Response.ok(metrics).build();
}
@@ -294,7 +317,7 @@ public Response getProjectMetricsXDays(
@PathParam("uuid") @ValidUuid String uuid,
@Parameter(description = "The number of days back to retrieve metrics for", required = true)
@PathParam("days") int days) {
- final Date since = DateUtils.addDays(new Date(), -days);
+ final Date since = addDays(new Date(), -days);
return getProjectMetrics(uuid, since);
}
@@ -429,7 +452,7 @@ public Response getComponentMetricsXDays(
@PathParam("uuid") @ValidUuid String uuid,
@Parameter(description = "The number of days back to retrieve metrics for", required = true)
@PathParam("days") int days) {
- final Date since = DateUtils.addDays(new Date(), -days);
+ final Date since = addDays(new Date(), -days);
return getComponentMetrics(uuid, since);
}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java
index 888a48dd7c..7a2df295a3 100644
--- a/apiserver/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java
@@ -19,7 +19,6 @@
package org.dependencytrack.resources.v1;
import alpine.common.logging.Logger;
-import alpine.model.ConfigProperty;
import alpine.notification.Notification;
import alpine.server.auth.PermissionRequired;
import alpine.server.resources.AlpineResource;
@@ -33,32 +32,36 @@
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.validation.Validator;
-import jakarta.ws.rs.Consumes;
-import jakarta.ws.rs.DELETE;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.PUT;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.event.kafka.KafkaEventDispatcher;
-import org.dependencytrack.model.ConfigPropertyConstants;
import org.dependencytrack.model.NotificationPublisher;
import org.dependencytrack.model.NotificationRule;
import org.dependencytrack.model.validation.ValidUuid;
import org.dependencytrack.notification.NotificationConstants;
import org.dependencytrack.notification.NotificationGroup;
import org.dependencytrack.notification.publisher.PublisherClass;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.dependencytrack.persistence.QueryManager;
+import org.dependencytrack.persistence.jdbi.ConfigPropertyDao;
import org.dependencytrack.util.NotificationUtil;
+import jakarta.validation.Validator;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
import java.util.Arrays;
import java.util.List;
+import static org.dependencytrack.model.ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
+
/**
* JAX-RS resources for processing notification publishers.
*
@@ -263,21 +266,14 @@ public Response deleteNotificationPublisher(@Parameter(description = "The UUID o
})
@PermissionRequired({Permissions.Constants.SYSTEM_CONFIGURATION, Permissions.Constants.SYSTEM_CONFIGURATION_CREATE})
public Response restoreDefaultTemplates() {
- try (QueryManager qm = new QueryManager()) {
- return qm.callInTransaction(() -> {
- final ConfigProperty property = qm.getConfigProperty(
- ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getGroupName(),
- ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED.getPropertyName()
- );
- property.setPropertyValue("false");
- qm.persist(property);
- NotificationUtil.loadDefaultNotificationPublishers(qm);
- return Response.ok().build();
- });
- } catch (Exception exception) {
- LOGGER.error(exception.getMessage(), exception);
- return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Exception occured while restoring default notification publisher templates.").build();
- }
+ useJdbiTransaction(handle -> {
+ handle.attach(ConfigPropertyDao.class).setValue(
+ NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED, "false");
+
+ DatabaseSeedingInitTask.seedDefaultNotificationPublishers(handle);
+ });
+
+ return Response.ok().build();
}
@POST
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/OpenApiResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/OpenApiResource.java
similarity index 97%
rename from apiserver/src/main/java/org/dependencytrack/resources/OpenApiResource.java
rename to apiserver/src/main/java/org/dependencytrack/resources/v1/OpenApiResource.java
index f67af48a7b..9c49cd3db6 100644
--- a/apiserver/src/main/java/org/dependencytrack/resources/OpenApiResource.java
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/OpenApiResource.java
@@ -16,7 +16,7 @@
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/
-package org.dependencytrack.resources;
+package org.dependencytrack.resources.v1;
import alpine.server.auth.AuthenticationNotRequired;
import io.swagger.v3.jaxrs2.integration.resources.BaseOpenApiResource;
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java
index 87af1017d4..5b023d434b 100644
--- a/apiserver/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java
@@ -37,21 +37,6 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
-import jakarta.validation.Validator;
-import jakarta.ws.rs.ClientErrorException;
-import jakarta.ws.rs.Consumes;
-import jakarta.ws.rs.DELETE;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.PATCH;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.PUT;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.QueryParam;
-import jakarta.ws.rs.ServerErrorException;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.event.CloneProjectEvent;
@@ -72,6 +57,21 @@
import org.dependencytrack.resources.v1.vo.ConciseProject;
import org.jdbi.v3.core.Handle;
+import jakarta.validation.Validator;
+import jakarta.ws.rs.ClientErrorException;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PATCH;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.ServerErrorException;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
import javax.jdo.FetchGroup;
import java.security.Principal;
import java.util.Collection;
@@ -171,6 +171,8 @@ public Response getProjects(@Parameter(description = "The optional name of the p
public Response getProjectsConcise(
@Parameter(description = "Name to filter on. Must be exact match.")
@QueryParam("name") final String nameFilter,
+ @Parameter(description = "Version to filter on. Must be exact match.")
+ @QueryParam("version") final String versionFilter,
@Parameter(description = "Classifier to filter on. Must be exact match.")
@QueryParam("classifier") final String classifierFilter,
@Parameter(description = "Tag to filter on. Must be exact match.")
@@ -185,7 +187,7 @@ public Response getProjectsConcise(
@QueryParam("includeMetrics") final boolean includeMetrics
) {
final List projectRows = withJdbiHandle(getAlpineRequest(), handle -> handle.attach(ProjectDao.class)
- .getPageConcise(nameFilter, classifierFilter, tagFilter, teamFilter, activeFilter, onlyRootFilter, /* parentUuidFilter */ null, includeMetrics));
+ .getPageConcise(nameFilter, versionFilter, classifierFilter, tagFilter, teamFilter, activeFilter, onlyRootFilter, /* parentUuidFilter */ null, includeMetrics));
final long totalCount = projectRows.isEmpty() ? 0 : projectRows.getFirst().totalCount();
final List projects = projectRows.stream().map(ConciseProject::new).toList();
@@ -216,6 +218,8 @@ public Response getProjectChildrenConcise(
@PathParam("uuid") final String parentUuid,
@Parameter(description = "Name to filter on. Must be exact match.")
@QueryParam("name") final String nameFilter,
+ @Parameter(description = "Version to filter on. Must be exact match.")
+ @QueryParam("version") final String versionFilter,
@Parameter(description = "Classifier to filter on. Must be exact match.")
@QueryParam("classifier") final String classifierFilter,
@Parameter(description = "Tag to filter on. Must be exact match.")
@@ -228,7 +232,7 @@ public Response getProjectChildrenConcise(
@QueryParam("includeMetrics") final boolean includeMetrics
) {
final List projectRows = withJdbiHandle(getAlpineRequest(), handle -> handle.attach(ProjectDao.class)
- .getPageConcise(nameFilter, classifierFilter, tagFilter, teamFilter, activeFilter, /* onlyRootFilter */ null, UUID.fromString(parentUuid), includeMetrics));
+ .getPageConcise(nameFilter, versionFilter, classifierFilter, tagFilter, teamFilter, activeFilter, /* onlyRootFilter */ null, UUID.fromString(parentUuid), includeMetrics));
final long totalCount = projectRows.isEmpty() ? 0 : projectRows.getFirst().totalCount();
final List projects = projectRows.stream().map(ConciseProject::new).toList();
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/MetricsResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/MetricsResource.java
new file mode 100644
index 0000000000..3596f851b9
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/MetricsResource.java
@@ -0,0 +1,111 @@
+/*
+ * 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;
+
+import alpine.server.auth.PermissionRequired;
+import alpine.server.resources.AlpineResource;
+import org.dependencytrack.api.v2.MetricsApi;
+import org.dependencytrack.api.v2.model.ListVulnerabilityMetricsResponse;
+import org.dependencytrack.api.v2.model.ListVulnerabilityMetricsResponseItem;
+import org.dependencytrack.api.v2.model.PortfolioMetricsResponse;
+import org.dependencytrack.auth.Permissions;
+import org.dependencytrack.model.PortfolioMetrics;
+import org.dependencytrack.persistence.jdbi.MetricsDao;
+import org.dependencytrack.persistence.pagination.Page;
+
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriInfo;
+import jakarta.ws.rs.ext.Provider;
+
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.inJdbiTransaction;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle;
+import static org.dependencytrack.persistence.pagination.PageUtil.createPaginationMetadata;
+
+@Provider
+public class MetricsResource extends AlpineResource implements MetricsApi {
+
+ @Context
+ private UriInfo uriInfo;
+
+ @Override
+ @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO)
+ public Response getPortfolioCurrentMetrics() {
+ PortfolioMetrics metrics = withJdbiHandle(
+ getAlpineRequest(),
+ handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics());
+ final var response = PortfolioMetricsResponse.builder()
+ .components(metrics.getComponents())
+ .critical(metrics.getCritical())
+ .findingsAudited(metrics.getFindingsAudited())
+ .findingsTotal(metrics.getFindingsTotal())
+ .findingsUnaudited(metrics.getFindingsUnaudited())
+ .high(metrics.getHigh())
+ .inheritedRiskScore(metrics.getInheritedRiskScore())
+ .observedAt(metrics.getLastOccurrence().getTime())
+ .low(metrics.getLow())
+ .medium(metrics.getMedium())
+ .policyViolationsAudited(metrics.getPolicyViolationsAudited())
+ .policyViolationsFail(metrics.getPolicyViolationsFail())
+ .policyViolationsInfo(metrics.getPolicyViolationsInfo())
+ .policyViolationsLicenseAudited(metrics.getPolicyViolationsLicenseAudited())
+ .policyViolationsLicenseTotal(metrics.getPolicyViolationsLicenseTotal())
+ .policyViolationsLicenseUnaudited(metrics.getPolicyViolationsLicenseUnaudited())
+ .policyViolationsOperationalAudited(metrics.getPolicyViolationsOperationalAudited())
+ .policyViolationsOperationalTotal(metrics.getPolicyViolationsOperationalTotal())
+ .policyViolationsOperationalUnaudited(metrics.getPolicyViolationsOperationalUnaudited())
+ .policyViolationsSecurityAudited(metrics.getPolicyViolationsSecurityAudited())
+ .policyViolationsSecurityTotal(metrics.getPolicyViolationsSecurityTotal())
+ .policyViolationsSecurityUnaudited(metrics.getPolicyViolationsSecurityUnaudited())
+ .policyViolationsTotal(metrics.getPolicyViolationsTotal())
+ .policyViolationsUnaudited(metrics.getPolicyViolationsUnaudited())
+ .policyViolationsWarn(metrics.getPolicyViolationsWarn())
+ .projects(metrics.getProjects())
+ .suppressed(metrics.getSuppressed())
+ .unassigned(metrics.getUnassigned())
+ .vulnerabilities(metrics.getVulnerabilities())
+ .vulnerableComponents(metrics.getVulnerableComponents())
+ .vulnerableProjects(metrics.getVulnerableProjects())
+ .build();
+ return Response.ok(response).build();
+ }
+
+ @Override
+ @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO)
+ public Response getVulnerabilityMetrics(Integer limit, String pageToken) {
+ final Page metricsPage = inJdbiTransaction(
+ getAlpineRequest(),
+ handle -> handle.attach(MetricsDao.class).getVulnerabilityMetrics(limit, pageToken));
+
+ final var response = ListVulnerabilityMetricsResponse.builder()
+ .metrics(metricsPage.items().stream()
+ .map(
+ metricRow -> ListVulnerabilityMetricsResponseItem.builder()
+ .year(metricRow.year())
+ .month(metricRow.month())
+ .count(metricRow.count())
+ .observedAt(metricRow.measuredAt().getEpochSecond())
+ .build())
+ .toList())
+ .pagination(createPaginationMetadata(uriInfo, metricsPage))
+ .build();
+
+ return Response.ok(response).build();
+ }
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/OpenApiResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/OpenApiResource.java
new file mode 100644
index 0000000000..6c8d84677b
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/OpenApiResource.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+import alpine.server.auth.AuthenticationNotRequired;
+import io.swagger.v3.oas.annotations.Operation;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import static java.util.Objects.requireNonNull;
+
+@Path("/openapi.yaml")
+public class OpenApiResource {
+
+ private static final ReadWriteLock LOCK = new ReentrantReadWriteLock();
+ private static String OPENAPI_YAML;
+
+ @GET
+ @Produces("application/yaml")
+ @Operation(hidden = true)
+ @AuthenticationNotRequired
+ public String getOpenApi() throws IOException {
+ LOCK.readLock().lock();
+ try {
+ if (OPENAPI_YAML == null) {
+ LOCK.readLock().unlock();
+
+ LOCK.writeLock().lock();
+ try {
+ if (OPENAPI_YAML == null) {
+ OPENAPI_YAML = loadOpenapiYaml();
+ }
+
+ LOCK.readLock().lock();
+ } finally {
+ LOCK.writeLock().unlock();
+ }
+ }
+
+ return OPENAPI_YAML;
+ } finally {
+ LOCK.readLock().unlock();
+ }
+ }
+
+ private static String loadOpenapiYaml() throws IOException {
+ try (final InputStream inputStream =
+ OpenApiResource.class.getResourceAsStream(
+ "/org/dependencytrack/api/v2/openapi.yaml")) {
+ requireNonNull(inputStream, "inputStream must not be null");
+ return new String(inputStream.readAllBytes());
+ }
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/ResourceConfig.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/ResourceConfig.java
new file mode 100644
index 0000000000..6f5741f580
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/ResourceConfig.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+import alpine.server.filters.ApiFilter;
+import alpine.server.filters.AuthenticationFeature;
+import alpine.server.filters.AuthorizationFeature;
+import alpine.server.filters.GZipInterceptor;
+import alpine.server.filters.HeaderFilter;
+import alpine.server.filters.RequestIdFilter;
+import alpine.server.filters.RequestMdcEnrichmentFilter;
+import org.dependencytrack.filters.JerseyMetricsFeature;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
+
+import static org.glassfish.jersey.server.ServerProperties.PROVIDER_PACKAGES;
+import static org.glassfish.jersey.server.ServerProperties.PROVIDER_SCANNING_RECURSIVE;
+
+/**
+ * @since 5.6.0
+ */
+public final class ResourceConfig extends org.glassfish.jersey.server.ResourceConfig {
+
+ public ResourceConfig() {
+ // Only scan the v2 package for providers, register everything else manually.
+ // This gives us more flexibility to pick-and-choose, and potentially configure
+ // specific features that do not necessarily overlap with v1.
+ property(PROVIDER_PACKAGES, getClass().getPackageName());
+ property(PROVIDER_SCANNING_RECURSIVE, true);
+
+ register(ApiFilter.class);
+ register(AuthenticationFeature.class);
+ register(AuthorizationFeature.class);
+ register(GZipInterceptor.class);
+ register(HeaderFilter.class);
+ register(JacksonFeature.withoutExceptionMappers());
+ register(JerseyMetricsFeature.class);
+ register(MultiPartFeature.class);
+ register(RequestIdFilter.class);
+ register(RequestMdcEnrichmentFilter.class);
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/TeamsResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/TeamsResource.java
new file mode 100644
index 0000000000..c4746e3aa9
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/TeamsResource.java
@@ -0,0 +1,219 @@
+/*
+ * 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;
+
+import alpine.model.Permission;
+import alpine.model.Team;
+import alpine.model.User;
+import alpine.server.auth.PermissionRequired;
+import org.dependencytrack.api.v2.TeamsApi;
+import org.dependencytrack.api.v2.model.CreateTeamMembershipRequest;
+import org.dependencytrack.api.v2.model.CreateTeamRequest;
+import org.dependencytrack.api.v2.model.GetTeamResponse;
+import org.dependencytrack.api.v2.model.ListTeamMembershipsResponse;
+import org.dependencytrack.api.v2.model.ListTeamMembershipsResponseItem;
+import org.dependencytrack.api.v2.model.ListTeamsResponse;
+import org.dependencytrack.api.v2.model.ListTeamsResponseItem;
+import org.dependencytrack.auth.Permissions;
+import org.dependencytrack.persistence.QueryManager;
+import org.dependencytrack.persistence.jdbi.TeamDao;
+import org.dependencytrack.persistence.jdbi.TeamDao.ListTeamMembershipsRow;
+import org.dependencytrack.persistence.jdbi.TeamDao.ListTeamsRow;
+import org.dependencytrack.persistence.pagination.Page;
+import org.owasp.security.logging.SecurityMarkers;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jakarta.ws.rs.ClientErrorException;
+import jakarta.ws.rs.NotFoundException;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriInfo;
+import jakarta.ws.rs.ext.Provider;
+import java.util.List;
+
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.inJdbiTransaction;
+import static org.dependencytrack.persistence.pagination.PageUtil.createPaginationMetadata;
+import static org.dependencytrack.util.PersistenceUtil.isUniqueConstraintViolation;
+
+@Provider
+public class TeamsResource implements TeamsApi {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TeamsResource.class);
+
+ @Context
+ private UriInfo uriInfo;
+
+ @Override
+ @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT)
+ public Response listTeams(final Integer limit, final String pageToken) {
+ final Page teamsPage = inJdbiTransaction(
+ handle -> handle.attach(TeamDao.class).listTeams(limit, pageToken));
+
+ final var response = ListTeamsResponse.builder()
+ .teams(teamsPage.items().stream()
+ .map(
+ teamRow -> ListTeamsResponseItem.builder()
+ .name(teamRow.name())
+ .apiKeys(teamRow.apiKeys())
+ .members(teamRow.members())
+ .build())
+ .toList())
+ .pagination(createPaginationMetadata(uriInfo, teamsPage))
+ .build();
+
+ return Response.ok(response).build();
+ }
+
+ @Override
+ @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT)
+ public Response getTeam(final String name) {
+ try (final var qm = new QueryManager()) {
+ final Team team = qm.getTeam(name);
+ if (team == null) {
+ throw new NotFoundException();
+ }
+
+ final var response = GetTeamResponse.builder()
+ .name(name)
+ .permissions(
+ team.getPermissions().stream()
+ .map(Permission::getName)
+ .sorted()
+ .toList())
+ .build();
+
+ return Response.ok(response).build();
+ }
+ }
+
+ @Override
+ @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT)
+ public Response createTeam(final CreateTeamRequest request) {
+ try (final var qm = new QueryManager()) {
+ qm.runInTransaction(() -> {
+ final List permissions =
+ qm.getPermissionsByName(request.getPermissions());
+
+ final var team = new Team();
+ team.setName(request.getName());
+ team.setPermissions(permissions);
+ qm.persist(team);
+ });
+ } catch (RuntimeException e) {
+ if (isUniqueConstraintViolation(e)) {
+ throw new ClientErrorException(Response.Status.CONFLICT);
+ }
+
+ throw e;
+ }
+
+ LOGGER.info(
+ SecurityMarkers.SECURITY_AUDIT,
+ "Team created: {}", request.getName());
+ return Response
+ .created(uriInfo.getBaseUriBuilder()
+ .path("/teams")
+ .path(request.getName())
+ .build())
+ .build();
+ }
+
+ @Override
+ @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT)
+ public Response deleteTeam(final String name) {
+ final List deletedTeamNames = inJdbiTransaction(
+ handle -> handle.attach(TeamDao.class).deleteTeamsByName(List.of(name)));
+ if (deletedTeamNames.isEmpty()) {
+ throw new NotFoundException();
+ }
+
+ LOGGER.info(SecurityMarkers.SECURITY_AUDIT, "Team deleted: {}", name);
+ return Response.noContent().build();
+ }
+
+ @Override
+ @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT)
+ public Response listTeamMemberships(final String team, final String user, final Integer limit, final String pageToken) {
+ final Page membershipsPage = inJdbiTransaction(
+ handle -> handle.attach(TeamDao.class).listTeamMembers(team, user, limit, pageToken));
+
+ final var response = ListTeamMembershipsResponse.builder()
+ .memberships(membershipsPage.items().stream()
+ .map(
+ membershipRow -> ListTeamMembershipsResponseItem.builder()
+ .teamName(membershipRow.teamName())
+ .username(membershipRow.username())
+ .build())
+ .toList())
+ .pagination(createPaginationMetadata(uriInfo, membershipsPage))
+ .build();
+
+ return Response.ok(response).build();
+ }
+
+ @Override
+ @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT)
+ public Response createTeamMembership(final CreateTeamMembershipRequest request) {
+ try (final var qm = new QueryManager()) {
+ qm.runInTransaction(() -> {
+ final Team team = qm.getTeam(request.getTeamName());
+ if (team == null) {
+ throw new NotFoundException();
+ }
+
+ final User user = qm.getUser(request.getUsername());
+ if (user == null) {
+ throw new NotFoundException();
+ }
+
+ team.getUsers().add(user);
+ user.getTeams().add(team);
+ });
+ } catch (RuntimeException e) {
+ if (isUniqueConstraintViolation(e)) {
+ throw new ClientErrorException(Response.Status.CONFLICT);
+ }
+
+ throw e;
+ }
+
+ LOGGER.info(
+ SecurityMarkers.SECURITY_AUDIT,
+ "Team membership created: team={}, user={}",
+ request.getTeamName(),
+ request.getUsername());
+ return Response.created(null).build();
+ }
+
+ @Override
+ @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT)
+ public Response deleteTeamMembership(final String team, final String user) {
+ final boolean deleted = inJdbiTransaction(
+ handle -> handle.attach(TeamDao.class).deleteTeamMembership(team, user));
+ if (!deleted) {
+ throw new NotFoundException();
+ }
+
+ LOGGER.info(
+ SecurityMarkers.SECURITY_AUDIT,
+ "Team membership deleted: team={}, user={}", team, user);
+ return Response.noContent().build();
+ }
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/WorkflowsResource.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/WorkflowsResource.java
new file mode 100644
index 0000000000..4c730f027e
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/WorkflowsResource.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+import alpine.server.auth.PermissionRequired;
+import org.dependencytrack.api.v2.WorkflowsApi;
+import org.dependencytrack.api.v2.model.ListWorkflowStatesResponse;
+import org.dependencytrack.api.v2.model.ListWorkflowStatesResponseItem;
+import org.dependencytrack.auth.Permissions;
+import org.dependencytrack.model.WorkflowState;
+import org.dependencytrack.persistence.QueryManager;
+
+import jakarta.ws.rs.NotFoundException;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.Provider;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Provider
+public class WorkflowsResource implements WorkflowsApi {
+
+ @Override
+ @PermissionRequired(Permissions.Constants.BOM_UPLOAD)
+ public Response getWorkflowStates(final UUID token) {
+ List workflowStates;
+ try (final var qm = new QueryManager()) {
+ workflowStates = qm.getAllWorkflowStatesForAToken(token);
+ if (workflowStates.isEmpty()) {
+ throw new NotFoundException();
+ }
+ }
+ List states = workflowStates.stream()
+ .map(this::mapWorkflowStateResponse)
+ .collect(Collectors.toList());
+ return Response.ok(ListWorkflowStatesResponse.builder().states(states).build()).build();
+ }
+
+ private ListWorkflowStatesResponseItem mapWorkflowStateResponse(WorkflowState workflowState) {
+ var mappedState = ListWorkflowStatesResponseItem.builder()
+ .token(workflowState.getToken())
+ .status(ListWorkflowStatesResponseItem.StatusEnum.fromString(workflowState.getStatus().name()))
+ .step(ListWorkflowStatesResponseItem.StepEnum.fromString(workflowState.getStep().name()))
+ .failureReason(workflowState.getFailureReason())
+ .build();
+ if (workflowState.getStartedAt() != null) {
+ mappedState.setStartedAt(workflowState.getStartedAt().getTime());
+ }
+ if (workflowState.getUpdatedAt() != null) {
+ mappedState.setUpdatedAt(workflowState.getUpdatedAt().getTime());
+ }
+ return mappedState;
+ }
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ClientErrorExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ClientErrorExceptionMapper.java
new file mode 100644
index 0000000000..b01a155a64
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ClientErrorExceptionMapper.java
@@ -0,0 +1,50 @@
+/*
+ * 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 org.dependencytrack.api.v2.model.ProblemDetails;
+
+import jakarta.ws.rs.ClientErrorException;
+import jakarta.ws.rs.ext.Provider;
+import java.util.Map;
+
+/**
+ * @since 5.6.0
+ */
+@Provider
+public final class ClientErrorExceptionMapper extends ProblemDetailsExceptionMapper {
+
+ private static final Map DETAIL_BY_STATUS = Map.ofEntries(
+ Map.entry(401, "Not authorized to access the requested resource."),
+ Map.entry(403, "Not permitted to access the requested resource."),
+ Map.entry(404, "The requested resource could not be found."),
+ Map.entry(409, "The resource already exists."));
+
+ @Override
+ public ProblemDetails map(final ClientErrorException exception) {
+ return ProblemDetails.builder()
+ .status(exception.getResponse().getStatus())
+ .title(exception.getResponse().getStatusInfo().getReasonPhrase())
+ .detail(DETAIL_BY_STATUS.getOrDefault(
+ exception.getResponse().getStatus(),
+ exception.getMessage()))
+ .build();
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ConstraintViolationExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ConstraintViolationExceptionMapper.java
new file mode 100644
index 0000000000..a9313b19b3
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ConstraintViolationExceptionMapper.java
@@ -0,0 +1,62 @@
+/*
+ * 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 org.dependencytrack.api.v2.model.ConstraintViolationError;
+import org.dependencytrack.api.v2.model.InvalidRequestProblemDetails;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.Provider;
+import java.util.ArrayList;
+import java.util.Set;
+
+/**
+ * @since 5.6.0
+ */
+@Provider
+public final class ConstraintViolationExceptionMapper extends ProblemDetailsExceptionMapper {
+
+ @Override
+ public InvalidRequestProblemDetails map(final ConstraintViolationException exception) {
+ final Set> violations = exception.getConstraintViolations();
+
+ final var errors = new ArrayList(violations.size());
+
+ for (final ConstraintViolation> violation : violations) {
+ errors.add(
+ ConstraintViolationError.builder()
+ .path(violation.getPropertyPath().toString())
+ .value(violation.getInvalidValue() != null
+ ? violation.getInvalidValue().toString()
+ : null)
+ .message(violation.getMessage())
+ .build());
+ }
+
+ return InvalidRequestProblemDetails.builder()
+ .status(Response.Status.BAD_REQUEST.getStatusCode())
+ .title("Bad Request")
+ .detail("The request could not be processed because it failed validation.")
+ .errors(errors)
+ .build();
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/DefaultExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/DefaultExceptionMapper.java
new file mode 100644
index 0000000000..01f372f4c1
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/DefaultExceptionMapper.java
@@ -0,0 +1,30 @@
+/*
+ * 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 org.dependencytrack.api.v2.model.ProblemDetails;
+
+import jakarta.ws.rs.ext.Provider;
+
+/**
+ * @since 5.6.0
+ */
+@Provider
+public final class DefaultExceptionMapper extends LoggingProblemDetailsExceptionMapper {
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/JsonProcessingExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/JsonProcessingExceptionMapper.java
new file mode 100644
index 0000000000..95698148f0
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/JsonProcessingExceptionMapper.java
@@ -0,0 +1,48 @@
+/*
+ * 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 com.fasterxml.jackson.core.JsonGenerationException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
+import org.dependencytrack.api.v2.model.ProblemDetails;
+
+import jakarta.ws.rs.ext.Provider;
+
+/**
+ * @since 5.6.0
+ */
+@Provider
+public class JsonProcessingExceptionMapper extends LoggingProblemDetailsExceptionMapper {
+
+ @Override
+ ProblemDetails map(final JsonProcessingException exception) {
+ if (exception instanceof InvalidDefinitionException
+ || exception instanceof JsonGenerationException) {
+ return super.map(exception);
+ }
+
+ return ProblemDetails.builder()
+ .status(400)
+ .title("JSON Processing Failed")
+ .detail("The provided JSON could not be processed.")
+ .build();
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/LoggingProblemDetailsExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/LoggingProblemDetailsExceptionMapper.java
new file mode 100644
index 0000000000..4b1dfc08f9
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/LoggingProblemDetailsExceptionMapper.java
@@ -0,0 +1,50 @@
+/*
+ * 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 org.dependencytrack.api.v2.model.ProblemDetails;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jakarta.ws.rs.ServerErrorException;
+
+/**
+ * @since 5.6.0
+ */
+abstract class LoggingProblemDetailsExceptionMapper extends ProblemDetailsExceptionMapper {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ @Override
+ @SuppressWarnings("unchecked")
+ P map(final E exception) {
+ logger.error("Uncaught exception occurred during request processing", exception);
+
+ final int status = exception instanceof final ServerErrorException see
+ ? see.getResponse().getStatus()
+ : 500;
+
+ return (P) ProblemDetails.builder()
+ .status(status)
+ .title("Unexpected error")
+ .detail("An error occurred that was not anticipated.")
+ .build();
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ProblemDetailsExceptionMapper.java b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ProblemDetailsExceptionMapper.java
new file mode 100644
index 0000000000..4912108c77
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/resources/v2/exception/ProblemDetailsExceptionMapper.java
@@ -0,0 +1,44 @@
+/*
+ * 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 org.dependencytrack.api.v2.model.ProblemDetails;
+
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.ExceptionMapper;
+
+/**
+ * @since 5.6.0
+ */
+abstract class ProblemDetailsExceptionMapper implements ExceptionMapper {
+
+ abstract P map(E exception);
+
+ @Override
+ public Response toResponse(final E exception) {
+ final P problemDetails = map(exception);
+
+ return Response
+ .status(problemDetails.getStatus())
+ .header("Content-Type", "application/problem+json")
+ .entity(problemDetails)
+ .build();
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/security/KeyGenerationInitTask.java b/apiserver/src/main/java/org/dependencytrack/security/KeyGenerationInitTask.java
new file mode 100644
index 0000000000..ebd3dc067f
--- /dev/null
+++ b/apiserver/src/main/java/org/dependencytrack/security/KeyGenerationInitTask.java
@@ -0,0 +1,48 @@
+/*
+ * 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.security;
+
+import alpine.security.crypto.KeyManager;
+import org.dependencytrack.init.InitTask;
+import org.dependencytrack.init.InitTaskContext;
+
+/**
+ * @since 5.6.0
+ */
+public class KeyGenerationInitTask implements InitTask {
+
+ @Override
+ public int priority() {
+ return PRIORITY_HIGHEST - 5;
+ }
+
+ @Override
+ public String name() {
+ return "key.generation";
+ }
+
+ @Override
+ public void execute(final InitTaskContext ctx) throws Exception {
+ // Force initialization of KeyManager, which will cause
+ // the secret, as well as the public-private key pair
+ // to be generated if necessary.
+ final var ignored = KeyManager.getInstance();
+ }
+
+}
diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTask.java
index cc55e95573..1b5ab052ea 100644
--- a/apiserver/src/main/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTask.java
+++ b/apiserver/src/main/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTask.java
@@ -71,8 +71,7 @@ public void inform(final Event event) {
private record Statistics(
Duration retentionDuration,
int deletedComponentMetrics,
- int deletedProjectMetrics,
- int deletedPortfolioMetrics) {
+ int deletedProjectMetrics) {
}
private Statistics informLocked(final Handle jdbiHandle) {
@@ -97,8 +96,7 @@ private Statistics informLocked(final Handle jdbiHandle) {
final int numDeletedComponent = metricsDao.deleteComponentMetricsForRetentionDuration(retentionDuration);
final int numDeletedProject = metricsDao.deleteProjectMetricsForRetentionDuration(retentionDuration);
- final int numDeletedPortfolio = metricsDao.deletePortfolioMetricsForRetentionDuration(retentionDuration);
- return new Statistics(retentionDuration, numDeletedComponent, numDeletedProject, numDeletedPortfolio);
+ return new Statistics(retentionDuration, numDeletedComponent, numDeletedProject);
}
}
diff --git a/apiserver/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java b/apiserver/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java
index e0f09c5696..4515ea3cd9 100644
--- a/apiserver/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java
+++ b/apiserver/src/main/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTask.java
@@ -28,12 +28,8 @@
import org.dependencytrack.event.CallbackEvent;
import org.dependencytrack.event.PortfolioMetricsUpdateEvent;
import org.dependencytrack.event.ProjectMetricsUpdateEvent;
-import org.dependencytrack.metrics.Metrics;
-import org.dependencytrack.model.Project;
-import org.dependencytrack.persistence.QueryManager;
+import org.jdbi.v3.core.mapper.reflect.ConstructorMapper;
-import javax.jdo.PersistenceManager;
-import javax.jdo.Query;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
@@ -42,6 +38,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle;
import static org.dependencytrack.util.LockProvider.executeWithLock;
import static org.dependencytrack.util.LockProvider.isTaskLockToBeExtended;
import static org.dependencytrack.util.TaskUtil.getLockConfigForTask;
@@ -63,7 +60,7 @@ public void inform(final Event e) {
try {
executeWithLock(
getLockConfigForTask(PortfolioMetricsUpdateTask.class),
- (LockingTaskExecutor.Task)() -> updateMetrics(event.isForceRefresh()));
+ (LockingTaskExecutor.Task) () -> updateMetrics(event.isForceRefresh()));
} catch (Throwable ex) {
LOGGER.error("Error in acquiring lock and executing portfolio metrics task", ex);
}
@@ -79,88 +76,97 @@ private void updateMetrics(final boolean forceRefresh) throws Exception {
LOGGER.info("Refreshing project metrics");
refreshProjectMetrics();
}
-
- Metrics.updatePortfolioMetrics();
} finally {
LOGGER.info("Completed portfolio metrics update in " + Duration.ofNanos(System.nanoTime() - startTimeNs));
}
}
private static void refreshProjectMetrics() throws Exception {
- try (final var qm = new QueryManager()) {
- final PersistenceManager pm = qm.getPersistenceManager();
-
- LOGGER.debug("Fetching first " + BATCH_SIZE + " projects");
- LockConfiguration portfolioMetricsTaskConfig = getLockConfigForTask(PortfolioMetricsUpdateTask.class);
- List activeProjects = fetchNextActiveProjectsPage(pm, null);
- long processStartTime = System.currentTimeMillis();
- while (!activeProjects.isEmpty()) {
- long startTimeOfBatch = System.currentTimeMillis();
- final long firstId = activeProjects.get(0).id();
- final long lastId = activeProjects.get(activeProjects.size() - 1).id();
-
- // Distribute the batch across at most MAX_CONCURRENCY events, and process them asynchronously.
- final List> partitions = partition(activeProjects, MAX_CONCURRENCY);
- final var countDownLatch = new CountDownLatch(partitions.size());
-
- for (final List partition : partitions) {
- final var partitionEvent = new CallbackEvent(() -> {
- for (final ProjectProjection project : partition) {
- new ProjectMetricsUpdateTask().inform(new ProjectMetricsUpdateEvent(project.uuid()));
- }
- });
-
- final var countDownEvent = new CallbackEvent(countDownLatch::countDown);
- Event.dispatch(partitionEvent
- .onSuccess(countDownEvent)
- .onFailure(countDownEvent));
- }
-
- LOGGER.debug("Waiting for metrics updates for projects " + firstId + "-" + lastId + " to complete");
- if (!countDownLatch.await(15, TimeUnit.MINUTES)) {
- // Depending on the system load, it may take a while for the queued events
- // to be processed. And depending on how large the projects are, it may take a
- // while for the processing of the respective event to complete.
- // It is unlikely though that either of these situations causes a block for
- // over 15 minutes. If that happens, the system is under-resourced.
- LOGGER.warn("Updating metrics for projects " + firstId + "-" + lastId +
+ LOGGER.debug("Fetching first " + BATCH_SIZE + " projects");
+ LockConfiguration portfolioMetricsTaskConfig = getLockConfigForTask(PortfolioMetricsUpdateTask.class);
+ List activeProjects = fetchNextActiveProjectsPage(/* lastId */ null);
+ long processStartTime = System.currentTimeMillis();
+ while (!activeProjects.isEmpty()) {
+ long startTimeOfBatch = System.currentTimeMillis();
+ final long firstId = activeProjects.getFirst().id();
+ final long lastId = activeProjects.getLast().id();
+
+ // Distribute the batch across at most MAX_CONCURRENCY events, and process them asynchronously.
+ final List> partitions = partition(activeProjects, MAX_CONCURRENCY);
+ final var countDownLatch = new CountDownLatch(partitions.size());
+
+ for (final List partition : partitions) {
+ final var partitionEvent = new CallbackEvent(() -> {
+ for (final ProjectProjection project : partition) {
+ new ProjectMetricsUpdateTask().inform(new ProjectMetricsUpdateEvent(project.uuid()));
+ }
+ });
+
+ final var countDownEvent = new CallbackEvent(countDownLatch::countDown);
+ Event.dispatch(partitionEvent
+ .onSuccess(countDownEvent)
+ .onFailure(countDownEvent));
+ }
+
+ LOGGER.debug("Waiting for metrics updates for projects " + firstId + "-" + lastId + " to complete");
+ if (!countDownLatch.await(15, TimeUnit.MINUTES)) {
+ // Depending on the system load, it may take a while for the queued events
+ // to be processed. And depending on how large the projects are, it may take a
+ // while for the processing of the respective event to complete.
+ // It is unlikely though that either of these situations causes a block for
+ // over 15 minutes. If that happens, the system is under-resourced.
+ LOGGER.warn("Updating metrics for projects " + firstId + "-" + lastId +
" took longer than expected (15m); Proceeding with potentially stale data");
- }
- LOGGER.debug("Completed metrics updates for projects " + firstId + "-" + lastId);
- LOGGER.debug("Fetching next " + BATCH_SIZE + " projects");
- long now = System.currentTimeMillis();
- long processDurationInMillis = now - startTimeOfBatch;
- long cumulativeDurationInMillis = now - processStartTime;
- //extend the lock for the duration of process
- //initial duration of portfolio metrics can be set to 20min.
- //No thread calculating metrics would be executing for more than 15min.
- //lock can only be extended if lock until is held for time after current db time
- if(isTaskLockToBeExtended(cumulativeDurationInMillis, PortfolioMetricsUpdateTask.class)) {
- Duration extendLockByDuration = Duration.ofMillis(processDurationInMillis).plus(portfolioMetricsTaskConfig.getLockAtLeastFor());
- LOGGER.debug("Extending lock duration by ms: " + extendLockByDuration);
- LockExtender.extendActiveLock(extendLockByDuration, portfolioMetricsTaskConfig.getLockAtLeastFor());
- }
- activeProjects = fetchNextActiveProjectsPage(pm, lastId);
}
+ LOGGER.debug("Completed metrics updates for projects " + firstId + "-" + lastId);
+ LOGGER.debug("Fetching next " + BATCH_SIZE + " projects");
+ long now = System.currentTimeMillis();
+ long processDurationInMillis = now - startTimeOfBatch;
+ long cumulativeDurationInMillis = now - processStartTime;
+ //extend the lock for the duration of process
+ //initial duration of portfolio metrics can be set to 20min.
+ //No thread calculating metrics would be executing for more than 15min.
+ //lock can only be extended if lock until is held for time after current db time
+ if (isTaskLockToBeExtended(cumulativeDurationInMillis, PortfolioMetricsUpdateTask.class)) {
+ Duration extendLockByDuration = Duration.ofMillis(processDurationInMillis).plus(portfolioMetricsTaskConfig.getLockAtLeastFor());
+ LOGGER.debug("Extending lock duration by ms: " + extendLockByDuration);
+ LockExtender.extendActiveLock(extendLockByDuration, portfolioMetricsTaskConfig.getLockAtLeastFor());
+ }
+ activeProjects = fetchNextActiveProjectsPage(lastId);
}
}
public record ProjectProjection(long id, UUID uuid) {
}
- private static List fetchNextActiveProjectsPage(final PersistenceManager pm, final Long lastId) throws Exception {
- try (final Query query = pm.newQuery(Project.class)) {
- if (lastId == null) {
- query.setFilter("inactiveSince == null");
- } else {
- query.setFilter("inactiveSince == null && id < :lastId");
- query.setParameters(lastId);
- }
- query.setOrdering("id DESC");
- query.range(0, BATCH_SIZE);
- query.setResult("id, uuid");
- return List.copyOf(query.executeResultList(ProjectProjection.class));
- }
+ private static List fetchNextActiveProjectsPage(final Long lastId) {
+ return withJdbiHandle(handle -> {
+ final org.jdbi.v3.core.statement.Query query = handle.createQuery("""
+ SELECT "ID"
+ , "UUID"
+ FROM "PROJECT"
+ WHERE "INACTIVE_SINCE" IS NULL
+ AND NOT EXISTS(
+ SELECT 1
+ FROM "PROJECTMETRICS"
+ WHERE "PROJECT_ID" = "PROJECT"."ID"
+ AND "LAST_OCCURRENCE" >= CURRENT_DATE
+ AND "LAST_OCCURRENCE" < CURRENT_DATE + INTERVAL '1 day'
+ )
+ <#if lastId>
+ AND "ID" > :lastId
+ #if>
+ ORDER BY "ID"
+ LIMIT :batchSize
+ """);
+
+ return query
+ .bind("lastId", lastId)
+ .bind("batchSize", BATCH_SIZE)
+ .defineNamedBindings()
+ .map(ConstructorMapper.of(ProjectProjection.class))
+ .list();
+ });
}
static List> partition(final List list, int numPartitions) {
diff --git a/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java b/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java
index 6d5bd22c2d..ee5c4d7731 100644
--- a/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java
+++ b/apiserver/src/main/java/org/dependencytrack/util/NotificationUtil.java
@@ -18,18 +18,14 @@
*/
package org.dependencytrack.util;
-import alpine.model.ConfigProperty;
import alpine.notification.Notification;
import alpine.notification.NotificationLevel;
-import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.event.kafka.KafkaEventDispatcher;
import org.dependencytrack.model.Analysis;
import org.dependencytrack.model.AnalysisState;
import org.dependencytrack.model.Bom;
import org.dependencytrack.model.Component;
-import org.dependencytrack.model.ConfigPropertyConstants;
-import org.dependencytrack.model.NotificationPublisher;
import org.dependencytrack.model.Policy;
import org.dependencytrack.model.PolicyCondition;
import org.dependencytrack.model.PolicyViolation;
@@ -44,7 +40,6 @@
import org.dependencytrack.notification.NotificationConstants;
import org.dependencytrack.notification.NotificationGroup;
import org.dependencytrack.notification.NotificationScope;
-import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
import org.dependencytrack.notification.vo.AnalysisDecisionChange;
import org.dependencytrack.notification.vo.BomConsumedOrProcessed;
import org.dependencytrack.notification.vo.BomProcessingFailed;
@@ -58,10 +53,6 @@
import javax.jdo.FetchPlan;
import javax.jdo.Query;
-import java.io.File;
-import java.io.IOException;
-import java.net.URLDecoder;
-import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
@@ -72,8 +63,6 @@
import java.util.UUID;
import java.util.stream.Collectors;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
public final class NotificationUtil {
/**
@@ -319,39 +308,6 @@ public static void analyzeNotificationCriteria(final QueryManager qm, final Long
.subject(new PolicyViolationIdentified(violation, component, project)));
}
- public static void loadDefaultNotificationPublishers(QueryManager qm) throws IOException {
- for (final DefaultNotificationPublishers publisher : DefaultNotificationPublishers.values()) {
- File templateFile = new File(URLDecoder.decode(NotificationUtil.class.getResource(publisher.getPublisherTemplateFile()).getFile(), UTF_8.name()));
- if (qm.isEnabled(ConfigPropertyConstants.NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED)) {
- ConfigProperty templateBaseDir = qm.getConfigProperty(
- ConfigPropertyConstants.NOTIFICATION_TEMPLATE_BASE_DIR.getGroupName(),
- ConfigPropertyConstants.NOTIFICATION_TEMPLATE_BASE_DIR.getPropertyName()
- );
- File userProvidedTemplateFile = new File(Path.of(templateBaseDir.getPropertyValue(), publisher.getPublisherTemplateFile()).toUri());
- if (userProvidedTemplateFile.exists()) {
- templateFile = userProvidedTemplateFile;
- }
- }
- final String templateContent = FileUtils.readFileToString(templateFile, UTF_8);
- final NotificationPublisher existingPublisher = qm.getDefaultNotificationPublisherByName(publisher.getPublisherName());
- if (existingPublisher == null) {
- qm.createNotificationPublisher(
- publisher.getPublisherName(), publisher.getPublisherDescription(),
- publisher.getPublisherClass().name(), templateContent, publisher.getTemplateMimeType(),
- publisher.isDefaultPublisher()
- );
- } else {
- existingPublisher.setName(publisher.getPublisherName());
- existingPublisher.setDescription(publisher.getPublisherDescription());
- existingPublisher.setPublisherClass(publisher.getPublisherClass().name());
- existingPublisher.setTemplate(templateContent);
- existingPublisher.setTemplateMimeType(publisher.getTemplateMimeType());
- existingPublisher.setDefaultPublisher(publisher.isDefaultPublisher());
- qm.updateNotificationPublisher(existingPublisher);
- }
- }
- }
-
public static String generateNotificationContent(final org.dependencytrack.proto.notification.v1.Vulnerability vulnerability) {
final String content;
if (vulnerability.hasDescription()) {
diff --git a/apiserver/src/main/resources/META-INF/services/org.dependencytrack.init.InitTask b/apiserver/src/main/resources/META-INF/services/org.dependencytrack.init.InitTask
new file mode 100644
index 0000000000..c7826402b6
--- /dev/null
+++ b/apiserver/src/main/resources/META-INF/services/org.dependencytrack.init.InitTask
@@ -0,0 +1,4 @@
+org.dependencytrack.persistence.DatabaseMigrationInitTask
+org.dependencytrack.persistence.DatabasePartitionMaintenanceInitTask
+org.dependencytrack.persistence.DatabaseSeedingInitTask
+org.dependencytrack.security.KeyGenerationInitTask
\ No newline at end of file
diff --git a/apiserver/src/main/resources/application.properties b/apiserver/src/main/resources/application.properties
index d243a21c1f..79cca2a77a 100644
--- a/apiserver/src/main/resources/application.properties
+++ b/apiserver/src/main/resources/application.properties
@@ -156,27 +156,7 @@ alpine.datanucleus.executioncontext.maxidle=0
# @hidden
alpine.datanucleus.deletionpolicy=DataNucleus
-# Defines whether database migrations should be executed on startup.
-#
-# From v5.6.0 onwards, migrations are considered part of the initialization tasks.
-# Setting init.tasks.enabled to `false` will disable migrations,
-# even if database.run.migrations is enabled.
-#
-# @category: Database
-# @type: boolean
-database.run.migrations=true
-
-# Defines whether the application should exit upon successful execution of database migrations.
-# Enabling this option makes the application suitable for running as k8s init container.
-# Has no effect unless database.run.migrations is `true`.
-#
-# From v5.6.0 onwards, usage of init.and.exit should be preferred.
-#
-# @category: Database
-# @type: boolean
-# database.run.migrations.only=false
-
-# Defines the database JDBC URL to use when executing migrations.
+# Defines the database JDBC URL to use when executing init tasks.
# If not set, the value of alpine.database.url will be used.
# Should generally not be set, unless TLS authentication is used,
# and custom connection variables are required.
@@ -184,23 +164,23 @@ database.run.migrations=true
# @category: Database
# @default: ${alpine.database.url}
# @type: string
-# database.migration.url=
+# init.tasks.database.url=
-# Defines the database user for executing migrations.
+# Defines the database user for executing init tasks.
# If not set, the value of alpine.database.username will be used.
#
# @category: Database
# @default: ${alpine.database.username}
# @type: string
-# database.migration.username=
+# init.tasks.database.username=
-# Defines the database password for executing migrations.
+# Defines the database password for executing init tasks.
# If not set, the value of alpine.database.password will be used.
#
# @category: Database
# @default: ${alpine.database.password}
# @type: string
-# database.migration.password=
+# init.tasks.database.password=
# Specifies the number of bcrypt rounds to use when hashing a user's password.
# The higher the number the more secure the password, at the expense of
@@ -1228,16 +1208,46 @@ vulnerability.policy.s3.bundle.name=
vulnerability.policy.s3.region=
# Whether to execute initialization tasks on startup.
-# Initialization tasks include:
-#
-# - Execution of database migrations
-# - Populating the database with default objects (permissions, users, licenses, etc.)
-#
#
# @category: General
# @type: boolean
init.tasks.enabled=true
+# Whether to enable the database migration init task.
+#
+# Has no effect unless init.tasks.enabled is `true`.
+#
+# @category: General
+# @type: boolean
+init.task.database.migration.enabled=true
+
+# Whether to enable the database partition maintenance init task.
+#
+# Has no effect unless init.tasks.enabled is `true`.
+#
+# @category: General
+# @type: boolean
+init.task.database.partition.maintenance.enabled=true
+
+# Whether to enable the database seeding init task.
+#
+# Seeding involves populating the database with default objects,
+# such as permissions, users, licenses, etc.
+#
+# Has no effect unless init.tasks.enabled is `true`.
+#
+# @category: General
+# @type: boolean
+init.task.database.seeding.enabled=true
+
+# Whether to enable the key generation init task.
+#
+# Has no effect unless init.tasks.enabled is `true`.
+#
+# @category: General
+# @type: boolean
+init.task.key.generation.enabled=true
+
# Whether to only execute initialization tasks and exit.
#
# @category: General
@@ -1277,7 +1287,7 @@ dev.services.image.frontend=ghcr.io/dependencytrack/hyades-frontend:snapshot
#
# @category: Development
# @type: string
-dev.services.image.kafka=apache/kafka-native:3.9.0
+dev.services.image.kafka=apache/kafka-native:3.9.1
# The image to use for the PostgreSQL dev services container.
#
@@ -1338,7 +1348,6 @@ task.component.metadata.maintenance.lock.min.duration=PT1M
#
# DEPENDENCYMETRICS
# PROJECTMETRICS
-# PORTFOLIOMETRICS
#
#
# @category: Task Scheduling
diff --git a/apiserver/src/main/webapp/WEB-INF/web.xml b/apiserver/src/main/webapp/WEB-INF/web.xml
index 645caec27c..ed671bd089 100644
--- a/apiserver/src/main/webapp/WEB-INF/web.xml
+++ b/apiserver/src/main/webapp/WEB-INF/web.xml
@@ -30,10 +30,7 @@
alpine.server.metrics.MetricsInitializer
- org.dependencytrack.common.KeyManagerInitializer
-
-
- org.dependencytrack.persistence.MigrationInitializer
+ org.dependencytrack.init.InitTaskServletContextListener
alpine.server.persistence.PersistenceManagerFactory
@@ -44,9 +41,6 @@
org.dependencytrack.health.HealthCheckInitializer
-
- org.dependencytrack.persistence.DefaultObjectGenerator
-
org.dependencytrack.event.kafka.KafkaProducerInitializer
@@ -116,7 +110,12 @@
alpine.server.AlpineServlet
jersey.config.server.provider.packages
- alpine.server.filters,alpine.server.resources,org.dependencytrack.resources,org.dependencytrack.filters
+
+ alpine.server.filters,
+ alpine.server.resources,
+ org.dependencytrack.filters,
+ org.dependencytrack.resources.v1
+
jersey.config.server.provider.classnames
@@ -132,6 +131,19 @@
DependencyTrack
/api/*
+
+
+ REST-API-v2
+ org.glassfish.jersey.servlet.ServletContainer
+
+ jakarta.ws.rs.Application
+ org.dependencytrack.resources.v2.ResourceConfig
+
+
+
+ REST-API-v2
+ /api/v2/*
+
Health
diff --git a/apiserver/src/test/java/org/dependencytrack/JerseyTestRule.java b/apiserver/src/test/java/org/dependencytrack/JerseyTestRule.java
index 8bffa30f48..0dd53ea86b 100644
--- a/apiserver/src/test/java/org/dependencytrack/JerseyTestRule.java
+++ b/apiserver/src/test/java/org/dependencytrack/JerseyTestRule.java
@@ -18,7 +18,9 @@
*/
package org.dependencytrack;
-import org.dependencytrack.resources.v1.exception.ClientErrorExceptionMapper;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.dependencytrack.resources.v2.OpenApiValidationClientResponseFilter;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider;
import org.glassfish.jersey.server.ResourceConfig;
@@ -32,6 +34,9 @@
import org.junit.rules.ExternalResource;
import jakarta.ws.rs.client.WebTarget;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
/**
* @since 4.11.0
@@ -41,6 +46,7 @@ public class JerseyTestRule extends ExternalResource {
private final JerseyTest jerseyTest;
public JerseyTestRule(final ResourceConfig resourceConfig) {
+ final boolean isV2 = isV2(resourceConfig);
this.jerseyTest = new JerseyTest() {
@Override
@@ -54,14 +60,25 @@ protected void configureClient(final ClientConfig config) {
// using the default HttpUrlConnection connector provider.
// See https://github.com/eclipse-ee4j/jersey/issues/4825
config.connectorProvider(new GrizzlyConnectorProvider());
+
+ if (isV2) {
+ config.register(OpenApiValidationClientResponseFilter.class);
+ }
}
@Override
protected DeploymentContext configureDeployment() {
forceSet(TestProperties.CONTAINER_PORT, "0");
- return ServletDeploymentContext.forServlet(new ServletContainer(
- // Ensure exception mappers are registered.
- resourceConfig.packages(ClientErrorExceptionMapper.class.getPackageName()))).build();
+
+ // Ensure exception mappers are registered.
+ if (isV2) {
+ resourceConfig.packages("org.dependencytrack.resources.v2.exception");
+ } else {
+ resourceConfig.packages("org.dependencytrack.resources.v1.exception");
+ }
+
+ return ServletDeploymentContext.forServlet(
+ new ServletContainer(resourceConfig)).build();
}
};
@@ -89,4 +106,28 @@ public final WebTarget target(final String path) {
return jerseyTest.target(path);
}
+ public final WebTarget target(final URI uri) {
+ WebTarget target = jerseyTest.target(uri.getPath());
+
+ if (uri.getQuery() != null) {
+ final List uriQueryParams =
+ URLEncodedUtils.parse(uri, StandardCharsets.UTF_8);
+ for (final NameValuePair queryParam : uriQueryParams) {
+ target = target.queryParam(queryParam.getName(), queryParam.getValue());
+ }
+ }
+
+ return target;
+ }
+
+ private boolean isV2(final ResourceConfig resourceConfig) {
+ for (final Class> clazz : resourceConfig.getClasses()) {
+ if (clazz.getPackageName().startsWith("org.dependencytrack.resources.v2")) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/PersistenceCapableTest.java b/apiserver/src/test/java/org/dependencytrack/PersistenceCapableTest.java
index 421a53cd20..3a87619323 100644
--- a/apiserver/src/test/java/org/dependencytrack/PersistenceCapableTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/PersistenceCapableTest.java
@@ -126,6 +126,25 @@ FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = CURRENT_SCHEMA()) L
END LOOP;
END $$;
""");
+
+ statement.execute("""
+ DO $$
+ DECLARE
+ partition_name TEXT;
+ today_partition_pattern TEXT := format('^(PROJECT|DEPENDENCY)METRICS_%s', TO_CHAR(CURRENT_DATE, 'YYYYMMDD'));
+ tomorrow_partition_pattern TEXT := format('^(PROJECT|DEPENDENCY)METRICS_%s', TO_CHAR(CURRENT_DATE + 1, 'YYYYMMDD'));
+ BEGIN
+ FOR partition_name IN
+ SELECT tablename
+ FROM pg_tables
+ WHERE tablename ~ '^(PROJECT|DEPENDENCY)METRICS_[0-9]{8}$'
+ AND tablename !~ today_partition_pattern
+ AND tablename !~ tomorrow_partition_pattern
+ LOOP
+ EXECUTE format('DROP TABLE "%s"', partition_name);
+ END LOOP;
+ END $$;
+ """);
}
}
diff --git a/apiserver/src/test/java/org/dependencytrack/event/kafka/processor/api/ProcessorManagerTest.java b/apiserver/src/test/java/org/dependencytrack/event/kafka/processor/api/ProcessorManagerTest.java
index 9341f3dde9..862b2465e6 100644
--- a/apiserver/src/test/java/org/dependencytrack/event/kafka/processor/api/ProcessorManagerTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/event/kafka/processor/api/ProcessorManagerTest.java
@@ -58,11 +58,7 @@ public class ProcessorManagerTest {
public final EnvironmentVariables environmentVariables = new EnvironmentVariables();
@Rule
- public KafkaContainer kafkaContainer = new KafkaContainer("apache/kafka-native:3.9.0")
- // TODO: Remove this when Kafka >= 3.9.1 is available.
- // * https://github.com/testcontainers/testcontainers-java/issues/9506#issuecomment-2463504967
- // * https://issues.apache.org/jira/browse/KAFKA-18281
- .withEnv("KAFKA_LISTENERS", "PLAINTEXT://:9092,BROKER://:9093,CONTROLLER://:9094");
+ public KafkaContainer kafkaContainer = new KafkaContainer("apache/kafka-native:3.9.1");
private AdminClient adminClient;
private Producer producer;
diff --git a/apiserver/src/test/java/org/dependencytrack/init/InitTaskExecutorTest.java b/apiserver/src/test/java/org/dependencytrack/init/InitTaskExecutorTest.java
new file mode 100644
index 0000000000..0ac943fae0
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/init/InitTaskExecutorTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.init;
+
+import alpine.Config;
+import org.dependencytrack.PersistenceCapableTest;
+import org.junit.Before;
+import org.junit.Test;
+import org.postgresql.ds.PGSimpleDataSource;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.Mockito.mock;
+
+public class InitTaskExecutorTest extends PersistenceCapableTest {
+
+ private Config configMock;
+ private PGSimpleDataSource dataSource;
+
+ @Before
+ public void before() throws Exception {
+ super.before();
+
+ configMock = mock(Config.class);
+
+ dataSource = new PGSimpleDataSource();
+ dataSource.setUrl(postgresContainer.getJdbcUrl());
+ dataSource.setUser(postgresContainer.getUsername());
+ dataSource.setPassword(postgresContainer.getPassword());
+ }
+
+ @Test
+ public void shouldExecuteTasksInPriorityOrder() {
+ final var executedTaskNames = new ArrayList(3);
+
+ final var executor = new InitTaskExecutor(configMock, dataSource, List.of(
+ new TestInitTask(1, "a", () -> executedTaskNames.add("a")),
+ new TestInitTask(5, "b", () -> executedTaskNames.add("b")),
+ new TestInitTask(3, "c", () -> executedTaskNames.add("c"))));
+ executor.execute();
+
+ assertThat(executedTaskNames).containsExactly("b", "c", "a");
+ }
+
+ @Test
+ public void shouldThrowWhenTaskExecutionFails() {
+ final var executor = new InitTaskExecutor(configMock, dataSource, List.of(
+ new TestInitTask(1, "test", () -> {
+ throw new IllegalStateException("boom");
+ })));
+
+ assertThatExceptionOfType(IllegalStateException.class)
+ .isThrownBy(executor::execute)
+ .withMessage("Failed to execute init task test")
+ .withCauseInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ public void shouldThrowOnDuplicateTaskName() {
+ final var executor = new InitTaskExecutor(configMock, dataSource, List.of(
+ new TestInitTask(1, "test"),
+ new TestInitTask(2, "test")));
+
+ assertThatExceptionOfType(IllegalStateException.class)
+ .isThrownBy(executor::execute)
+ // NB: In a real-world scenario, the class names would be different.
+ .withMessage("""
+ Duplicate task name test: Registered by \
+ org.dependencytrack.init.InitTaskExecutorTest$TestInitTask and \
+ org.dependencytrack.init.InitTaskExecutorTest$TestInitTask""");
+ }
+
+ @Test
+ public void shouldThrowOnInvalidTaskPriority() {
+ final var executor = new InitTaskExecutor(configMock, dataSource, List.of(
+ new TestInitTask(-1, "test")));
+
+ assertThatExceptionOfType(IllegalStateException.class)
+ .isThrownBy(executor::execute)
+ .withMessage("Invalid priority of task test: Must be within [0..100] but is -1");
+ }
+
+ private static final class TestInitTask implements InitTask {
+
+ private final int priority;
+ private final String name;
+ private final Runnable runnable;
+
+ private TestInitTask(final int priority, final String name) {
+ this(priority, name, null);
+ }
+
+ private TestInitTask(final int priority, final String name, final Runnable runnable) {
+ this.priority = priority;
+ this.name = name;
+ this.runnable = runnable;
+ }
+
+ @Override
+ public int priority() {
+ return priority;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public void execute(final InitTaskContext ctx) {
+ if (runnable != null) {
+ runnable.run();
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/model/DefaultRepositoryTest.java b/apiserver/src/test/java/org/dependencytrack/model/DefaultRepositoryTest.java
new file mode 100644
index 0000000000..aab5ea215a
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/model/DefaultRepositoryTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.model;
+
+import org.assertj.core.api.SoftAssertions;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class DefaultRepositoryTest {
+
+ @Test
+ public void shouldNotHaveDuplicateResolutionOrdersPerType() {
+ final Map> defaultRepoByType =
+ Arrays.stream(DefaultRepository.values()).collect(
+ Collectors.groupingBy(
+ DefaultRepository::getType,
+ Collectors.mapping(
+ DefaultRepository::getResolutionOrder,
+ Collectors.toList())));
+
+ final var softAsserts = new SoftAssertions();
+ for (final RepositoryType type : RepositoryType.values()) {
+ final List defaultRepos = defaultRepoByType.get(type);
+ if (defaultRepos != null) {
+ softAsserts.assertThat(defaultRepos).doesNotHaveDuplicates();
+ }
+ }
+
+ softAsserts.assertAll();
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/MigrationInitializerTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseMigrationInitTaskTest.java
similarity index 56%
rename from apiserver/src/test/java/org/dependencytrack/persistence/MigrationInitializerTest.java
rename to apiserver/src/test/java/org/dependencytrack/persistence/DatabaseMigrationInitTaskTest.java
index d0636b9d84..526859115a 100644
--- a/apiserver/src/test/java/org/dependencytrack/persistence/MigrationInitializerTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseMigrationInitTaskTest.java
@@ -20,10 +20,12 @@
import alpine.Config;
import org.dependencytrack.common.ConfigKey;
+import org.dependencytrack.init.InitTaskContext;
import org.jdbi.v3.core.Jdbi;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.postgresql.ds.PGSimpleDataSource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
@@ -34,9 +36,10 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-public class MigrationInitializerTest {
+public class DatabaseMigrationInitTaskTest {
private PostgreSQLContainer> postgresContainer;
+ private PGSimpleDataSource dataSource;
private Jdbi jdbi;
@Before
@@ -44,11 +47,12 @@ public void setUp() {
postgresContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:13-alpine"));
postgresContainer.start();
- jdbi = Jdbi.create(
- postgresContainer.getJdbcUrl(),
- postgresContainer.getUsername(),
- postgresContainer.getPassword()
- );
+ dataSource = new PGSimpleDataSource();
+ dataSource.setUrl(postgresContainer.getJdbcUrl());
+ dataSource.setUser(postgresContainer.getUsername());
+ dataSource.setPassword(postgresContainer.getPassword());
+
+ jdbi = Jdbi.create(dataSource);
}
@After
@@ -59,67 +63,35 @@ public void tearDown() {
}
@Test
- public void test() {
+ public void test() throws Exception {
final var configMock = mock(Config.class);
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_URL))).thenReturn(postgresContainer.getJdbcUrl());
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_DRIVER))).thenReturn(postgresContainer.getDriverClassName());
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_USERNAME))).thenReturn(postgresContainer.getUsername());
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_PASSWORD))).thenReturn(postgresContainer.getPassword());
when(configMock.getPropertyAsBoolean(eq(ConfigKey.INIT_TASKS_ENABLED))).thenReturn(true);
- when(configMock.getPropertyAsBoolean(eq(ConfigKey.DATABASE_RUN_MIGRATIONS))).thenReturn(true);
- new MigrationInitializer(configMock).contextInitialized(null);
+ new DatabaseMigrationInitTask().execute(new InitTaskContext(configMock, dataSource));
assertMigrationExecuted(/* expectExecuted */ true);
}
@Test
- public void testWithMigrationCredentials() {
+ public void testWithMigrationCredentials() throws Exception {
final var configMock = mock(Config.class);
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_URL))).thenReturn(postgresContainer.getJdbcUrl());
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_DRIVER))).thenReturn(postgresContainer.getDriverClassName());
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_USERNAME))).thenReturn("username");
when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_PASSWORD))).thenReturn("password");
when(configMock.getPropertyAsBoolean(eq(ConfigKey.INIT_TASKS_ENABLED))).thenReturn(true);
- when(configMock.getPropertyAsBoolean(eq(ConfigKey.DATABASE_RUN_MIGRATIONS))).thenReturn(true);
- when(configMock.getProperty(eq(ConfigKey.DATABASE_MIGRATION_USERNAME))).thenReturn(postgresContainer.getUsername());
- when(configMock.getProperty(eq(ConfigKey.DATABASE_MIGRATION_PASSWORD))).thenReturn(postgresContainer.getPassword());
+ when(configMock.getProperty(eq(ConfigKey.INIT_TASKS_DATABASE_USERNAME))).thenReturn(postgresContainer.getUsername());
+ when(configMock.getProperty(eq(ConfigKey.INIT_TASKS_DATABASE_PASSWORD))).thenReturn(postgresContainer.getPassword());
- new MigrationInitializer(configMock).contextInitialized(null);
+ new DatabaseMigrationInitTask().execute(new InitTaskContext(configMock, dataSource));
assertMigrationExecuted(/* expectExecuted */ true);
}
- @Test
- public void testWithRunMigrationsDisabled() {
- final var configMock = mock(Config.class);
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_URL))).thenReturn(postgresContainer.getJdbcUrl());
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_DRIVER))).thenReturn(postgresContainer.getDriverClassName());
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_USERNAME))).thenReturn(postgresContainer.getUsername());
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_PASSWORD))).thenReturn(postgresContainer.getPassword());
- when(configMock.getPropertyAsBoolean(eq(ConfigKey.INIT_TASKS_ENABLED))).thenReturn(true);
- when(configMock.getPropertyAsBoolean(eq(ConfigKey.DATABASE_RUN_MIGRATIONS))).thenReturn(false);
-
- new MigrationInitializer(configMock).contextInitialized(null);
-
- assertMigrationExecuted(/* expectExecuted */ false);
- }
-
- @Test
- public void testWithInitTasksDisabled() {
- final var configMock = mock(Config.class);
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_URL))).thenReturn(postgresContainer.getJdbcUrl());
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_DRIVER))).thenReturn(postgresContainer.getDriverClassName());
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_USERNAME))).thenReturn(postgresContainer.getUsername());
- when(configMock.getProperty(eq(Config.AlpineKey.DATABASE_PASSWORD))).thenReturn(postgresContainer.getPassword());
- when(configMock.getPropertyAsBoolean(eq(ConfigKey.INIT_TASKS_ENABLED))).thenReturn(false);
- when(configMock.getPropertyAsBoolean(eq(ConfigKey.DATABASE_RUN_MIGRATIONS))).thenReturn(true);
-
- new MigrationInitializer(configMock).contextInitialized(null);
-
- assertMigrationExecuted(/* expectExecuted */ false);
- }
-
private void assertMigrationExecuted(final boolean expectExecuted) {
final List tableNames = jdbi.withHandle(handle -> handle.createQuery("""
SELECT "table_name"
@@ -136,4 +108,4 @@ private void assertMigrationExecuted(final boolean expectExecuted) {
}
}
-}
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTaskTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTaskTest.java
new file mode 100644
index 0000000000..47fd057387
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/persistence/DatabasePartitionMaintenanceInitTaskTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.persistence;
+
+import alpine.Config;
+import org.dependencytrack.PersistenceCapableTest;
+import org.dependencytrack.init.InitTaskContext;
+import org.dependencytrack.persistence.jdbi.MetricsDao;
+import org.junit.Test;
+import org.postgresql.ds.PGSimpleDataSource;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle;
+import static org.mockito.Mockito.mock;
+
+public class DatabasePartitionMaintenanceInitTaskTest extends PersistenceCapableTest {
+
+ @Test
+ public void testMetricsPartitionsForTodayAndTomorrow() throws Exception {
+ final var dataSource = new PGSimpleDataSource();
+ dataSource.setUrl(postgresContainer.getJdbcUrl());
+ dataSource.setUser(postgresContainer.getUsername());
+ dataSource.setPassword(postgresContainer.getPassword());
+
+ new DatabasePartitionMaintenanceInitTask().execute(new InitTaskContext(mock(Config.class), dataSource));
+
+ var today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
+ var tomorrow = LocalDate.now().plusDays(1).format(DateTimeFormatter.BASIC_ISO_DATE);
+
+ useJdbiHandle(handle -> {
+ var metricsDao = handle.attach(MetricsDao.class);
+ assertThat(Collections.frequency(metricsDao.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(today))).isEqualTo(1);
+ assertThat(Collections.frequency(metricsDao.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1);
+ assertThat(Collections.frequency(metricsDao.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(today))).isEqualTo(1);
+ assertThat(Collections.frequency(metricsDao.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1);
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseSeedingInitTaskTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseSeedingInitTaskTest.java
new file mode 100644
index 0000000000..8a7b23edf2
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/persistence/DatabaseSeedingInitTaskTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.persistence;
+
+import alpine.Config;
+import alpine.model.ConfigProperty;
+import alpine.model.ManagedUser;
+import alpine.model.Permission;
+import alpine.model.Team;
+import org.dependencytrack.PersistenceCapableTest;
+import org.dependencytrack.auth.Permissions;
+import org.dependencytrack.init.InitTaskContext;
+import org.dependencytrack.model.ConfigPropertyConstants;
+import org.dependencytrack.model.DefaultRepository;
+import org.dependencytrack.model.License;
+import org.dependencytrack.model.LicenseGroup;
+import org.dependencytrack.model.NotificationPublisher;
+import org.dependencytrack.model.Repository;
+import org.dependencytrack.model.Role;
+import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
+import org.junit.Before;
+import org.junit.Test;
+import org.postgresql.ds.PGSimpleDataSource;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+public class DatabaseSeedingInitTaskTest extends PersistenceCapableTest {
+
+ private Config configMock;
+ private PGSimpleDataSource dataSource;
+
+ @Before
+ public void before() throws Exception {
+ super.before();
+
+ configMock = mock(Config.class);
+ doReturn("25b88cc9-ff96-4ec5-9921-6212e954a46f").when(configMock).getApplicationBuildUuid();
+ doReturn("1970-01-01 00:00:00").when(configMock).getApplicationBuildTimestamp();
+
+ dataSource = new PGSimpleDataSource();
+ dataSource.setUrl(postgresContainer.getJdbcUrl());
+ dataSource.setUser(postgresContainer.getUsername());
+ dataSource.setPassword(postgresContainer.getPassword());
+ }
+
+ @Test
+ public void test() throws Exception {
+ new DatabaseSeedingInitTask().execute(new InitTaskContext(configMock, dataSource));
+
+ final List configProperties = qm.getConfigProperties();
+ assertThat(configProperties).hasSize(ConfigPropertyConstants.values().length);
+ assertThat(configProperties).allSatisfy(property -> {
+ assertThat(property.getGroupName()).isNotBlank();
+ assertThat(property.getPropertyName()).isNotBlank();
+ assertThat(property.getPropertyType()).isNotNull();
+ assertThat(property.getDescription()).isNotNull();
+ });
+ assertThat(configProperties).anySatisfy(property -> assertThat(property.getPropertyValue()).isNotBlank());
+
+ final List permissions = qm.getPermissions();
+ assertThat(permissions).hasSize(Permissions.values().length);
+ assertThat(permissions).allSatisfy(permission -> {
+ assertThat(permission.getName()).isNotBlank();
+ assertThat(permission.getDescription()).isNotBlank();
+ });
+
+ final List teams = qm.getTeams().getList(Team.class);
+ assertThat(teams).isNotEmpty();
+ assertThat(teams).allSatisfy(team -> {
+ assertThat(team.getName()).isNotBlank();
+ assertThat(team.getUuid()).isNotNull();
+ assertThat(team.getPermissions()).isNotEmpty();
+ });
+
+ final List roles = qm.getRoles();
+ assertThat(roles).isNotEmpty();
+ assertThat(roles).allSatisfy(role -> {
+ assertThat(role.getName()).isNotBlank();
+ assertThat(role.getUuid()).isNotNull();
+ assertThat(role.getPermissions()).isNotEmpty();
+ });
+
+ final List users = qm.getManagedUsers();
+ assertThat(users).satisfiesExactly(user -> {
+ assertThat(user.getUsername()).isEqualTo("admin");
+ assertThat(user.getEmail()).isEqualTo("admin@localhost");
+ assertThat(user.getPassword()).isNotBlank();
+ assertThat(user.getLastPasswordChange()).isNotNull();
+ assertThat(user.isForcePasswordChange()).isTrue();
+ assertThat(user.isNonExpiryPassword()).isTrue();
+ assertThat(user.isSuspended()).isFalse();
+ assertThat(user.getPermissions()).hasSize(Permissions.values().length);
+ assertThat(user.getTeams()).extracting(Team::getName).containsOnly("Administrators");
+ });
+
+ final List licenses = qm.getLicenses().getList(License.class);
+ assertThat(licenses).isNotEmpty();
+ assertThat(licenses).allSatisfy(license -> {
+ assertThat(license.getLicenseId()).isNotBlank();
+ assertThat(license.getName()).isNotBlank();
+ assertThat(license.getUuid()).isNotNull();
+ });
+ assertThat(licenses).anySatisfy(license -> assertThat(license.getHeader()).isNotBlank());
+ assertThat(licenses).anySatisfy(license -> assertThat(license.getHeader()).isNotBlank());
+ assertThat(licenses).anySatisfy(license -> assertThat(license.getText()).isNotBlank());
+ assertThat(licenses).anySatisfy(license -> assertThat(license.getTemplate()).isNotBlank());
+ assertThat(licenses).anySatisfy(license -> assertThat(license.getComment()).isNotBlank());
+ assertThat(licenses).anySatisfy(license -> assertThat(license.getSeeAlso()).isNotEmpty());
+
+ final List licenseGroups = qm.getLicenseGroups().getList(LicenseGroup.class);
+ assertThat(licenseGroups).isNotEmpty();
+ assertThat(licenseGroups).allSatisfy(licenseGroup -> {
+ assertThat(licenseGroup.getName()).isNotBlank();
+ assertThat(licenseGroup.getUuid()).isNotNull();
+ assertThat(licenseGroup.getLicenses()).isNotEmpty();
+ });
+
+ final List notificationPublishers = qm.getAllNotificationPublishers();
+ assertThat(notificationPublishers).hasSize(DefaultNotificationPublishers.values().length);
+ assertThat(notificationPublishers).allSatisfy(notificationPublisher -> {
+ assertThat(notificationPublisher.getName()).isNotBlank();
+ assertThat(notificationPublisher.getPublisherClass()).isNotBlank();
+ assertThat(notificationPublisher.getDescription()).isNotBlank();
+ assertThat(notificationPublisher.getTemplate()).isNotBlank();
+ assertThat(notificationPublisher.getTemplateMimeType()).isNotBlank();
+ assertThat(notificationPublisher.isDefaultPublisher()).isTrue();
+ });
+
+ final List repositories = qm.getRepositories().getList(Repository.class);
+ assertThat(repositories).hasSize(DefaultRepository.values().length);
+ assertThat(repositories).allSatisfy(repository -> {
+ assertThat(repository.getType()).isNotNull();
+ assertThat(repository.getIdentifier()).isNotBlank();
+ assertThat(repository.getUrl()).isNotBlank();
+ assertThat(repository.getResolutionOrder()).isNotZero();
+ assertThat(repository.isEnabled()).isTrue();
+ assertThat(repository.getUuid()).isNotNull();
+ });
+ }
+
+ @Test
+ public void testWithDefaultObjectsAlreadyPopulated() throws Exception {
+ new DatabaseSeedingInitTask().execute(new InitTaskContext(configMock, dataSource));
+
+ List licenses = qm.getLicenses().getList(License.class);
+ assertThat(licenses).isNotEmpty();
+
+ qm.delete(licenses);
+
+ new DatabaseSeedingInitTask().execute(new InitTaskContext(configMock, dataSource));
+
+ // Default objects must not have been populated again, since their
+ // version is already current for this application build.
+ licenses = qm.getLicenses().getList(License.class);
+ assertThat(licenses).isEmpty();
+ }
+
+ @Test
+ public void testLoadDefaultLicensesUpdatesExistingLicenses() throws Exception {
+ final var license = new License();
+ license.setLicenseId("LGPL-2.1+");
+ license.setName("name");
+ license.setComment("comment");
+ license.setHeader("header");
+ license.setSeeAlso("seeAlso");
+ license.setTemplate("template");
+ license.setText("text");
+ qm.persist(license);
+
+ new DatabaseSeedingInitTask().execute(new InitTaskContext(configMock, dataSource));
+
+ qm.getPersistenceManager().refresh(license);
+ assertThat(license.getLicenseId()).isEqualTo("LGPL-2.1+");
+ assertThat(license.getName()).isEqualTo("GNU Lesser General Public License v2.1 or later");
+ assertThat(license.getComment()).isNotEqualTo("comment");
+ assertThat(license.getHeader()).isNotEqualTo("header");
+ assertThat(license.getSeeAlso()).isNotEqualTo(new String[]{"seeAlso"});
+ assertThat(license.getTemplate()).isNotEqualTo("template");
+ assertThat(license.getText()).isNotEqualTo("text");
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java
deleted file mode 100644
index 4bf32e82c4..0000000000
--- a/apiserver/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * This file is part of Dependency-Track.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * SPDX-License-Identifier: Apache-2.0
- * Copyright (c) OWASP Foundation. All Rights Reserved.
- */
-package org.dependencytrack.persistence;
-
-import org.dependencytrack.PersistenceCapableTest;
-import org.dependencytrack.auth.Permissions;
-import org.dependencytrack.common.ConfigKey;
-import org.dependencytrack.model.ConfigPropertyConstants;
-import org.dependencytrack.model.License;
-import org.dependencytrack.model.Repository;
-import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
-import org.dependencytrack.persistence.jdbi.MetricsDao;
-import org.junit.Assert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.contrib.java.lang.system.EnvironmentVariables;
-
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
-import java.util.Collections;
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle;
-
-public class DefaultObjectGeneratorTest extends PersistenceCapableTest {
-
- @Rule
- public EnvironmentVariables environmentVariables = new EnvironmentVariables();
-
- @Test
- public void testContextInitialized() throws Exception {
- testLoadDefaultPermissions();
- testLoadDefaultPersonas();
- testLoadDefaultLicenses();
- testLoadDefaultRepositories();
- testLoadDefaultConfigProperties();
- testLoadDefaultNotificationPublishers();
- }
-
- @Test
- public void testWithInitTasksDisabled() {
- environmentVariables.set(ConfigKey.INIT_TASKS_ENABLED.name(), "false");
-
- new DefaultObjectGenerator().contextInitialized(null);
-
- assertThat(qm.getPermissions()).isEmpty();
- assertThat(qm.getManagedUsers()).isEmpty();
- assertThat(qm.getLicenses().getList(License.class)).isEmpty();
- assertThat(qm.getRepositories().getList(Repository.class)).isEmpty();
- assertThat(qm.getConfigProperties()).isEmpty();
- assertThat(qm.getAllNotificationPublishers()).isEmpty();
- }
-
- @Test
- public void testWithDefaultObjectsAlreadyPopulated() {
- new DefaultObjectGenerator().contextInitialized(null);
-
- List licenses = qm.getLicenses().getList(License.class);
- assertThat(licenses).isNotEmpty();
-
- qm.delete(licenses);
-
- new DefaultObjectGenerator().contextInitialized(null);
-
- // Default objects must not have been populated again, since their
- // version is already current for this application build.
- licenses = qm.getLicenses().getList(License.class);
- assertThat(licenses).isEmpty();
- }
-
- @Test
- public void testLoadDefaultLicenses() throws Exception {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.loadDefaultLicenses();
- Assert.assertEquals(738, qm.getAllLicensesConcise().size());
- }
-
- @Test
- public void testLoadDefaultLicensesUpdatesExistingLicenses() throws Exception {
- final var license = new License();
- license.setLicenseId("LGPL-2.1+");
- license.setName("name");
- license.setComment("comment");
- license.setHeader("header");
- license.setSeeAlso("seeAlso");
- license.setTemplate("template");
- license.setText("text");
- qm.persist(license);
-
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultLicenses();
-
- qm.getPersistenceManager().refresh(license);
- assertThat(license.getLicenseId()).isEqualTo("LGPL-2.1+");
- assertThat(license.getName()).isEqualTo("GNU Lesser General Public License v2.1 or later");
- assertThat(license.getComment()).isNotEqualTo("comment");
- assertThat(license.getHeader()).isNotEqualTo("header");
- assertThat(license.getSeeAlso()).isNotEqualTo(new String[]{"seeAlso"});
- assertThat(license.getTemplate()).isNotEqualTo("template");
- assertThat(license.getText()).isNotEqualTo("text");
- }
-
- @Test
- public void testLoadDefaultPermissions() throws Exception {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.loadDefaultPermissions();
- Assert.assertEquals(Permissions.values().length, qm.getPermissions().size());
- }
-
- @Test
- public void testLoadDefaultPersonas() throws Exception {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.loadDefaultPersonas();
- Assert.assertEquals(4, qm.getTeams().getTotal());
- }
-
- @Test
- public void testLoadDefaultRepositories() throws Exception {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.loadDefaultRepositories();
- Assert.assertEquals(17, qm.getAllRepositories().size());
- }
-
- @Test
- public void testLoadDefaultConfigProperties() throws Exception {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.loadDefaultConfigProperties();
- Assert.assertEquals(ConfigPropertyConstants.values().length, qm.getConfigProperties().size());
- }
-
- @Test
- public void testLoadDefaultNotificationPublishers() throws Exception {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.loadDefaultNotificationPublishers();
- Assert.assertEquals(DefaultNotificationPublishers.values().length, qm.getAllNotificationPublishers().size());
- }
-
- @Test
- public void testMetricsPartitionsForTodayAndTomorrow() {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.ensureMetricsPartitions();
- var today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
- var tomorrow = LocalDate.now().plusDays(1).format(DateTimeFormatter.BASIC_ISO_DATE);
- withJdbiHandle(handle -> {
- var metricsHandle = handle.attach(MetricsDao.class);
- assertThat(Collections.frequency(metricsHandle.getPortfolioMetricsPartitions(), "\"PORTFOLIOMETRICS_%s\"".formatted(today))).isEqualTo(1);
- assertThat(Collections.frequency(metricsHandle.getPortfolioMetricsPartitions(), "\"PORTFOLIOMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1);
- assertThat(Collections.frequency(metricsHandle.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(today))).isEqualTo(1);
- assertThat(Collections.frequency(metricsHandle.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1);
- assertThat(Collections.frequency(metricsHandle.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(today))).isEqualTo(1);
- assertThat(Collections.frequency(metricsHandle.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(tomorrow))).isEqualTo(1);
- return null;
- });
- }
-}
diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/NotificationQueryManagerTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/NotificationQueryManagerTest.java
index c0dbfe64f7..7e92a5a370 100644
--- a/apiserver/src/test/java/org/dependencytrack/persistence/NotificationQueryManagerTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/persistence/NotificationQueryManagerTest.java
@@ -23,20 +23,22 @@
import org.junit.Assert;
import org.junit.Test;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
+
public class NotificationQueryManagerTest extends PersistenceCapableTest {
@Test
public void testGetNotificationPublisher() {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.contextInitialized(null);
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultNotificationPublishers);
+
var publisher = qm.getNotificationPublisher(DefaultNotificationPublishers.SLACK.getPublisherName());
Assert.assertEquals("SlackPublisher", publisher.getPublisherClass());
}
@Test
public void testGetDefaultNotificationPublisher() {
- DefaultObjectGenerator generator = new DefaultObjectGenerator();
- generator.contextInitialized(null);
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultNotificationPublishers);
+
var publisher = qm.getDefaultNotificationPublisherByName(DefaultNotificationPublishers.SLACK.getPublisherName());
Assert.assertEquals("Slack", publisher.getName());
Assert.assertEquals("SlackPublisher", publisher.getPublisherClass());
diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsDaoTest.java b/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsDaoTest.java
index 825f4e5638..6352e0e413 100644
--- a/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsDaoTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsDaoTest.java
@@ -21,7 +21,6 @@
import org.dependencytrack.PersistenceCapableTest;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.DependencyMetrics;
-import org.dependencytrack.model.PortfolioMetrics;
import org.dependencytrack.model.ProjectMetrics;
import org.jdbi.v3.core.Handle;
import org.junit.After;
@@ -61,35 +60,6 @@ public void after() {
super.after();
}
- @Test
- public void testGetPortfolioMetricsForXDays() {
- metricsTestDao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 40);
- var metrics = new PortfolioMetrics();
- metrics.setVulnerabilities(4);
- metrics.setFirstOccurrence(Date.from(Instant.now()));
- metrics.setLastOccurrence(Date.from(Instant.now().minus(Duration.ofDays(40))));
- metricsTestDao.createPortfolioMetrics(metrics);
-
- metricsTestDao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 30);
- metrics = new PortfolioMetrics();
- metrics.setVulnerabilities(3);
- metrics.setFirstOccurrence(Date.from(Instant.now()));
- metrics.setLastOccurrence(Date.from(Instant.now().minus(Duration.ofDays(30))));
- metricsTestDao.createPortfolioMetrics(metrics);
-
- metricsTestDao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 20);
- metrics = new PortfolioMetrics();
- metrics.setVulnerabilities(2);
- metrics.setFirstOccurrence(Date.from(Instant.now()));
- metrics.setLastOccurrence(Date.from(Instant.now().minus(Duration.ofDays(20))));
- metricsTestDao.createPortfolioMetrics(metrics);
-
- var portfolioMetrics = metricsDao.getPortfolioMetricsSince(Instant.now().minus(Duration.ofDays(35)));
- assertThat(portfolioMetrics.size()).isEqualTo(2);
- assertThat(portfolioMetrics.get(0).getVulnerabilities()).isEqualTo(3);
- assertThat(portfolioMetrics.get(1).getVulnerabilities()).isEqualTo(2);
- }
-
@Test
public void testGetProjectMetricsForXDays() {
final var project = qm.createProject("acme-app", null, "1.0.0", null, null, null, null, false);
@@ -172,10 +142,8 @@ public void testGetDependencyMetricsForXDays() {
public void testCreateMetricsPartitionsForToday() {
metricsDao.createMetricsPartitionsForDate(LocalDate.now().toString(), LocalDate.now().plusDays(1).toString());
var today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
- var metricsPartition = metricsDao.getPortfolioMetricsPartitions();
- assertThat(metricsPartition.contains("\"PORTFOLIOMETRICS_%s\"".formatted(today))).isTrue();
- metricsPartition = metricsDao.getProjectMetricsPartitions();
+ var metricsPartition = metricsDao.getProjectMetricsPartitions();
assertThat(metricsPartition.contains("\"PROJECTMETRICS_%s\"".formatted(today))).isTrue();
metricsPartition = metricsDao.getDependencyMetricsPartitions();
@@ -184,7 +152,6 @@ public void testCreateMetricsPartitionsForToday() {
// If called again on the same day with partitions already created,
// It won't create more.
metricsDao.createMetricsPartitionsForDate(LocalDate.now().toString(), LocalDate.now().plusDays(1).toString());
- assertThat(Collections.frequency(metricsDao.getPortfolioMetricsPartitions(), "\"PORTFOLIOMETRICS_%s\"".formatted(today))).isEqualTo(1);
assertThat(Collections.frequency(metricsDao.getProjectMetricsPartitions(), "\"PROJECTMETRICS_%s\"".formatted(today))).isEqualTo(1);
assertThat(Collections.frequency(metricsDao.getDependencyMetricsPartitions(), "\"DEPENDENCYMETRICS_%s\"".formatted(today))).isEqualTo(1);
}
diff --git a/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsTestDao.java b/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsTestDao.java
index 0512860fcc..b04143f990 100644
--- a/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsTestDao.java
+++ b/apiserver/src/test/java/org/dependencytrack/persistence/jdbi/MetricsTestDao.java
@@ -19,7 +19,6 @@
package org.dependencytrack.persistence.jdbi;
import org.dependencytrack.model.DependencyMetrics;
-import org.dependencytrack.model.PortfolioMetrics;
import org.dependencytrack.model.ProjectMetrics;
import org.jdbi.v3.sqlobject.SqlObject;
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
@@ -31,25 +30,72 @@
public interface MetricsTestDao extends SqlObject {
-
- @SqlQuery("""
- INSERT INTO "PORTFOLIOMETRICS"(
- "PROJECTS", "COMPONENTS", "FIRST_OCCURRENCE", "LAST_OCCURRENCE", "CRITICAL", "HIGH", "MEDIUM", "LOW", "RISKSCORE",
- "SUPPRESSED", "VULNERABILITIES", "VULNERABLEPROJECTS", "VULNERABLECOMPONENTS")
- VALUES (:projects, :components, :firstOccurrence, :lastOccurrence, :critical, :high, :medium, :low, :inheritedRiskScore,
- :suppressed, :vulnerabilities, :vulnerableProjects, :vulnerableComponents)
- RETURNING *
- """)
- @RegisterBeanMapper(PortfolioMetrics.class)
- PortfolioMetrics createPortfolioMetrics(@BindBean PortfolioMetrics portfolioMetrics);
-
-
@SqlQuery("""
INSERT INTO "PROJECTMETRICS"(
- "PROJECT_ID", "COMPONENTS", "FIRST_OCCURRENCE", "LAST_OCCURRENCE", "CRITICAL", "HIGH", "MEDIUM", "LOW", "RISKSCORE",
- "SUPPRESSED", "VULNERABILITIES", "VULNERABLECOMPONENTS")
- VALUES (:projectId, :components, :firstOccurrence, :lastOccurrence, :critical, :high, :medium, :low, :inheritedRiskScore,
- :suppressed, :vulnerabilities, :vulnerableComponents)
+ "COMPONENTS"
+ , "CRITICAL"
+ , "FINDINGS_AUDITED"
+ , "FINDINGS_TOTAL"
+ , "FINDINGS_UNAUDITED"
+ , "FIRST_OCCURRENCE"
+ , "HIGH"
+ , "LAST_OCCURRENCE"
+ , "LOW"
+ , "MEDIUM"
+ , "POLICYVIOLATIONS_AUDITED"
+ , "POLICYVIOLATIONS_FAIL"
+ , "POLICYVIOLATIONS_INFO"
+ , "POLICYVIOLATIONS_LICENSE_AUDITED"
+ , "POLICYVIOLATIONS_LICENSE_TOTAL"
+ , "POLICYVIOLATIONS_LICENSE_UNAUDITED"
+ , "POLICYVIOLATIONS_OPERATIONAL_AUDITED"
+ , "POLICYVIOLATIONS_OPERATIONAL_TOTAL"
+ , "POLICYVIOLATIONS_OPERATIONAL_UNAUDITED"
+ , "POLICYVIOLATIONS_SECURITY_AUDITED"
+ , "POLICYVIOLATIONS_SECURITY_TOTAL"
+ , "POLICYVIOLATIONS_SECURITY_UNAUDITED"
+ , "POLICYVIOLATIONS_TOTAL"
+ , "POLICYVIOLATIONS_UNAUDITED"
+ , "POLICYVIOLATIONS_WARN"
+ , "PROJECT_ID"
+ , "RISKSCORE"
+ , "SUPPRESSED"
+ , "UNASSIGNED_SEVERITY"
+ , "VULNERABILITIES"
+ , "VULNERABLECOMPONENTS"
+ ) VALUES (
+ :components
+ , :critical
+ , :findingsAudited
+ , :findingsTotal
+ , :findingsUnaudited
+ , :firstOccurrence
+ , :high
+ , :lastOccurrence
+ , :low
+ , :medium
+ , :policyViolationsAudited
+ , :policyViolationsFail
+ , :policyViolationsInfo
+ , :policyViolationsLicenseAudited
+ , :policyViolationsLicenseTotal
+ , :policyViolationsLicenseUnaudited
+ , :policyViolationsOperationalAudited
+ , :policyViolationsOperationalTotal
+ , :policyViolationsOperationalUnaudited
+ , :policyViolationsSecurityAudited
+ , :policyViolationsSecurityTotal
+ , :policyViolationsSecurityUnaudited
+ , :policyViolationsTotal
+ , :policyViolationsUnaudited
+ , :policyViolationsWarn
+ , :projectId
+ , :inheritedRiskScore
+ , :suppressed
+ , :unassigned
+ , :vulnerabilities
+ , :vulnerableComponents
+ )
RETURNING *
""")
@RegisterBeanMapper(ProjectMetrics.class)
diff --git a/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java b/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java
index 65f589d6b1..45777105fe 100644
--- a/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/policy/cel/CelPolicyEngineTest.java
@@ -48,7 +48,7 @@
import org.dependencytrack.model.ViolationAnalysisState;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.model.VulnerabilityAlias;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.dependencytrack.plugin.PluginManager;
import org.dependencytrack.proto.filestorage.v1.FileMetadata;
import org.dependencytrack.tasks.BomUploadProcessingTask;
@@ -73,6 +73,7 @@
import static org.apache.commons.io.IOUtils.resourceToURL;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
public class CelPolicyEngineTest extends PersistenceCapableTest {
@@ -1949,8 +1950,10 @@ public void testEvaluateProjectWithNoLongerApplicableViolationWithAnalysis() {
@Test
@Ignore // Un-ignore for manual profiling purposes.
public void testWithBloatedBom() throws Exception {
- // Import all default objects (includes licenses and license groups).
- new DefaultObjectGenerator().contextInitialized(null);
+ useJdbiTransaction(handle -> {
+ DatabaseSeedingInitTask.seedDefaultLicenses(handle);
+ DatabaseSeedingInitTask.seedDefaultLicenseGroups(handle);
+ });
final var project = new Project();
project.setName("acme-app");
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java
index dc90233938..8397c070a1 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/LicenseResourceTest.java
@@ -24,7 +24,7 @@
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.model.License;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
import org.junit.ClassRule;
@@ -36,6 +36,8 @@
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
+
public class LicenseResourceTest extends ResourceTest {
@ClassRule
@@ -47,8 +49,8 @@ public class LicenseResourceTest extends ResourceTest {
@Override
public void before() throws Exception {
super.before();
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultLicenses();
+
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultLicenses);
}
@Test
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/MetricsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/MetricsResourceTest.java
index 91234e9fb6..f4a8b8ab58 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/MetricsResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/MetricsResourceTest.java
@@ -21,29 +21,30 @@
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFeature;
import alpine.server.filters.AuthorizationFeature;
-import jakarta.json.JsonArray;
-import jakarta.ws.rs.core.Response;
+import net.javacrumbs.jsonunit.core.Option;
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.auth.Permissions;
-import org.dependencytrack.persistence.jdbi.MetricsTestDao;
import org.dependencytrack.model.Component;
-import org.dependencytrack.model.PortfolioMetrics;
import org.dependencytrack.model.Project;
-import org.dependencytrack.util.DateUtil;
+import org.dependencytrack.model.ProjectMetrics;
+import org.dependencytrack.persistence.jdbi.MetricsTestDao;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.ClassRule;
import org.junit.Test;
-import java.time.Duration;
-import java.time.Instant;
+import jakarta.ws.rs.core.Response;
+import java.math.BigDecimal;
import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.function.Supplier;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;
import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle;
+import static org.hamcrest.Matchers.closeTo;
public class MetricsResourceTest extends ResourceTest {
@@ -323,38 +324,392 @@ public void refreshComponentMetricsAclTest() {
}
@Test
- public void getPortfolioMetricsXDaysAclTest() {
+ public void getCurrentPortfolioMetricsEmptyTest() {
+ initializeWithPermissions(Permissions.VIEW_PORTFOLIO);
+ enablePortfolioAccessControl();
+
+ final Response response = jersey
+ .target(V1_METRICS + "/portfolio/current")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThatJson(getPlainTextBody(response))
+ .isEqualTo(/* language=JSON */ """
+ {
+ "components": 0,
+ "critical": 0,
+ "findingsAudited": 0,
+ "findingsTotal": 0,
+ "findingsUnaudited": 0,
+ "firstOccurrence": "${json-unit.any-number}",
+ "high": 0,
+ "inheritedRiskScore": 0.0,
+ "lastOccurrence": "${json-unit.any-number}",
+ "low": 0,
+ "medium": 0,
+ "policyViolationsAudited": 0,
+ "policyViolationsFail": 0,
+ "policyViolationsInfo": 0,
+ "policyViolationsLicenseAudited": 0,
+ "policyViolationsLicenseTotal": 0,
+ "policyViolationsLicenseUnaudited": 0,
+ "policyViolationsOperationalAudited": 0,
+ "policyViolationsOperationalTotal": 0,
+ "policyViolationsOperationalUnaudited": 0,
+ "policyViolationsSecurityAudited": 0,
+ "policyViolationsSecurityTotal": 0,
+ "policyViolationsSecurityUnaudited": 0,
+ "policyViolationsTotal": 0,
+ "policyViolationsUnaudited": 0,
+ "policyViolationsWarn": 0,
+ "projects": 0,
+ "suppressed": 0,
+ "unassigned": 0,
+ "vulnerabilities": 0,
+ "vulnerableComponents": 0,
+ "vulnerableProjects": 0
+ }
+ """);
+ }
+
+ @Test
+ public void getCurrentPortfolioMetricsAclTest() {
initializeWithPermissions(Permissions.VIEW_PORTFOLIO);
enablePortfolioAccessControl();
+ final var accessibleProjectA = new Project();
+ accessibleProjectA.setName("acme-app-a");
+ accessibleProjectA.addAccessTeam(super.team);
+ qm.persist(accessibleProjectA);
+
+ final var accessibleProjectB = new Project();
+ accessibleProjectB.setName("acme-app-b");
+ accessibleProjectB.addAccessTeam(super.team);
+ qm.persist(accessibleProjectB);
+
+ final var inactiveAccessibleProject = new Project();
+ inactiveAccessibleProject.setName("acme-app-inactive");
+ inactiveAccessibleProject.setInactiveSince(new Date());
+ inactiveAccessibleProject.addAccessTeam(super.team);
+ qm.persist(inactiveAccessibleProject);
+
+ final var inaccessibleProject = new Project();
+ inaccessibleProject.setName("acme-app-inaccessible");
+ qm.persist(inaccessibleProject);
+
+ final var today = LocalDate.now();
+
useJdbiHandle(handle -> {
var dao = handle.attach(MetricsTestDao.class);
- dao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 30);
- var metrics = new PortfolioMetrics();
- metrics.setVulnerabilities(3);
- metrics.setFirstOccurrence(Date.from(Instant.now()));
- metrics.setLastOccurrence(Date.from(Instant.now().minus(Duration.ofDays(30))));
- dao.createPortfolioMetrics(metrics);
-
- dao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 20);
- metrics = new PortfolioMetrics();
- metrics.setVulnerabilities(2);
- metrics.setFirstOccurrence(Date.from(Instant.now()));
- metrics.setLastOccurrence(Date.from(Instant.now().minus(Duration.ofDays(20))));
- dao.createPortfolioMetrics(metrics);
+
+ dao.createMetricsPartitionsForDate("PROJECTMETRICS", today);
+ dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(1));
+ dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(2));
+
+ {
+ // Create metrics for "yesterday".
+
+ var accessibleProjectAMetrics = new ProjectMetrics();
+ accessibleProjectAMetrics.setProjectId(accessibleProjectA.getId());
+ accessibleProjectAMetrics.setComponents(2);
+ accessibleProjectAMetrics.setFirstOccurrence(Date.from(today.minusDays(1).atTime(1, 1).atZone(ZoneId.systemDefault()).toInstant()));
+ accessibleProjectAMetrics.setLastOccurrence(accessibleProjectAMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(accessibleProjectAMetrics);
+ }
+
+ {
+ // Create metrics for "today".
+
+ // Do not create metrics for accessibleProjectA.
+ // Its metrics from "yesterday" are supposed to carry over to "today".
+
+ var accessibleProjectBMetrics = new ProjectMetrics();
+ accessibleProjectBMetrics.setProjectId(accessibleProjectB.getId());
+ accessibleProjectBMetrics.setComponents(1);
+ accessibleProjectBMetrics.setFirstOccurrence(Date.from(today.atTime(1, 1).atZone(ZoneId.systemDefault()).toInstant()));
+ accessibleProjectBMetrics.setLastOccurrence(accessibleProjectBMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(accessibleProjectBMetrics);
+
+ // Metrics of inactive projects must not be considered.
+ var inactiveAccessibleProjectMetrics = new ProjectMetrics();
+ inactiveAccessibleProjectMetrics.setProjectId(inactiveAccessibleProject.getId());
+ inactiveAccessibleProjectMetrics.setComponents(111);
+ inactiveAccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(2, 2).atZone(ZoneId.systemDefault()).toInstant()));
+ inactiveAccessibleProjectMetrics.setLastOccurrence(inactiveAccessibleProjectMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(inactiveAccessibleProjectMetrics);
+
+ // Metrics of inaccessible projects must not be considered.
+ var inaccessibleProjectMetrics = new ProjectMetrics();
+ inaccessibleProjectMetrics.setProjectId(inaccessibleProject.getId());
+ inaccessibleProjectMetrics.setComponents(666);
+ inaccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(3, 3).atZone(ZoneId.systemDefault()).toInstant()));
+ inaccessibleProjectMetrics.setLastOccurrence(inaccessibleProjectMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(inaccessibleProjectMetrics);
+ }
});
- final Supplier responseSupplier = () -> jersey
- .target(V1_METRICS + "/portfolio/25/days")
+ final Response response = jersey
+ .target(V1_METRICS + "/portfolio/current")
.request()
.header(X_API_KEY, apiKey)
.get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThatJson(getPlainTextBody(response))
+ .withOptions(Option.IGNORING_EXTRA_FIELDS)
+ .isEqualTo(/* language=JSON */ """
+ {
+ "projects": 2,
+ "components": 3
+ }
+ """);
+ }
- Response response = responseSupplier.get();
+ @Test
+ public void getPortfolioMetricsXDaysAclTest() {
+ initializeWithPermissions(Permissions.VIEW_PORTFOLIO);
+ enablePortfolioAccessControl();
+
+ final var accessibleProjectA = new Project();
+ accessibleProjectA.setName("acme-app-a");
+ accessibleProjectA.addAccessTeam(super.team);
+ qm.persist(accessibleProjectA);
+
+ final var accessibleProjectB = new Project();
+ accessibleProjectB.setName("acme-app-b");
+ accessibleProjectB.addAccessTeam(super.team);
+ qm.persist(accessibleProjectB);
+
+ final var inactiveAccessibleProject = new Project();
+ inactiveAccessibleProject.setName("acme-app-inactive");
+ inactiveAccessibleProject.setInactiveSince(new Date());
+ inactiveAccessibleProject.addAccessTeam(super.team);
+ qm.persist(inactiveAccessibleProject);
+
+ final var inaccessibleProject = new Project();
+ inaccessibleProject.setName("acme-app-inaccessible");
+ qm.persist(inaccessibleProject);
+
+ final var today = LocalDate.now();
+
+ useJdbiHandle(handle -> {
+ var dao = handle.attach(MetricsTestDao.class);
+
+ dao.createMetricsPartitionsForDate("PROJECTMETRICS", today);
+ dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(1));
+ dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(2));
+
+ {
+ // Create metrics for "yesterday".
+
+ var accessibleProjectAMetrics = new ProjectMetrics();
+ accessibleProjectAMetrics.setProjectId(accessibleProjectA.getId());
+ accessibleProjectAMetrics.setComponents(1);
+ accessibleProjectAMetrics.setCritical(1);
+ accessibleProjectAMetrics.setFindingsAudited(1);
+ accessibleProjectAMetrics.setFindingsTotal(1);
+ accessibleProjectAMetrics.setFindingsUnaudited(1);
+ accessibleProjectAMetrics.setHigh(1);
+ accessibleProjectAMetrics.setInheritedRiskScore(1.1);
+ accessibleProjectAMetrics.setLow(1);
+ accessibleProjectAMetrics.setMedium(1);
+ accessibleProjectAMetrics.setPolicyViolationsAudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsFail(1);
+ accessibleProjectAMetrics.setPolicyViolationsInfo(1);
+ accessibleProjectAMetrics.setPolicyViolationsLicenseAudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsLicenseTotal(1);
+ accessibleProjectAMetrics.setPolicyViolationsLicenseUnaudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsOperationalAudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsOperationalTotal(1);
+ accessibleProjectAMetrics.setPolicyViolationsOperationalUnaudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsSecurityAudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsSecurityTotal(1);
+ accessibleProjectAMetrics.setPolicyViolationsSecurityUnaudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsTotal(1);
+ accessibleProjectAMetrics.setPolicyViolationsUnaudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsWarn(1);
+ accessibleProjectAMetrics.setSuppressed(1);
+ accessibleProjectAMetrics.setUnassigned(1);
+ accessibleProjectAMetrics.setVulnerabilities(1);
+ accessibleProjectAMetrics.setVulnerableComponents(1);
+ accessibleProjectAMetrics.setFirstOccurrence(Date.from(today.minusDays(1).atTime(1, 1).atZone(ZoneId.systemDefault()).toInstant()));
+ accessibleProjectAMetrics.setLastOccurrence(accessibleProjectAMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(accessibleProjectAMetrics);
+ }
+
+ {
+ // Create metrics for "today".
+
+ // Do not create metrics for accessibleProjectA.
+ // Its metrics from "yesterday" are supposed to carry over to "today".
+
+ var accessibleProjectBMetrics = new ProjectMetrics();
+ accessibleProjectBMetrics.setProjectId(accessibleProjectB.getId());
+ accessibleProjectBMetrics.setComponents(2);
+ accessibleProjectBMetrics.setCritical(2);
+ accessibleProjectBMetrics.setFindingsAudited(2);
+ accessibleProjectBMetrics.setFindingsTotal(2);
+ accessibleProjectBMetrics.setFindingsUnaudited(2);
+ accessibleProjectBMetrics.setHigh(2);
+ accessibleProjectBMetrics.setInheritedRiskScore(2.2);
+ accessibleProjectBMetrics.setLow(2);
+ accessibleProjectBMetrics.setMedium(2);
+ accessibleProjectBMetrics.setPolicyViolationsAudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsFail(2);
+ accessibleProjectBMetrics.setPolicyViolationsInfo(2);
+ accessibleProjectBMetrics.setPolicyViolationsLicenseAudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsLicenseTotal(2);
+ accessibleProjectBMetrics.setPolicyViolationsLicenseUnaudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsOperationalAudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsOperationalTotal(2);
+ accessibleProjectBMetrics.setPolicyViolationsOperationalUnaudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsSecurityAudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsSecurityTotal(2);
+ accessibleProjectBMetrics.setPolicyViolationsSecurityUnaudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsTotal(2);
+ accessibleProjectBMetrics.setPolicyViolationsUnaudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsWarn(2);
+ accessibleProjectBMetrics.setSuppressed(2);
+ accessibleProjectBMetrics.setUnassigned(2);
+ accessibleProjectBMetrics.setVulnerabilities(2);
+ accessibleProjectBMetrics.setVulnerableComponents(2);
+ accessibleProjectBMetrics.setFirstOccurrence(Date.from(today.atTime(2, 2).atZone(ZoneId.systemDefault()).toInstant()));
+ accessibleProjectBMetrics.setLastOccurrence(accessibleProjectBMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(accessibleProjectBMetrics);
+
+ // Metrics of inactive projects must not be considered.
+ var inactiveAccessibleProjectMetrics = new ProjectMetrics();
+ inactiveAccessibleProjectMetrics.setProjectId(inactiveAccessibleProject.getId());
+ inactiveAccessibleProjectMetrics.setComponents(111);
+ inactiveAccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(2, 2).atZone(ZoneId.systemDefault()).toInstant()));
+ inactiveAccessibleProjectMetrics.setLastOccurrence(inactiveAccessibleProjectMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(inactiveAccessibleProjectMetrics);
+
+ // Metrics of inaccessible projects must not be considered.
+ var inaccessibleProjectMetrics = new ProjectMetrics();
+ inaccessibleProjectMetrics.setProjectId(inaccessibleProject.getId());
+ inaccessibleProjectMetrics.setVulnerabilities(666);
+ inaccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(3, 3).atZone(ZoneId.systemDefault()).toInstant()));
+ inaccessibleProjectMetrics.setLastOccurrence(inaccessibleProjectMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(inaccessibleProjectMetrics);
+ }
+ });
+
+ final Response response = jersey
+ .target(V1_METRICS + "/portfolio/3/days")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
assertThat(response.getStatus()).isEqualTo(200);
- JsonArray json = parseJsonArray(response);
- assertThat(json.size()).isEqualTo(1);
- assertThat(json.getJsonObject(0).getInt("vulnerabilities")).isEqualTo(2);
+ assertThatJson(getPlainTextBody(response))
+ .withMatcher("inheritedRiskScoreDay2", closeTo(BigDecimal.valueOf(1.1), BigDecimal.valueOf(0.01)))
+ .withMatcher("inheritedRiskScoreDay3", closeTo(BigDecimal.valueOf(3.3), BigDecimal.valueOf(0.01)))
+ .isEqualTo(/* language=JSON */ """
+ [
+ {
+ "components": 0,
+ "critical": 0,
+ "findingsAudited": 0,
+ "findingsTotal": 0,
+ "findingsUnaudited": 0,
+ "firstOccurrence": "${json-unit.any-number}",
+ "high": 0,
+ "inheritedRiskScore": 0.0,
+ "lastOccurrence": "${json-unit.any-number}",
+ "low": 0,
+ "medium": 0,
+ "policyViolationsAudited": 0,
+ "policyViolationsFail": 0,
+ "policyViolationsInfo": 0,
+ "policyViolationsLicenseAudited": 0,
+ "policyViolationsLicenseTotal": 0,
+ "policyViolationsLicenseUnaudited": 0,
+ "policyViolationsOperationalAudited": 0,
+ "policyViolationsOperationalTotal": 0,
+ "policyViolationsOperationalUnaudited": 0,
+ "policyViolationsSecurityAudited": 0,
+ "policyViolationsSecurityTotal": 0,
+ "policyViolationsSecurityUnaudited": 0,
+ "policyViolationsTotal": 0,
+ "policyViolationsUnaudited": 0,
+ "policyViolationsWarn": 0,
+ "projects": 0,
+ "suppressed": 0,
+ "unassigned": 0,
+ "vulnerabilities": 0,
+ "vulnerableComponents": 0,
+ "vulnerableProjects": 0
+ },
+ {
+ "components": 1,
+ "critical": 1,
+ "findingsAudited": 1,
+ "findingsTotal": 1,
+ "findingsUnaudited": 1,
+ "firstOccurrence": "${json-unit.any-number}",
+ "high": 1,
+ "inheritedRiskScore": "${json-unit.matches:inheritedRiskScoreDay2}",
+ "lastOccurrence": "${json-unit.any-number}",
+ "low": 1,
+ "medium": 1,
+ "policyViolationsAudited": 1,
+ "policyViolationsFail": 1,
+ "policyViolationsInfo": 1,
+ "policyViolationsLicenseAudited": 1,
+ "policyViolationsLicenseTotal": 1,
+ "policyViolationsLicenseUnaudited": 1,
+ "policyViolationsOperationalAudited": 1,
+ "policyViolationsOperationalTotal": 1,
+ "policyViolationsOperationalUnaudited": 1,
+ "policyViolationsSecurityAudited": 1,
+ "policyViolationsSecurityTotal": 1,
+ "policyViolationsSecurityUnaudited": 1,
+ "policyViolationsTotal": 1,
+ "policyViolationsUnaudited": 1,
+ "policyViolationsWarn": 1,
+ "projects": 1,
+ "suppressed": 1,
+ "unassigned": 1,
+ "vulnerabilities": 1,
+ "vulnerableComponents": 1,
+ "vulnerableProjects": 1
+ },
+ {
+ "components": 3,
+ "critical": 3,
+ "findingsAudited": 3,
+ "findingsTotal": 3,
+ "findingsUnaudited": 3,
+ "firstOccurrence": "${json-unit.any-number}",
+ "high": 3,
+ "inheritedRiskScore": "${json-unit.matches:inheritedRiskScoreDay3}",
+ "lastOccurrence": "${json-unit.any-number}",
+ "low": 3,
+ "medium": 3,
+ "policyViolationsAudited": 3,
+ "policyViolationsFail": 3,
+ "policyViolationsInfo": 3,
+ "policyViolationsLicenseAudited": 3,
+ "policyViolationsLicenseTotal": 3,
+ "policyViolationsLicenseUnaudited": 3,
+ "policyViolationsOperationalAudited": 3,
+ "policyViolationsOperationalTotal": 3,
+ "policyViolationsOperationalUnaudited": 3,
+ "policyViolationsSecurityAudited": 3,
+ "policyViolationsSecurityTotal": 3,
+ "policyViolationsSecurityUnaudited": 3,
+ "policyViolationsTotal": 3,
+ "policyViolationsUnaudited": 3,
+ "policyViolationsWarn": 3,
+ "projects": 2,
+ "suppressed": 3,
+ "unassigned": 3,
+ "vulnerabilities": 3,
+ "vulnerableComponents": 3,
+ "vulnerableProjects": 2
+ }
+ ]
+ """);
}
@Test
@@ -362,33 +717,245 @@ public void getPortfolioMetricsSinceAclTest() {
initializeWithPermissions(Permissions.VIEW_PORTFOLIO);
enablePortfolioAccessControl();
+ final var accessibleProjectA = new Project();
+ accessibleProjectA.setName("acme-app-a");
+ accessibleProjectA.addAccessTeam(super.team);
+ qm.persist(accessibleProjectA);
+
+ final var accessibleProjectB = new Project();
+ accessibleProjectB.setName("acme-app-b");
+ accessibleProjectB.addAccessTeam(super.team);
+ qm.persist(accessibleProjectB);
+
+ final var inactiveAccessibleProject = new Project();
+ inactiveAccessibleProject.setName("acme-app-inactive");
+ inactiveAccessibleProject.setInactiveSince(new Date());
+ inactiveAccessibleProject.addAccessTeam(super.team);
+ qm.persist(inactiveAccessibleProject);
+
+ final var inaccessibleProject = new Project();
+ inaccessibleProject.setName("acme-app-inaccessible");
+ qm.persist(inaccessibleProject);
+
+ final var today = LocalDate.now();
+
useJdbiHandle(handle -> {
var dao = handle.attach(MetricsTestDao.class);
- dao.createMetricsPartitionsForDate("PORTFOLIOMETRICS", LocalDate.of(2025, 1, 1));
- var metrics = new PortfolioMetrics();
- metrics.setVulnerabilities(3);
- metrics.setFirstOccurrence(Date.from(Instant.now()));
- metrics.setLastOccurrence(DateUtil.parseShortDate("20250101"));
- dao.createPortfolioMetrics(metrics);
-
- dao.createMetricsPartitionsForDate("PORTFOLIOMETRICS", LocalDate.of(2025, 2, 1));
- metrics = new PortfolioMetrics();
- metrics.setVulnerabilities(2);
- metrics.setFirstOccurrence(Date.from(Instant.now()));
- metrics.setLastOccurrence(DateUtil.parseShortDate("20250201"));
- dao.createPortfolioMetrics(metrics);
+
+ dao.createMetricsPartitionsForDate("PROJECTMETRICS", today);
+ dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(1));
+ dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(2));
+
+ {
+ // Create metrics for "yesterday".
+
+ var accessibleProjectAMetrics = new ProjectMetrics();
+ accessibleProjectAMetrics.setProjectId(accessibleProjectA.getId());
+ accessibleProjectAMetrics.setComponents(1);
+ accessibleProjectAMetrics.setCritical(1);
+ accessibleProjectAMetrics.setFindingsAudited(1);
+ accessibleProjectAMetrics.setFindingsTotal(1);
+ accessibleProjectAMetrics.setFindingsUnaudited(1);
+ accessibleProjectAMetrics.setHigh(1);
+ accessibleProjectAMetrics.setInheritedRiskScore(1.1);
+ accessibleProjectAMetrics.setLow(1);
+ accessibleProjectAMetrics.setMedium(1);
+ accessibleProjectAMetrics.setPolicyViolationsAudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsFail(1);
+ accessibleProjectAMetrics.setPolicyViolationsInfo(1);
+ accessibleProjectAMetrics.setPolicyViolationsLicenseAudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsLicenseTotal(1);
+ accessibleProjectAMetrics.setPolicyViolationsLicenseUnaudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsOperationalAudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsOperationalTotal(1);
+ accessibleProjectAMetrics.setPolicyViolationsOperationalUnaudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsSecurityAudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsSecurityTotal(1);
+ accessibleProjectAMetrics.setPolicyViolationsSecurityUnaudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsTotal(1);
+ accessibleProjectAMetrics.setPolicyViolationsUnaudited(1);
+ accessibleProjectAMetrics.setPolicyViolationsWarn(1);
+ accessibleProjectAMetrics.setSuppressed(1);
+ accessibleProjectAMetrics.setUnassigned(1);
+ accessibleProjectAMetrics.setVulnerabilities(1);
+ accessibleProjectAMetrics.setVulnerableComponents(1);
+ accessibleProjectAMetrics.setFirstOccurrence(Date.from(today.minusDays(1).atTime(1, 1).atZone(ZoneId.systemDefault()).toInstant()));
+ accessibleProjectAMetrics.setLastOccurrence(accessibleProjectAMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(accessibleProjectAMetrics);
+ }
+
+ {
+ // Create metrics for "today".
+
+ // Do not create metrics for accessibleProjectA.
+ // Its metrics from "yesterday" are supposed to carry over to "today".
+
+ var accessibleProjectBMetrics = new ProjectMetrics();
+ accessibleProjectBMetrics.setProjectId(accessibleProjectB.getId());
+ accessibleProjectBMetrics.setComponents(2);
+ accessibleProjectBMetrics.setCritical(2);
+ accessibleProjectBMetrics.setFindingsAudited(2);
+ accessibleProjectBMetrics.setFindingsTotal(2);
+ accessibleProjectBMetrics.setFindingsUnaudited(2);
+ accessibleProjectBMetrics.setHigh(2);
+ accessibleProjectBMetrics.setInheritedRiskScore(2.2);
+ accessibleProjectBMetrics.setLow(2);
+ accessibleProjectBMetrics.setMedium(2);
+ accessibleProjectBMetrics.setPolicyViolationsAudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsFail(2);
+ accessibleProjectBMetrics.setPolicyViolationsInfo(2);
+ accessibleProjectBMetrics.setPolicyViolationsLicenseAudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsLicenseTotal(2);
+ accessibleProjectBMetrics.setPolicyViolationsLicenseUnaudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsOperationalAudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsOperationalTotal(2);
+ accessibleProjectBMetrics.setPolicyViolationsOperationalUnaudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsSecurityAudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsSecurityTotal(2);
+ accessibleProjectBMetrics.setPolicyViolationsSecurityUnaudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsTotal(2);
+ accessibleProjectBMetrics.setPolicyViolationsUnaudited(2);
+ accessibleProjectBMetrics.setPolicyViolationsWarn(2);
+ accessibleProjectBMetrics.setSuppressed(2);
+ accessibleProjectBMetrics.setUnassigned(2);
+ accessibleProjectBMetrics.setVulnerabilities(2);
+ accessibleProjectBMetrics.setVulnerableComponents(2);
+ accessibleProjectBMetrics.setFirstOccurrence(Date.from(today.atTime(2, 2).atZone(ZoneId.systemDefault()).toInstant()));
+ accessibleProjectBMetrics.setLastOccurrence(accessibleProjectBMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(accessibleProjectBMetrics);
+
+ // Metrics of inactive projects must not be considered.
+ var inactiveAccessibleProjectMetrics = new ProjectMetrics();
+ inactiveAccessibleProjectMetrics.setProjectId(inactiveAccessibleProject.getId());
+ inactiveAccessibleProjectMetrics.setComponents(111);
+ inactiveAccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(2, 2).atZone(ZoneId.systemDefault()).toInstant()));
+ inactiveAccessibleProjectMetrics.setLastOccurrence(inactiveAccessibleProjectMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(inactiveAccessibleProjectMetrics);
+
+ // Metrics of inaccessible projects must not be considered.
+ var inaccessibleProjectMetrics = new ProjectMetrics();
+ inaccessibleProjectMetrics.setProjectId(inaccessibleProject.getId());
+ inaccessibleProjectMetrics.setVulnerabilities(666);
+ inaccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(3, 3).atZone(ZoneId.systemDefault()).toInstant()));
+ inaccessibleProjectMetrics.setLastOccurrence(inaccessibleProjectMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(inaccessibleProjectMetrics);
+ }
});
- final Supplier responseSupplier = () -> jersey
- .target(V1_METRICS + "/portfolio/since/20250201")
+ final Response response = jersey
+ .target(V1_METRICS + "/portfolio/since/" + today.minusDays(2).format(DateTimeFormatter.ofPattern("yyyyMMdd")))
.request()
.header(X_API_KEY, apiKey)
.get();
-
- Response response = responseSupplier.get();
assertThat(response.getStatus()).isEqualTo(200);
- JsonArray json = parseJsonArray(response);
- assertThat(json.size()).isEqualTo(1);
- assertThat(json.getJsonObject(0).getInt("vulnerabilities")).isEqualTo(2);
+ assertThatJson(getPlainTextBody(response))
+ .withMatcher("inheritedRiskScoreDay2", closeTo(BigDecimal.valueOf(1.1), BigDecimal.valueOf(0.01)))
+ .withMatcher("inheritedRiskScoreDay3", closeTo(BigDecimal.valueOf(3.3), BigDecimal.valueOf(0.01)))
+ .isEqualTo(/* language=JSON */ """
+ [
+ {
+ "components": 0,
+ "critical": 0,
+ "findingsAudited": 0,
+ "findingsTotal": 0,
+ "findingsUnaudited": 0,
+ "firstOccurrence": "${json-unit.any-number}",
+ "high": 0,
+ "inheritedRiskScore": 0.0,
+ "lastOccurrence": "${json-unit.any-number}",
+ "low": 0,
+ "medium": 0,
+ "policyViolationsAudited": 0,
+ "policyViolationsFail": 0,
+ "policyViolationsInfo": 0,
+ "policyViolationsLicenseAudited": 0,
+ "policyViolationsLicenseTotal": 0,
+ "policyViolationsLicenseUnaudited": 0,
+ "policyViolationsOperationalAudited": 0,
+ "policyViolationsOperationalTotal": 0,
+ "policyViolationsOperationalUnaudited": 0,
+ "policyViolationsSecurityAudited": 0,
+ "policyViolationsSecurityTotal": 0,
+ "policyViolationsSecurityUnaudited": 0,
+ "policyViolationsTotal": 0,
+ "policyViolationsUnaudited": 0,
+ "policyViolationsWarn": 0,
+ "projects": 0,
+ "suppressed": 0,
+ "unassigned": 0,
+ "vulnerabilities": 0,
+ "vulnerableComponents": 0,
+ "vulnerableProjects": 0
+ },
+ {
+ "components": 1,
+ "critical": 1,
+ "findingsAudited": 1,
+ "findingsTotal": 1,
+ "findingsUnaudited": 1,
+ "firstOccurrence": "${json-unit.any-number}",
+ "high": 1,
+ "inheritedRiskScore": "${json-unit.matches:inheritedRiskScoreDay2}",
+ "lastOccurrence": "${json-unit.any-number}",
+ "low": 1,
+ "medium": 1,
+ "policyViolationsAudited": 1,
+ "policyViolationsFail": 1,
+ "policyViolationsInfo": 1,
+ "policyViolationsLicenseAudited": 1,
+ "policyViolationsLicenseTotal": 1,
+ "policyViolationsLicenseUnaudited": 1,
+ "policyViolationsOperationalAudited": 1,
+ "policyViolationsOperationalTotal": 1,
+ "policyViolationsOperationalUnaudited": 1,
+ "policyViolationsSecurityAudited": 1,
+ "policyViolationsSecurityTotal": 1,
+ "policyViolationsSecurityUnaudited": 1,
+ "policyViolationsTotal": 1,
+ "policyViolationsUnaudited": 1,
+ "policyViolationsWarn": 1,
+ "projects": 1,
+ "suppressed": 1,
+ "unassigned": 1,
+ "vulnerabilities": 1,
+ "vulnerableComponents": 1,
+ "vulnerableProjects": 1
+ },
+ {
+ "components": 3,
+ "critical": 3,
+ "findingsAudited": 3,
+ "findingsTotal": 3,
+ "findingsUnaudited": 3,
+ "firstOccurrence": "${json-unit.any-number}",
+ "high": 3,
+ "inheritedRiskScore": "${json-unit.matches:inheritedRiskScoreDay3}",
+ "lastOccurrence": "${json-unit.any-number}",
+ "low": 3,
+ "medium": 3,
+ "policyViolationsAudited": 3,
+ "policyViolationsFail": 3,
+ "policyViolationsInfo": 3,
+ "policyViolationsLicenseAudited": 3,
+ "policyViolationsLicenseTotal": 3,
+ "policyViolationsLicenseUnaudited": 3,
+ "policyViolationsOperationalAudited": 3,
+ "policyViolationsOperationalTotal": 3,
+ "policyViolationsOperationalUnaudited": 3,
+ "policyViolationsSecurityAudited": 3,
+ "policyViolationsSecurityTotal": 3,
+ "policyViolationsSecurityUnaudited": 3,
+ "policyViolationsTotal": 3,
+ "policyViolationsUnaudited": 3,
+ "policyViolationsWarn": 3,
+ "projects": 2,
+ "suppressed": 3,
+ "unassigned": 3,
+ "vulnerabilities": 3,
+ "vulnerableComponents": 3,
+ "vulnerableProjects": 2
+ }
+ ]
+ """);
}
}
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java
index 479fa5e6a4..7506111f2f 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationPublisherResourceTest.java
@@ -22,11 +22,6 @@
import alpine.notification.NotificationLevel;
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFeature;
-import jakarta.json.JsonArray;
-import jakarta.json.JsonObject;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.model.ConfigPropertyConstants;
@@ -35,19 +30,25 @@
import org.dependencytrack.notification.NotificationGroup;
import org.dependencytrack.notification.NotificationScope;
import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.dependencytrack.notification.publisher.PublisherClass.SendMailPublisher;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
public class NotificationPublisherResourceTest extends ResourceTest {
@@ -60,8 +61,8 @@ public class NotificationPublisherResourceTest extends ResourceTest {
@Before
public void before() throws Exception {
super.before();
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultNotificationPublishers();
+
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultNotificationPublishers);
}
@Test
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java
index b7596752f5..aa74b5a2ad 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/NotificationRuleResourceTest.java
@@ -23,11 +23,6 @@
import alpine.notification.NotificationLevel;
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFeature;
-import jakarta.json.JsonArray;
-import jakarta.json.JsonObject;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
import net.javacrumbs.jsonunit.core.Option;
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
@@ -37,7 +32,7 @@
import org.dependencytrack.notification.NotificationGroup;
import org.dependencytrack.notification.NotificationScope;
import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
@@ -45,6 +40,11 @@
import org.junit.ClassRule;
import org.junit.Test;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -56,6 +56,7 @@
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.dependencytrack.notification.publisher.PublisherClass.SendMailPublisher;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
import static org.hamcrest.Matchers.equalTo;
public class NotificationRuleResourceTest extends ResourceTest {
@@ -69,8 +70,8 @@ public class NotificationRuleResourceTest extends ResourceTest {
@Before
public void before() throws Exception {
super.before();
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultNotificationPublishers();
+
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultNotificationPublishers);
}
@Test
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/OpenApiResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/OpenApiResourceTest.java
similarity index 98%
rename from apiserver/src/test/java/org/dependencytrack/resources/OpenApiResourceTest.java
rename to apiserver/src/test/java/org/dependencytrack/resources/v1/OpenApiResourceTest.java
index c7fda53cbd..46f24cecac 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/OpenApiResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/OpenApiResourceTest.java
@@ -16,12 +16,11 @@
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/
-package org.dependencytrack.resources;
+package org.dependencytrack.resources.v1;
import alpine.server.filters.ApiFilter;
import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
-import jakarta.ws.rs.core.Response;
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
import org.glassfish.jersey.client.ClientProperties;
@@ -29,6 +28,7 @@
import org.junit.ClassRule;
import org.junit.Test;
+import jakarta.ws.rs.core.Response;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java
index 90c3ea8698..0f4778baf5 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java
@@ -27,7 +27,7 @@
import org.dependencytrack.ResourceTest;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.model.Role;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
import org.junit.Before;
@@ -40,12 +40,13 @@
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
-
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
+
public class PermissionResourceTest extends ResourceTest {
@ClassRule
@@ -57,8 +58,8 @@ public class PermissionResourceTest extends ResourceTest {
@Before
public void before() throws Exception {
super.before();
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultPermissions();
+
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultPermissions);
}
@Test
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java
index b7dde48e7b..63717c3d97 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java
@@ -27,14 +27,6 @@
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFeature;
import com.github.packageurl.PackageURL;
-import jakarta.json.Json;
-import jakarta.json.JsonArray;
-import jakarta.json.JsonObject;
-import jakarta.json.JsonObjectBuilder;
-import jakarta.ws.rs.HttpMethod;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.auth.Permissions;
@@ -79,6 +71,14 @@
import org.junit.ClassRule;
import org.junit.Test;
+import jakarta.json.Json;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonObjectBuilder;
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
@@ -835,6 +835,50 @@ public void getProjectsConciseFilterByNameTest() {
""");
}
+ @Test
+ public void getProjectsConciseFilterByVersionTest() {
+ final var projectA = new Project();
+ projectA.setName("acme-app-a");
+ projectA.setVersion("1.0");
+ qm.persist(projectA);
+
+ final var projectB = new Project();
+ projectB.setName("acme-app-b");
+ projectB.setVersion("2.0");
+ qm.persist(projectB);
+
+ // Should not return results for partial matches.
+ Response response = jersey.target(V1_PROJECT + "/concise")
+ .queryParam("version", "0")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("0");
+ assertThat(getPlainTextBody(response)).isEqualTo("[]");
+
+ // Should return results for exact matches.
+ response = jersey.target(V1_PROJECT + "/concise")
+ .queryParam("version", "2.0")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1");
+ assertThatJson(getPlainTextBody(response)).isEqualTo("""
+ [
+ {
+ "uuid": "${json-unit.any-string}",
+ "name": "acme-app-b",
+ "version": "2.0",
+ "active": true,
+ "isLatest": false,
+ "hasChildren": false
+ }
+ ]
+ """);
+ }
+
@Test
public void getProjectsConciseFilterByTagTest() {
final var projectA = new Project();
@@ -1155,15 +1199,15 @@ public void getProjectsConciseWithLatestMetricsTest() {
"high": 3,
"low": 4,
"medium": 5,
- "policyViolationsFail": 0,
- "policyViolationsInfo": 0,
- "policyViolationsLicenseTotal": 0,
- "policyViolationsOperationalTotal": 0,
- "policyViolationsSecurityTotal": 0,
- "policyViolationsTotal": 0,
- "policyViolationsWarn": 0,
+ "policyViolationsFail": 6,
+ "policyViolationsInfo": 7,
+ "policyViolationsLicenseTotal": 8,
+ "policyViolationsOperationalTotal": 9,
+ "policyViolationsSecurityTotal": 10,
+ "policyViolationsTotal": 11,
+ "policyViolationsWarn": 12,
"inheritedRiskScore": 13.13,
- "unassigned": 0,
+ "unassigned": 14,
"vulnerabilities": 15
}
}
@@ -1445,6 +1489,56 @@ public void getProjectChildrenConciseFilterByNameTest() {
""");
}
+ @Test
+ public void getProjectChildrenConciseFilterByVersionTest() {
+ final var parentProject = new Project();
+ parentProject.setName("acme-app");
+ qm.persist(parentProject);
+
+ final var childProjectA = new Project();
+ childProjectA.setParent(parentProject);
+ childProjectA.setName("acme-child-app-a");
+ childProjectA.setVersion("1.0");
+ qm.persist(childProjectA);
+
+ final var childProjectB = new Project();
+ childProjectB.setParent(parentProject);
+ childProjectB.setName("acme-child-app-b");
+ childProjectB.setVersion("2.0");
+ qm.persist(childProjectB);
+
+ // Should not return results for partial matches.
+ Response response = jersey.target(V1_PROJECT + "/concise/" + parentProject.getUuid() + "/children")
+ .queryParam("version", "0")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("0");
+ assertThat(getPlainTextBody(response)).isEqualTo("[]");
+
+ // Should return results for exact matches.
+ response = jersey.target(V1_PROJECT + "/concise/" + parentProject.getUuid() + "/children")
+ .queryParam("version", "1.0")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isEqualTo("1");
+ assertThatJson(getPlainTextBody(response)).isEqualTo("""
+ [
+ {
+ "uuid": "${json-unit.any-string}",
+ "name": "acme-child-app-a",
+ "version": "1.0",
+ "active": true,
+ "isLatest": false,
+ "hasChildren": false
+ }
+ ]
+ """);
+ }
+
@Test
public void getProjectChildrenConciseFilterByTagTest() {
final var parentProject = new Project();
@@ -1641,15 +1735,15 @@ public void getProjectChildrenConciseWithLatestMetricsTest() {
"high": 3,
"low": 4,
"medium": 5,
- "policyViolationsFail": 0,
- "policyViolationsInfo": 0,
- "policyViolationsLicenseTotal": 0,
- "policyViolationsOperationalTotal": 0,
- "policyViolationsSecurityTotal": 0,
- "policyViolationsTotal": 0,
- "policyViolationsWarn": 0,
+ "policyViolationsFail": 6,
+ "policyViolationsInfo": 7,
+ "policyViolationsLicenseTotal": 8,
+ "policyViolationsOperationalTotal": 9,
+ "policyViolationsSecurityTotal": 10,
+ "policyViolationsTotal": 11,
+ "policyViolationsWarn": 12,
"inheritedRiskScore": 13.13,
- "unassigned": 0,
+ "unassigned": 14,
"vulnerabilities": 15
}
}
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java
index 0b14692f5a..dbea1ec7df 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/RepositoryResourceTest.java
@@ -25,7 +25,7 @@
import org.dependencytrack.model.Repository;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.dependencytrack.persistence.QueryManager;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
@@ -41,6 +41,8 @@
import java.util.Date;
import java.util.List;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
+
public class RepositoryResourceTest extends ResourceTest {
@ClassRule
@@ -53,8 +55,8 @@ public class RepositoryResourceTest extends ResourceTest {
@Override
public void before() throws Exception {
super.before();
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultRepositories();
+
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultRepositories);
}
@Test
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java
index eb208e6d61..7f73489f24 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/RoleResourceTest.java
@@ -18,33 +18,34 @@
*/
package org.dependencytrack.resources.v1;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-
+import alpine.common.util.UuidUtil;
+import alpine.model.ManagedUser;
+import alpine.model.Permission;
+import alpine.server.filters.ApiFilter;
+import alpine.server.filters.AuthenticationFeature;
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.Role;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
-import alpine.common.util.UuidUtil;
-import alpine.model.ManagedUser;
-import alpine.model.Permission;
-import alpine.server.filters.ApiFilter;
-import alpine.server.filters.AuthenticationFeature;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
public class RoleResourceTest extends ResourceTest {
@@ -58,9 +59,11 @@ public class RoleResourceTest extends ResourceTest {
@Override
public void before() throws Exception {
super.before();
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultPermissions();
- generator.loadDefaultRoles();
+
+ useJdbiTransaction(handle -> {
+ DatabaseSeedingInitTask.seedDefaultPermissions(handle);
+ DatabaseSeedingInitTask.seedDefaultRoles(handle);
+ });
}
@Test
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java
index 11214a9298..02b0dff16c 100644
--- a/apiserver/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java
@@ -27,30 +27,31 @@
import alpine.server.auth.JsonWebToken;
import alpine.server.filters.ApiFilter;
import alpine.server.filters.AuthenticationFeature;
-import jakarta.json.JsonArray;
-import jakarta.json.JsonObject;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
import org.assertj.core.api.Assertions;
import org.dependencytrack.JerseyTestRule;
import org.dependencytrack.ResourceTest;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.model.ConfigPropertyConstants;
import org.dependencytrack.model.Project;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Test;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
import static org.hamcrest.CoreMatchers.equalTo;
public class TeamResourceTest extends ResourceTest {
@@ -70,8 +71,7 @@ public void setUpUser(boolean isAdmin) {
qm.addUserToTeam(testUser, team);
userNotPartof = qm.createTeam("UserNotPartof");
if (isAdmin) {
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultPermissions();
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultPermissions);
List permissionsList = new ArrayList<>();
final Permission adminPermission = qm.getPermission("ACCESS_MANAGEMENT");
permissionsList.add(adminPermission);
@@ -521,8 +521,7 @@ public void getVisibleNonApiKeyTeams() {
@Test
public void getVisibleAdminApiKeyTeams() {
userNotPartof = qm.createTeam("UserNotPartof");
- final var generator = new DefaultObjectGenerator();
- generator.loadDefaultPermissions();
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultPermissions);
List permissionsList = new ArrayList<>();
final Permission adminPermission = qm.getPermission("ACCESS_MANAGEMENT");
permissionsList.add(adminPermission);
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/MetricsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/MetricsResourceTest.java
new file mode 100644
index 0000000000..91fda35170
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/MetricsResourceTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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;
+
+import net.javacrumbs.jsonunit.core.Option;
+import org.dependencytrack.JerseyTestRule;
+import org.dependencytrack.ResourceTest;
+import org.dependencytrack.auth.Permissions;
+import org.dependencytrack.model.Project;
+import org.dependencytrack.model.ProjectMetrics;
+import org.dependencytrack.model.VulnerabilityMetrics;
+import org.dependencytrack.persistence.jdbi.MetricsTestDao;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import jakarta.json.JsonObject;
+import jakarta.ws.rs.core.Response;
+import java.net.URI;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Date;
+
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle;
+
+public class MetricsResourceTest extends ResourceTest {
+
+ @ClassRule
+ public static JerseyTestRule jersey = new JerseyTestRule(new ResourceConfig());
+
+ @Test
+ public void getCurrentPortfolioMetricsEmptyTest() {
+ initializeWithPermissions(Permissions.VIEW_PORTFOLIO);
+ enablePortfolioAccessControl();
+
+ final Response response = jersey
+ .target("/metrics/portfolio/current")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThatJson(getPlainTextBody(response))
+ .isEqualTo(/* language=JSON */ """
+ {
+ "components": 0,
+ "critical": 0,
+ "findings_audited": 0,
+ "findings_total": 0,
+ "findings_unaudited": 0,
+ "high": 0,
+ "inherited_risk_score": 0.0,
+ "low": 0,
+ "medium": 0,
+ "observed_at": "${json-unit.any-number}",
+ "policy_violations_audited": 0,
+ "policy_violations_fail": 0,
+ "policy_violations_info": 0,
+ "policy_violations_license_audited": 0,
+ "policy_violations_license_total": 0,
+ "policy_violations_license_unaudited": 0,
+ "policy_violations_operational_audited": 0,
+ "policy_violations_operational_total": 0,
+ "policy_violations_operational_unaudited": 0,
+ "policy_violations_security_audited": 0,
+ "policy_violations_security_total": 0,
+ "policy_violations_security_unaudited": 0,
+ "policy_violations_total": 0,
+ "policy_violations_unaudited": 0,
+ "policy_violations_warn": 0,
+ "projects": 0,
+ "suppressed": 0,
+ "unassigned": 0,
+ "vulnerabilities": 0,
+ "vulnerable_components": 0,
+ "vulnerable_projects": 0
+ }
+ """);
+ }
+
+ @Test
+ public void getCurrentPortfolioMetricsAclTest() {
+ initializeWithPermissions(Permissions.VIEW_PORTFOLIO);
+ enablePortfolioAccessControl();
+
+ final var accessibleProjectA = new Project();
+ accessibleProjectA.setName("acme-app-a");
+ accessibleProjectA.addAccessTeam(super.team);
+ qm.persist(accessibleProjectA);
+
+ final var accessibleProjectB = new Project();
+ accessibleProjectB.setName("acme-app-b");
+ accessibleProjectB.addAccessTeam(super.team);
+ qm.persist(accessibleProjectB);
+
+ final var inactiveAccessibleProject = new Project();
+ inactiveAccessibleProject.setName("acme-app-inactive");
+ inactiveAccessibleProject.setInactiveSince(new Date());
+ inactiveAccessibleProject.addAccessTeam(super.team);
+ qm.persist(inactiveAccessibleProject);
+
+ final var inaccessibleProject = new Project();
+ inaccessibleProject.setName("acme-app-inaccessible");
+ qm.persist(inaccessibleProject);
+
+ final var today = LocalDate.now();
+
+ useJdbiHandle(handle -> {
+ var dao = handle.attach(MetricsTestDao.class);
+
+ dao.createMetricsPartitionsForDate("PROJECTMETRICS", today);
+ dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(1));
+ dao.createMetricsPartitionsForDate("PROJECTMETRICS", today.minusDays(2));
+
+ {
+ // Create metrics for "yesterday".
+
+ var accessibleProjectAMetrics = new ProjectMetrics();
+ accessibleProjectAMetrics.setProjectId(accessibleProjectA.getId());
+ accessibleProjectAMetrics.setComponents(2);
+ accessibleProjectAMetrics.setFirstOccurrence(Date.from(today.minusDays(1).atTime(1, 1).atZone(ZoneId.systemDefault()).toInstant()));
+ accessibleProjectAMetrics.setLastOccurrence(accessibleProjectAMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(accessibleProjectAMetrics);
+ }
+
+ {
+ // Create metrics for "today".
+
+ // Do not create metrics for accessibleProjectA.
+ // Its metrics from "yesterday" are supposed to carry over to "today".
+
+ var accessibleProjectBMetrics = new ProjectMetrics();
+ accessibleProjectBMetrics.setProjectId(accessibleProjectB.getId());
+ accessibleProjectBMetrics.setComponents(1);
+ accessibleProjectBMetrics.setFirstOccurrence(Date.from(today.atTime(1, 1).atZone(ZoneId.systemDefault()).toInstant()));
+ accessibleProjectBMetrics.setLastOccurrence(accessibleProjectBMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(accessibleProjectBMetrics);
+
+ // Metrics of inactive projects must not be considered.
+ var inactiveAccessibleProjectMetrics = new ProjectMetrics();
+ inactiveAccessibleProjectMetrics.setProjectId(inactiveAccessibleProject.getId());
+ inactiveAccessibleProjectMetrics.setComponents(111);
+ inactiveAccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(2, 2).atZone(ZoneId.systemDefault()).toInstant()));
+ inactiveAccessibleProjectMetrics.setLastOccurrence(inactiveAccessibleProjectMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(inactiveAccessibleProjectMetrics);
+
+ // Metrics of inaccessible projects must not be considered.
+ var inaccessibleProjectMetrics = new ProjectMetrics();
+ inaccessibleProjectMetrics.setProjectId(inaccessibleProject.getId());
+ inaccessibleProjectMetrics.setComponents(666);
+ inaccessibleProjectMetrics.setFirstOccurrence(Date.from(today.atTime(3, 3).atZone(ZoneId.systemDefault()).toInstant()));
+ inaccessibleProjectMetrics.setLastOccurrence(inaccessibleProjectMetrics.getFirstOccurrence());
+ dao.createProjectMetrics(inaccessibleProjectMetrics);
+ }
+ });
+
+ final Response response = jersey
+ .target("/metrics/portfolio/current")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThatJson(getPlainTextBody(response))
+ .withOptions(Option.IGNORING_EXTRA_FIELDS)
+ .isEqualTo(/* language=JSON */ """
+ {
+ "projects": 2,
+ "components": 3
+ }
+ """);
+ }
+
+ @Test
+ public void getVulnerabilityMetricsPaginated() {
+ initializeWithPermissions(Permissions.VIEW_PORTFOLIO);
+ enablePortfolioAccessControl();
+
+ for (int i = 1; i < 4; i++) {
+ var metrics = new VulnerabilityMetrics();
+ metrics.setYear(2025);
+ metrics.setMonth(i);
+ metrics.setCount(i);
+ metrics.setMeasuredAt(Date.from(Instant.now()));
+ qm.persist(metrics);
+ }
+
+ Response response = jersey.target("/metrics/vulnerabilities")
+ .queryParam("limit", 2)
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ final JsonObject responseJson = parseJsonObject(response);
+ assertThatJson(responseJson.toString()).isEqualTo(/* language=JSON */ """
+ {
+ "metrics" :
+ [
+ {
+ "observed_at" : "${json-unit.any-number}",
+ "year" : 2025,
+ "month" : 1,
+ "count" : 1
+ },
+ {
+ "observed_at" : "${json-unit.any-number}",
+ "year" : 2025,
+ "month" : 2,
+ "count" : 2
+ }
+ ],
+ "_pagination" : {
+ "links" : {
+ "self" : "${json-unit.any-string}",
+ "next": "${json-unit.any-string}"
+ }
+ }
+ }
+ """);
+
+ final var nextPageUri = URI.create(
+ responseJson
+ .getJsonObject("_pagination")
+ .getJsonObject("links")
+ .getString("next"));
+
+ response = jersey.target(nextPageUri)
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "metrics" :
+ [
+ {
+ "observed_at" : "${json-unit.any-number}",
+ "year" : 2025,
+ "month" : 3,
+ "count" : 3
+ }
+ ],
+ "_pagination": {
+ "links": {
+ "self": "${json-unit.any-string}"
+ }
+ }
+ }
+ """);
+ }
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/OpenApiResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/OpenApiResourceTest.java
new file mode 100644
index 0000000000..d6bbc772ac
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/OpenApiResourceTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import io.swagger.parser.OpenAPIParser;
+import io.swagger.v3.parser.core.models.SwaggerParseResult;
+import org.dependencytrack.JerseyTestRule;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import jakarta.ws.rs.core.Response;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.dependencytrack.resources.v2.OpenApiValidationClientResponseFilter.DISABLE_OPENAPI_VALIDATION;
+
+public class OpenApiResourceTest {
+
+ @ClassRule
+ public static JerseyTestRule jersey = new JerseyTestRule(new ResourceConfig());
+
+ @Test
+ public void shouldReturnSpecYaml() {
+ final Response response = jersey.target("/openapi.yaml")
+ .request()
+ .property(DISABLE_OPENAPI_VALIDATION, "true")
+ .get(Response.class);
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(response.getHeaderString("Content-Type")).isEqualTo("application/yaml");
+
+ final String openApiYaml = response.readEntity(String.class);
+ final SwaggerParseResult parseResult = new OpenAPIParser().readContents(openApiYaml, null, null);
+
+ final List validationMessages = parseResult.getMessages();
+ assertThat(validationMessages).isEmpty();
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/OpenApiValidationClientResponseFilter.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/OpenApiValidationClientResponseFilter.java
new file mode 100644
index 0000000000..5a228716fd
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/OpenApiValidationClientResponseFilter.java
@@ -0,0 +1,186 @@
+/*
+ * 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;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.networknt.schema.InputFormat;
+import com.networknt.schema.JsonMetaSchema;
+import com.networknt.schema.JsonSchema;
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.NonValidationKeyword;
+import com.networknt.schema.SpecVersion;
+import com.networknt.schema.ValidationMessage;
+import com.networknt.schema.oas.OpenApi30;
+import io.swagger.parser.OpenAPIParser;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.media.MediaType;
+import io.swagger.v3.oas.models.responses.ApiResponse;
+import io.swagger.v3.parser.ObjectMapperFactory;
+import io.swagger.v3.parser.core.models.ParseOptions;
+import org.junit.Assert;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+
+import static java.util.Objects.requireNonNull;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @since 5.6.0
+ */
+public class OpenApiValidationClientResponseFilter implements ClientResponseFilter {
+
+ public static final String DISABLE_OPENAPI_VALIDATION = "disable-openapi-validation";
+
+ private static final JsonSchemaFactory SCHEMA_FACTORY =
+ JsonSchemaFactory.getInstance(
+ SpecVersion.VersionFlag.V4,
+ builder -> builder
+ .metaSchema(JsonMetaSchema.builder(OpenApi30.getInstance())
+ .keyword(new NonValidationKeyword("exampleSetFlag"))
+ .keyword(new NonValidationKeyword("extensions"))
+ .keyword(new NonValidationKeyword("types"))
+ .build())
+ .defaultMetaSchemaIri(OpenApi30.getInstance().getIri()));
+
+ private final OpenAPI openApiSpec;
+ private final ObjectMapper objectMapper;
+
+ public OpenApiValidationClientResponseFilter() {
+ this.openApiSpec = loadOpenApiSpec();
+ this.objectMapper = ObjectMapperFactory.createJson();
+ }
+
+ @Override
+ public void filter(
+ final ClientRequestContext requestContext,
+ final ClientResponseContext responseContext) throws IOException {
+ if (requestContext.hasProperty(DISABLE_OPENAPI_VALIDATION)) {
+ return;
+ }
+
+ final Operation operationDef = findOpenApiOperation(requestContext);
+ if (operationDef == null) {
+ // Undocumented request?
+ Assert.fail("No OpenAPI operation found for %s %s".formatted(
+ requestContext.getMethod(), requestContext.getUri()));
+ }
+
+ // Read the response content and assign it back to the response context.
+ // Without this, clients won't be able to read the response anymore.
+ final byte[] responseBytes = responseContext.getEntityStream().readAllBytes();
+ responseContext.setEntityStream(new ByteArrayInputStream(responseBytes));
+
+ // Identity the correct response object in the spec based on the status.
+ final String responseStatus = String.valueOf(responseContext.getStatus());
+ assertThat(operationDef.getResponses().keySet()).contains(responseStatus);
+ final ApiResponse responseDef = operationDef.getResponses().get(responseStatus);
+
+ // If the spec does not define a response, ensure that the actual
+ // response is also empty.
+ if (responseDef.getContent() == null) {
+ assertThat(responseBytes).asString().isEmpty();
+ return;
+ }
+
+ // Identity the correct media type in the spec response.
+ final String responseContentType = responseContext.getHeaderString("Content-Type");
+ assertThat(responseDef.getContent().keySet()).contains(responseContentType);
+ final MediaType mediaType = responseDef.getContent().get(responseContentType);
+
+ // Serialize the response schema to JSON so it can be used for validation.
+ // NB: The schema already has all $refs resolved so can be handled "standalone".
+ final String schemaJson = objectMapper.writeValueAsString(mediaType.getSchema());
+ final JsonSchema schema = SCHEMA_FACTORY.getSchema(schemaJson);
+
+ final Set messages = schema.validate(
+ new String(responseBytes), InputFormat.JSON);
+ assertThat(messages).isEmpty();
+ }
+
+ private Operation findOpenApiOperation(final ClientRequestContext requestContext) {
+ final String requestPath = requestContext.getUri().getPath();
+
+ for (final String specPath : openApiSpec.getPaths().keySet()) {
+ if (!pathsMatch(requestPath, specPath)) {
+ continue;
+ }
+
+ final PathItem pathItem = openApiSpec.getPaths().get(specPath);
+ return switch (requestContext.getMethod()) {
+ case "DELETE" -> pathItem.getDelete();
+ case "GET" -> pathItem.getGet();
+ case "HEAD" -> pathItem.getHead();
+ case "OPTIONS" -> pathItem.getOptions();
+ case "PATCH" -> pathItem.getPatch();
+ case "POST" -> pathItem.getPost();
+ case "PUT" -> pathItem.getPut();
+ case "TRACE" -> pathItem.getTrace();
+ default -> null;
+ };
+ }
+
+ return null;
+ }
+
+ private boolean pathsMatch(final String requestPath, final String specPath) {
+ final String[] requestPathSegments = requestPath.split("/");
+ final String[] specPathSegments = specPath.split("/");
+
+ if (requestPathSegments.length != specPathSegments.length) {
+ return false;
+ }
+
+ for (int i = 0; i < requestPathSegments.length; i++) {
+ final String requestPathSegment = requestPathSegments[i];
+ final String specPathSegment = specPathSegments[i];
+
+ if (!requestPathSegment.equals(specPathSegment) && !specPathSegment.startsWith("{")) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static OpenAPI loadOpenApiSpec() {
+ try (final InputStream specInputStream =
+ OpenApiValidationClientResponseFilter.class.getResourceAsStream(
+ "/org/dependencytrack/api/v2/openapi.yaml")) {
+ requireNonNull(specInputStream);
+ final String specString = new String(specInputStream.readAllBytes());
+
+ final var parseOptions = new ParseOptions();
+ parseOptions.setResolve(true);
+ parseOptions.setResolveFully(true);
+
+ return new OpenAPIParser().readContents(specString, null, parseOptions).getOpenAPI();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to load OpenAPI spec", e);
+ }
+ }
+
+}
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/TeamsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/TeamsResourceTest.java
new file mode 100644
index 0000000000..ad9f50b2e3
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/TeamsResourceTest.java
@@ -0,0 +1,589 @@
+/*
+ * 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;
+
+import alpine.model.Permission;
+import alpine.model.Team;
+import alpine.model.User;
+import org.dependencytrack.JerseyTestRule;
+import org.dependencytrack.ResourceTest;
+import org.dependencytrack.auth.Permissions;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import jakarta.json.JsonObject;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Response;
+import java.net.URI;
+import java.util.List;
+
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TeamsResourceTest extends ResourceTest {
+
+ @ClassRule
+ public static JerseyTestRule jersey = new JerseyTestRule(new ResourceConfig());
+
+ @Test
+ public void listTeamsShouldReturnPaginatedTeams() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ qm.createTeam("team0");
+ qm.createTeam("team1");
+
+ Response response = jersey.target("/teams")
+ .queryParam("limit", 2)
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ final JsonObject responseJson = parseJsonObject(response);
+ assertThatJson(responseJson.toString()).isEqualTo(/* language=JSON */ """
+ {
+ "teams": [
+ {
+ "name": "Test Users",
+ "api_keys": 1,
+ "members": 0
+ },
+ {
+ "name": "team0",
+ "api_keys": 0,
+ "members": 0
+ }
+ ],
+ "_pagination": {
+ "links": {
+ "self": "${json-unit.any-string}",
+ "next": "${json-unit.any-string}"
+ }
+ }
+ }
+ """);
+
+ final var nextPageUri = URI.create(
+ responseJson
+ .getJsonObject("_pagination")
+ .getJsonObject("links")
+ .getString("next"));
+
+ response = jersey.target(nextPageUri)
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "teams": [
+ {
+ "name": "team1",
+ "api_keys": 0,
+ "members": 0
+ }
+ ],
+ "_pagination": {
+ "links": {
+ "self": "${json-unit.any-string}"
+ }
+ }
+ }
+ """);
+ }
+
+ @Test
+ public void createTeamShouldCreateTeam() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ qm.createPermission(
+ Permissions.VIEW_PORTFOLIO.name(),
+ Permissions.VIEW_PORTFOLIO.getDescription());
+
+ final Response response = jersey.target("/teams")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .post(Entity.json(/* language=JSON */ """
+ {
+ "name": "foo",
+ "permissions": [
+ "VIEW_PORTFOLIO"
+ ]
+ }
+ """));
+ assertThat(response.getStatus()).isEqualTo(201);
+ assertThat(response.getLocation()).hasPath("/teams/foo");
+ assertThat(getPlainTextBody(response)).isEmpty();
+
+ qm.getPersistenceManager().evictAll();
+
+ final Team team = qm.getTeam("foo");
+ assertThat(team).isNotNull();
+ assertThat(team.getPermissions()).extracting(Permission::getName).containsOnly("VIEW_PORTFOLIO");
+ }
+
+ @Test
+ public void createTeamShouldReturnConflictWhenAlreadyExists() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ qm.createTeam("foo");
+
+ final Response response = jersey.target("/teams")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .post(Entity.json(/* language=JSON */ """
+ {
+ "name": "foo",
+ "permissions": [
+ "VIEW_PORTFOLIO"
+ ]
+ }
+ """));
+ assertThat(response.getStatus()).isEqualTo(409);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "type":"about:blank",
+ "status":409,
+ "title": "Conflict",
+ "detail": "The resource already exists."
+ }
+ """);
+
+ qm.getPersistenceManager().evictAll();
+
+ final Team team = qm.getTeam("foo");
+ assertThat(team).isNotNull();
+ assertThat(team.getPermissions()).isEmpty();
+ }
+
+ @Test
+ public void getTeamShouldReturnTeam() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ final Team team = qm.createTeam("foo");
+ team.setPermissions(List.of(
+ qm.createPermission(
+ Permissions.VIEW_PORTFOLIO.name(),
+ Permissions.VIEW_PORTFOLIO.getDescription()),
+ qm.createPermission(
+ Permissions.VIEW_VULNERABILITY.name(),
+ Permissions.VIEW_VULNERABILITY.getDescription())));
+
+ final Response response = jersey.target("/teams/foo")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "name": "foo",
+ "permissions": [
+ "VIEW_PORTFOLIO",
+ "VIEW_VULNERABILITY"
+ ]
+ }
+ """);
+ }
+
+ @Test
+ public void getTeamShouldReturnNotFoundWhenTeamDoesNotExist() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ final Response response = jersey.target("/teams/foo")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(404);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "type":"about:blank",
+ "status": 404,
+ "title": "Not Found",
+ "detail": "The requested resource could not be found."
+ }
+ """);
+ }
+
+ @Test
+ public void deleteTeamShouldDeleteTeam() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ qm.createTeam("foo");
+
+ final Response response = jersey.target("/teams/foo")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .delete();
+ assertThat(response.getStatus()).isEqualTo(204);
+ assertThat(getPlainTextBody(response)).isEmpty();
+
+ qm.getPersistenceManager().evictAll();
+
+ assertThat(qm.getTeam("foo")).isNull();
+ }
+
+ @Test
+ public void deleteTeamsShouldReturnNotFoundWhenNotExists() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ final Response response = jersey.target("/teams/does-not-exist")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .delete();
+ assertThat(response.getStatus()).isEqualTo(404);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "type": "about:blank",
+ "status": 404,
+ "title": "Not Found",
+ "detail": "The requested resource could not be found."
+ }
+ """);
+ }
+
+ @Test
+ public void listTeamMembershipsShouldReturnPaginatedTeamMemberships() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ final Team teamA = qm.createTeam("team-a");
+ qm.addUserToTeam(qm.createManagedUser("foo", "password"), teamA);
+ qm.addUserToTeam(qm.createManagedUser("bar", "password"), teamA);
+
+ final Team teamB = qm.createTeam("team-b");
+ qm.addUserToTeam(qm.createManagedUser("aaa", "password"), teamB);
+
+ Response response = jersey.target("/team-memberships")
+ .queryParam("limit", 2)
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ final JsonObject responseJson = parseJsonObject(response);
+ assertThatJson(responseJson.toString()).isEqualTo(/* language=JSON */ """
+ {
+ "memberships": [
+ {
+ "team_name": "team-a",
+ "username": "bar"
+ },
+ {
+ "team_name": "team-a",
+ "username": "foo"
+ }
+ ],
+ "_pagination": {
+ "links": {
+ "self": "${json-unit.any-string}",
+ "next": "${json-unit.any-string}"
+ }
+ }
+ }
+ """);
+
+ final var nextPageUri = URI.create(
+ responseJson
+ .getJsonObject("_pagination")
+ .getJsonObject("links")
+ .getString("next"));
+
+ response = jersey.target(nextPageUri)
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "memberships": [
+ {
+ "team_name": "team-b",
+ "username": "aaa"
+ }
+ ],
+ "_pagination": {
+ "links": {
+ "self": "${json-unit.any-string}"
+ }
+ }
+ }
+ """);
+ }
+
+ @Test
+ public void listTeamMembershipsShouldFilterByTeamName() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ final Team teamA = qm.createTeam("team-a");
+ qm.addUserToTeam(qm.createManagedUser("foo", "password"), teamA);
+
+ final Team teamB = qm.createTeam("team-b");
+ qm.addUserToTeam(qm.createManagedUser("bar", "password"), teamB);
+
+ Response response = jersey.target("/team-memberships")
+ .queryParam("team", "team-b")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "memberships": [
+ {
+ "team_name": "team-b",
+ "username": "bar"
+ }
+ ],
+ "_pagination": {
+ "links": {
+ "self": "${json-unit.any-string}"
+ }
+ }
+ }
+ """);
+ }
+
+ @Test
+ public void listTeamMembershipsShouldFilterByUsername() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ final Team teamA = qm.createTeam("team-a");
+ qm.addUserToTeam(qm.createManagedUser("foo", "password"), teamA);
+
+ final Team teamB = qm.createTeam("team-b");
+ qm.addUserToTeam(qm.createManagedUser("bar", "password"), teamB);
+
+ final Response response = jersey.target("/team-memberships")
+ .queryParam("user", "bar")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "memberships": [
+ {
+ "team_name": "team-b",
+ "username": "bar"
+ }
+ ],
+ "_pagination": {
+ "links": {
+ "self": "${json-unit.any-string}"
+ }
+ }
+ }
+ """);
+ }
+
+ @Test
+ public void createTeamMembershipShouldCreateTeamMembership() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ qm.createTeam("team-foo");
+ qm.createManagedUser("user-bar", "password");
+
+ final Response response = jersey.target("/team-memberships")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .post(Entity.json(/* language=JSON */ """
+ {
+ "team_name": "team-foo",
+ "username": "user-bar"
+ }
+ """));
+ assertThat(response.getStatus()).isEqualTo(201);
+ assertThat(response.getLocation()).isNull();
+ assertThat(getPlainTextBody(response)).isEmpty();
+
+ qm.getPersistenceManager().evictAll();
+
+ assertThat(qm.getTeam("team-foo").getUsers())
+ .extracting(User::getUsername)
+ .contains("user-bar");
+ }
+
+ @Test
+ public void createTeamMembershipShouldReturnConflictWhenAlreadyExists() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ final Team team = qm.createTeam("team-foo");
+ final User user = qm.createManagedUser("user-bar", "password");
+ qm.addUserToTeam(user, team);
+
+ final Response response = jersey.target("/team-memberships")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .post(Entity.json(/* language=JSON */ """
+ {
+ "team_name": "team-foo",
+ "username": "user-bar"
+ }
+ """));
+ assertThat(response.getStatus()).isEqualTo(409);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "type":"about:blank",
+ "status":409,
+ "title": "Conflict",
+ "detail": "The resource already exists."
+ }
+ """);
+ }
+
+ @Test
+ public void createTeamMembershipShouldReturnNotFoundWhenTeamNotExists() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ qm.createManagedUser("user-bar", "password");
+
+ final Response response = jersey.target("/team-memberships")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .post(Entity.json(/* language=JSON */ """
+ {
+ "team_name": "team-foo",
+ "username": "user-bar"
+ }
+ """));
+ assertThat(response.getStatus()).isEqualTo(404);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "type":"about:blank",
+ "status": 404,
+ "title": "Not Found",
+ "detail": "The requested resource could not be found."
+ }
+ """);
+ }
+
+ @Test
+ public void createTeamMembershipShouldReturnNotFoundWhenUserNotExists() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ qm.createTeam("team-foo");
+
+ final Response response = jersey.target("/team-memberships")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .post(Entity.json(/* language=JSON */ """
+ {
+ "team_name": "team-foo",
+ "username": "user-bar"
+ }
+ """));
+ assertThat(response.getStatus()).isEqualTo(404);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "type":"about:blank",
+ "status": 404,
+ "title": "Not Found",
+ "detail": "The requested resource could not be found."
+ }
+ """);
+ }
+
+ @Test
+ public void deleteTeamMembershipShouldDeleteTeamMembership() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ final Team team = qm.createTeam("foo");
+ qm.addUserToTeam(qm.createManagedUser("bar", "password"), team);
+
+ final Response response = jersey.target("/team-memberships")
+ .queryParam("team", "foo")
+ .queryParam("user", "bar")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .delete();
+ assertThat(response.getStatus()).isEqualTo(204);
+
+ qm.getPersistenceManager().evictAll();
+
+ assertThat(team.getUsers()).isEmpty();
+ }
+
+ @Test
+ public void deleteTeamMembershipShouldReturnNotFoundWhenTeamDoesNotExist() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ qm.createManagedUser("bar", "password");
+
+ final Response response = jersey.target("/team-memberships")
+ .queryParam("team", "foo")
+ .queryParam("user", "bar")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .delete();
+ assertThat(response.getStatus()).isEqualTo(404);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "type":"about:blank",
+ "status": 404,
+ "title": "Not Found",
+ "detail": "The requested resource could not be found."
+ }
+ """);
+ }
+
+ @Test
+ public void deleteTeamMembershipShouldReturnNotFoundWhenUserDoesNotExist() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ qm.createTeam("foo");
+
+ final Response response = jersey.target("/team-memberships")
+ .queryParam("team", "foo")
+ .queryParam("user", "bar")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .delete();
+ assertThat(response.getStatus()).isEqualTo(404);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "type":"about:blank",
+ "status": 404,
+ "title": "Not Found",
+ "detail": "The requested resource could not be found."
+ }
+ """);
+ }
+
+ @Test
+ public void deleteTeamMembershipShouldReturnNotFoundWhenMembershipDoesNotExist() {
+ initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
+
+ qm.createTeam("foo");
+ qm.createManagedUser("bar", "password");
+
+ final Response response = jersey.target("/team-memberships")
+ .queryParam("team", "foo")
+ .queryParam("user", "bar")
+ .request()
+ .header(X_API_KEY, apiKey)
+ .delete();
+ assertThat(response.getStatus()).isEqualTo(404);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "type":"about:blank",
+ "status": 404,
+ "title": "Not Found",
+ "detail": "The requested resource could not be found."
+ }
+ """);
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/WorkflowsResourceTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/WorkflowsResourceTest.java
new file mode 100644
index 0000000000..5e9edbf004
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/WorkflowsResourceTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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;
+
+import net.javacrumbs.jsonunit.core.Option;
+import org.apache.http.HttpStatus;
+import org.dependencytrack.JerseyTestRule;
+import org.dependencytrack.ResourceTest;
+import org.dependencytrack.auth.Permissions;
+import org.dependencytrack.model.WorkflowState;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import jakarta.ws.rs.core.Response;
+import java.time.Instant;
+import java.util.Date;
+import java.util.UUID;
+
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.dependencytrack.model.WorkflowStatus.COMPLETED;
+import static org.dependencytrack.model.WorkflowStatus.PENDING;
+import static org.dependencytrack.model.WorkflowStep.BOM_CONSUMPTION;
+import static org.dependencytrack.model.WorkflowStep.BOM_PROCESSING;
+import static org.hamcrest.CoreMatchers.equalTo;
+
+public class WorkflowsResourceTest extends ResourceTest {
+
+ @ClassRule
+ public static JerseyTestRule jersey = new JerseyTestRule(new ResourceConfig());
+
+ @Test
+ public void getWorkflowStatusOk() {
+ initializeWithPermissions(Permissions.BOM_UPLOAD);
+
+ UUID uuid = UUID.randomUUID();
+ WorkflowState workflowState1 = new WorkflowState();
+ workflowState1.setParent(null);
+ workflowState1.setFailureReason(null);
+ workflowState1.setStep(BOM_CONSUMPTION);
+ workflowState1.setStatus(COMPLETED);
+ workflowState1.setToken(uuid);
+ workflowState1.setUpdatedAt(new Date());
+ var workflowState1Persisted = qm.persist(workflowState1);
+
+ WorkflowState workflowState2 = new WorkflowState();
+ workflowState2.setParent(workflowState1Persisted);
+ workflowState2.setFailureReason(null);
+ workflowState2.setStep(BOM_PROCESSING);
+ workflowState2.setStatus(PENDING);
+ workflowState2.setToken(uuid);
+ workflowState2.setStartedAt(Date.from(Instant.now()));
+ workflowState2.setUpdatedAt(Date.from(Instant.now()));
+ qm.persist(workflowState2);
+
+ Response response = jersey.target("/workflows/" + uuid).request()
+ .header(X_API_KEY, apiKey)
+ .get();
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK);
+ final String jsonResponse = getPlainTextBody(response);
+ assertThatJson(jsonResponse)
+ .withOptions(Option.IGNORING_ARRAY_ORDER)
+ .withMatcher("token", equalTo(uuid.toString()))
+ .withMatcher("step1", equalTo("BOM_CONSUMPTION"))
+ .withMatcher("status1", equalTo("COMPLETED"))
+ .withMatcher("step2", equalTo("BOM_PROCESSING"))
+ .withMatcher("status2", equalTo("PENDING"))
+ .isEqualTo(/* language=JSON */ """
+ {
+ "states": [
+ {
+ "token": "${json-unit.matches:token}",
+ "step": "${json-unit.matches:step1}",
+ "status": "${json-unit.matches:status1}",
+ "updated_at": "${json-unit.any-number}"
+ },
+ {
+ "token": "${json-unit.matches:token}",
+ "started_at": "${json-unit.any-number}",
+ "updated_at": "${json-unit.any-number}",
+ "step": "${json-unit.matches:step2}",
+ "status": "${json-unit.matches:status2}"
+ }
+ ]
+ }
+ """);
+ }
+
+ @Test
+ public void getWorkflowStatusNotFound() {
+ initializeWithPermissions(Permissions.BOM_UPLOAD);
+
+ WorkflowState workflowState1 = new WorkflowState();
+ workflowState1.setParent(null);
+ workflowState1.setFailureReason(null);
+ workflowState1.setStep(BOM_CONSUMPTION);
+ workflowState1.setStatus(COMPLETED);
+ workflowState1.setToken(UUID.randomUUID());
+ workflowState1.setUpdatedAt(new Date());
+ qm.persist(workflowState1);
+
+ UUID randomUuid = UUID.randomUUID();
+ Response response = jersey.target("/workflows/" + randomUuid).request()
+ .header(X_API_KEY, apiKey)
+ .get();
+
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+ assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """
+ {
+ "type":"about:blank",
+ "status": 404,
+ "title": "Not Found",
+ "detail": "The requested resource could not be found."
+ }
+ """);
+ }
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/ConstraintViolationExceptionMapperTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/ConstraintViolationExceptionMapperTest.java
new file mode 100644
index 0000000000..dce59268f2
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/ConstraintViolationExceptionMapperTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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 alpine.server.auth.AuthenticationNotRequired;
+import net.javacrumbs.jsonunit.core.Option;
+import org.dependencytrack.JerseyTestRule;
+import org.dependencytrack.model.validation.ValidUuid;
+import org.dependencytrack.resources.v2.ResourceConfig;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import jakarta.validation.constraints.Pattern;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.dependencytrack.resources.v2.OpenApiValidationClientResponseFilter.DISABLE_OPENAPI_VALIDATION;
+
+public class ConstraintViolationExceptionMapperTest {
+
+ @ClassRule
+ public static JerseyTestRule jersey = new JerseyTestRule(
+ new ResourceConfig()
+ .register(JsonProcessingExceptionMapperTest.TestResource.class));
+
+ @Test
+ public void test() {
+ final Response response = jersey.target("/test/not-a-uuid")
+ .queryParam("foo", "666")
+ .request()
+ .property(DISABLE_OPENAPI_VALIDATION, "true")
+ .get();
+ assertThat(response.getStatus()).isEqualTo(400);
+ assertThatJson(response.readEntity(String.class))
+ .withOptions(Option.IGNORING_ARRAY_ORDER)
+ .isEqualTo(/* language=JSON */ """
+ {
+ "type": "about:blank",
+ "status": 400,
+ "title": "Bad Request",
+ "detail": "The request could not be processed because it failed validation.",
+ "errors": [
+ {
+ "message": "must match \\"^[a-z]+$\\"",
+ "path": "get.foo",
+ "value": "666"
+ },
+ {
+ "message": "Invalid UUID",
+ "path": "get.uuid",
+ "value": "not-a-uuid"
+ }
+ ]
+ }
+ """);
+ }
+
+ @Path("/test")
+ public static class TestResource {
+
+ @GET
+ @Path("/{uuid}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @AuthenticationNotRequired
+ public Response get(@PathParam("uuid") @ValidUuid final String uuid,
+ @QueryParam("optionalUuid") @ValidUuid final String optionalUuid,
+ @QueryParam("foo") @Pattern(regexp = "^[a-z]+$") final String foo) {
+ return Response.noContent().build();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/DefaultExceptionMapperTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/DefaultExceptionMapperTest.java
new file mode 100644
index 0000000000..c2172663ea
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/DefaultExceptionMapperTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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 alpine.server.auth.AuthenticationNotRequired;
+import org.dependencytrack.JerseyTestRule;
+import org.dependencytrack.resources.v2.ResourceConfig;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.ServerErrorException;
+import jakarta.ws.rs.core.Response;
+
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.dependencytrack.resources.v2.OpenApiValidationClientResponseFilter.DISABLE_OPENAPI_VALIDATION;
+
+public class DefaultExceptionMapperTest {
+
+ @ClassRule
+ public static JerseyTestRule jersey = new JerseyTestRule(
+ new ResourceConfig()
+ .register(JsonProcessingExceptionMapperTest.TestResource.class));
+
+ @Test
+ public void shouldReturnInternalServerError() {
+ final Response response = jersey.target("/test/error")
+ .request()
+ .property(DISABLE_OPENAPI_VALIDATION, "true")
+ .get();
+ assertThat(response.getStatus()).isEqualTo(500);
+ assertThatJson(response.readEntity(String.class)).isEqualTo(/* language=JSON */ """
+ {
+ "type": "about:blank",
+ "status": 500,
+ "title": "Unexpected error",
+ "detail": "An error occurred that was not anticipated."
+ }
+ """);
+ }
+
+ @Test
+ public void shouldReturnGivenStatusForServerErrors() {
+ final Response response = jersey.target("/test/server-error")
+ .request()
+ .property(DISABLE_OPENAPI_VALIDATION, "true")
+ .get();
+ assertThat(response.getStatus()).isEqualTo(503);
+ assertThatJson(response.readEntity(String.class)).isEqualTo(/* language=JSON */ """
+ {
+ "type": "about:blank",
+ "status": 503,
+ "title": "Unexpected error",
+ "detail": "An error occurred that was not anticipated."
+ }
+ """);
+ }
+
+ @Path("/test")
+ public static class TestResource {
+
+ @GET
+ @Path("/error")
+ @AuthenticationNotRequired
+ public Response fail() throws Exception {
+ throw new ClassNotFoundException("test");
+ }
+
+ @GET
+ @Path("/server-error")
+ @AuthenticationNotRequired
+ public Response serverError() {
+ throw new ServerErrorException(Response.status(Response.Status.SERVICE_UNAVAILABLE).build());
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/JsonProcessingExceptionMapperTest.java b/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/JsonProcessingExceptionMapperTest.java
new file mode 100644
index 0000000000..a437321487
--- /dev/null
+++ b/apiserver/src/test/java/org/dependencytrack/resources/v2/exception/JsonProcessingExceptionMapperTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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 alpine.server.auth.AuthenticationNotRequired;
+import com.fasterxml.jackson.core.JsonGenerationException;
+import org.dependencytrack.JerseyTestRule;
+import org.dependencytrack.resources.v2.ResourceConfig;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.dependencytrack.resources.v2.OpenApiValidationClientResponseFilter.DISABLE_OPENAPI_VALIDATION;
+
+public class JsonProcessingExceptionMapperTest {
+
+ @ClassRule
+ public static JerseyTestRule jersey = new JerseyTestRule(
+ new ResourceConfig()
+ .register(TestResource.class));
+
+ @Test
+ public void shouldReturnInternalServerErrorForServerSideJsonException() {
+ final Response response = jersey.target("/test/json-generation")
+ .request()
+ .property(DISABLE_OPENAPI_VALIDATION, "true")
+ .get();
+ assertThat(response.getStatus()).isEqualTo(500);
+ assertThatJson(response.readEntity(String.class)).isEqualTo(/* language=JSON */ """
+ {
+ "type": "about:blank",
+ "status": 500,
+ "title": "Unexpected error",
+ "detail": "An error occurred that was not anticipated."
+ }
+ """);
+ }
+
+ @Test
+ public void shouldReturnBadRequestForClientSideJsonException() {
+ final Response response = jersey.target("/test")
+ .request()
+ .property(DISABLE_OPENAPI_VALIDATION, "true")
+ .post(Entity.json("[]"));
+ assertThat(response.getStatus()).isEqualTo(400);
+ assertThatJson(response.readEntity(String.class)).isEqualTo(/* language=JSON */ """
+ {
+ "type": "about:blank",
+ "status": 400,
+ "title": "JSON Processing Failed",
+ "detail": "The provided JSON could not be processed."
+ }
+ """);
+ }
+
+ @Path("/test")
+ public static class TestResource {
+
+ public record TestRequest(String name) {
+ }
+
+ @POST
+ @Path("/")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @AuthenticationNotRequired
+ public Response test(final TestRequest request) {
+ return Response.ok(request.name()).build();
+ }
+
+ @GET
+ @Path("/json-generation")
+ @AuthenticationNotRequired
+ @SuppressWarnings("deprecation")
+ public Response jsonGeneration() throws Exception {
+ throw new JsonGenerationException("boom");
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/apiserver/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java b/apiserver/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java
index c362035c5f..17010c4d2f 100644
--- a/apiserver/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/tasks/BomUploadProcessingTaskTest.java
@@ -47,7 +47,7 @@
import org.dependencytrack.model.Project;
import org.dependencytrack.model.VulnerabilityScan;
import org.dependencytrack.model.WorkflowStep;
-import org.dependencytrack.persistence.DefaultObjectGenerator;
+import org.dependencytrack.persistence.DatabaseSeedingInitTask;
import org.dependencytrack.plugin.PluginManager;
import org.dependencytrack.proto.filestorage.v1.FileMetadata;
import org.dependencytrack.proto.notification.v1.BomProcessingFailedSubject;
@@ -99,6 +99,7 @@
import static org.dependencytrack.model.WorkflowStep.METRICS_UPDATE;
import static org.dependencytrack.model.WorkflowStep.POLICY_EVALUATION;
import static org.dependencytrack.model.WorkflowStep.VULN_ANALYSIS;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_BOM_PROCESSED;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_BOM_PROCESSING_FAILED;
import static org.dependencytrack.proto.notification.v1.Level.LEVEL_ERROR;
@@ -127,7 +128,7 @@ public void before() throws Exception {
@Test
public void informTest() throws Exception {
// Required for license resolution.
- DefaultObjectGenerator.loadDefaultLicenses();
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultLicenses);
Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false);
@@ -283,7 +284,7 @@ public void informTest() throws Exception {
@Test
public void informTestWithComponentAlreadyExistsForIntegrityCheck() throws Exception {
// Required for license resolution.
- DefaultObjectGenerator.loadDefaultLicenses();
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultLicenses);
Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false);
@@ -1747,7 +1748,7 @@ public void informWithEmptyComponentAndServiceNameTest() throws Exception {
@Test
public void informBomWithProtobufFormat() throws Exception {
- DefaultObjectGenerator.loadDefaultLicenses();
+ useJdbiTransaction(DatabaseSeedingInitTask::seedDefaultLicenses);
Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, null, false);
diff --git a/apiserver/src/test/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTaskTest.java b/apiserver/src/test/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTaskTest.java
index c29120d2be..0e45f80bd6 100644
--- a/apiserver/src/test/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTaskTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/tasks/maintenance/MetricsMaintenanceTaskTest.java
@@ -22,7 +22,6 @@
import org.dependencytrack.event.maintenance.MetricsMaintenanceEvent;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.DependencyMetrics;
-import org.dependencytrack.model.PortfolioMetrics;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.ProjectMetrics;
import org.dependencytrack.persistence.jdbi.MetricsDao;
@@ -104,14 +103,6 @@ public void test() throws Exception {
metricsTestDao.createProjectMetrics(metrics);
};
- final BiConsumer createPortfolioMetricsForLastOccurrence = (lastOccurrence, vulns) -> {
- var metrics = new PortfolioMetrics();
- metrics.setVulnerabilities(vulns);
- metrics.setFirstOccurrence(Date.from(lastOccurrence));
- metrics.setLastOccurrence(Date.from(lastOccurrence));
- metricsTestDao.createPortfolioMetrics(metrics);
- };
-
final Instant now = Instant.now();
// Create component metrics partitions for dates required
@@ -132,15 +123,6 @@ public void test() throws Exception {
createProjectMetricsForLastOccurrence.accept(now.minus(90, ChronoUnit.DAYS), 90);
createProjectMetricsForLastOccurrence.accept(now.minus(89, ChronoUnit.DAYS), 89);
- // Create portfolio metrics partitions for dates required
- metricsTestDao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 91);
- metricsTestDao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 90);
- metricsTestDao.createPartitionForDaysAgo("PORTFOLIOMETRICS", 89);
-
- createPortfolioMetricsForLastOccurrence.accept(now.minus(91, ChronoUnit.DAYS), 91);
- createPortfolioMetricsForLastOccurrence.accept(now.minus(90, ChronoUnit.DAYS), 90);
- createPortfolioMetricsForLastOccurrence.accept(now.minus(89, ChronoUnit.DAYS), 89);
-
final var task = new MetricsMaintenanceTask();
assertThatNoException().isThrownBy(() -> task.inform(new MetricsMaintenanceEvent()));
@@ -149,21 +131,22 @@ public void test() throws Exception {
assertThat(metricsDao.getProjectMetricsSince(project.getId(), now.minus(91, ChronoUnit.DAYS))).satisfiesExactly(
metrics -> assertThat(metrics.getVulnerabilities()).isEqualTo(89));
-
- assertThat(metricsDao.getPortfolioMetricsSince(now.minus(91, ChronoUnit.DAYS))).satisfiesExactly(
- metrics -> assertThat(metrics.getVulnerabilities()).isEqualTo(89));
}
@Test
public void testCreateMetricsPartitions() {
+ qm.createConfigProperty(
+ MAINTENANCE_METRICS_RETENTION_DAYS.getGroupName(),
+ MAINTENANCE_METRICS_RETENTION_DAYS.getPropertyName(),
+ MAINTENANCE_METRICS_RETENTION_DAYS.getDefaultPropertyValue(),
+ MAINTENANCE_METRICS_RETENTION_DAYS.getPropertyType(),
+ MAINTENANCE_METRICS_RETENTION_DAYS.getDescription());
+
new MetricsMaintenanceTask().inform(new MetricsMaintenanceEvent());
var today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
var tomorrow = LocalDate.now().plusDays(1).format(DateTimeFormatter.BASIC_ISO_DATE);
- var metricsPartitions = metricsDao.getPortfolioMetricsPartitions();
- assertThat(metricsPartitions.getFirst()).isEqualTo("\"PORTFOLIOMETRICS_%s\"".formatted(today));
- assertThat(metricsPartitions.getLast()).isEqualTo("\"PORTFOLIOMETRICS_%s\"".formatted(tomorrow));
- metricsPartitions = metricsDao.getProjectMetricsPartitions();
+ var metricsPartitions = metricsDao.getProjectMetricsPartitions();
assertThat(metricsPartitions.getFirst()).isEqualTo("\"PROJECTMETRICS_%s\"".formatted(today));
assertThat(metricsPartitions.getLast()).isEqualTo("\"PROJECTMETRICS_%s\"".formatted(tomorrow));
diff --git a/apiserver/src/test/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTaskTest.java b/apiserver/src/test/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTaskTest.java
index 54fe961d2d..a87b291395 100644
--- a/apiserver/src/test/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTaskTest.java
+++ b/apiserver/src/test/java/org/dependencytrack/tasks/metrics/PortfolioMetricsUpdateTaskTest.java
@@ -42,14 +42,12 @@
import org.junit.BeforeClass;
import org.junit.Test;
-import java.time.Duration;
-import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle;
+import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiTransaction;
import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle;
import static org.dependencytrack.tasks.metrics.PortfolioMetricsUpdateTask.partition;
@@ -107,49 +105,6 @@ public void testUpdateMetricsEmpty() {
assertThat(metrics.getPolicyViolationsOperationalUnaudited()).isZero();
}
- @Test
- public void testUpdateMetricsUnchanged() throws Exception {
- // Create risk score configproperties
- createTestConfigProperties();
-
- // Record initial portfolio metrics
- new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent());
- final PortfolioMetrics metrics = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics());
- assertThat(metrics.getLastOccurrence()).isEqualTo(metrics.getFirstOccurrence());
-
- //sleep for the least duration lock held for, so lock could be released
- Thread.sleep(2000);
-
- // Run the task a second time, without any metric being changed
- final var beforeSecondRun = new Date();
- new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent());
-
- // Two records should be created in today's partition since it's append-only
- var totalMetricsForToday = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getPortfolioMetricsSince(Instant.now().minus(Duration.ofDays(1))));
- assertThat(totalMetricsForToday.size()).isEqualTo(2);
- var recentMetrics = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics());
- assertThat(recentMetrics.getLastOccurrence()).isNotEqualTo(metrics.getFirstOccurrence());
- assertThat(recentMetrics.getLastOccurrence()).isAfterOrEqualTo(beforeSecondRun);
- }
-
- @Test
- public void testUpdateMetricsDidNotExecuteWhenLockWasHeld() throws Exception {
- // Create risk score configproperties
- createTestConfigProperties();
-
- // Record initial portfolio metrics
- new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent());
- PortfolioMetrics metrics = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics());
- assertThat(metrics.getLastOccurrence()).isEqualTo(metrics.getFirstOccurrence());
-
- // Run the task a second time, without any metric being changed
- new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent());
-
- // Ensure no new record of metrics is appended
- var totalMetricsForToday = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getPortfolioMetricsSince(Instant.now().minus(Duration.ofDays(1))));
- assertThat(totalMetricsForToday.size()).isEqualTo(1);
- }
-
@Test
public void testUpdateMetricsVulnerabilities() {
var vuln = new Vulnerability();
@@ -173,7 +128,7 @@ public void testUpdateMetricsVulnerabilities() {
var projectAudited = new Project();
projectAudited.setName("acme-app-b");
qm.createProject(projectAudited, List.of(), false);
-
+
// Create risk score configproperties
createTestConfigProperties();
@@ -189,7 +144,7 @@ public void testUpdateMetricsVulnerabilities() {
var projectSuppressed = new Project();
projectSuppressed.setName("acme-app-c");
qm.createProject(projectSuppressed, List.of(), false);
-
+
var componentSuppressed = new Component();
componentSuppressed.setProject(projectSuppressed);
componentSuppressed.setName("acme-lib-c");
@@ -198,34 +153,6 @@ public void testUpdateMetricsVulnerabilities() {
withJdbiHandle(handle -> handle.attach(AnalysisDao.class)
.makeAnalysis(projectSuppressed.getId(), componentSuppressed.getId(), vuln.getId(), AnalysisState.FALSE_POSITIVE, null, null, null, true));
- // Create "old" metrics data points for all three projects.
- // When the calculating portfolio metrics, only the latest data point for each project
- // must be considered. Because the update task calculates new project metrics data points,
- // the ones created below must be ignored.
- useJdbiHandle(handle -> {
- var dao = handle.attach(MetricsTestDao.class);
- final var projectUnauditedOldMetrics = new ProjectMetrics();
- projectUnauditedOldMetrics.setProjectId(projectUnaudited.getId());
- projectUnauditedOldMetrics.setCritical(666);
- projectUnauditedOldMetrics.setFirstOccurrence(Date.from(Instant.now()));
- projectUnauditedOldMetrics.setLastOccurrence(Date.from(Instant.now()));
- dao.createProjectMetrics(projectUnauditedOldMetrics);
-
- final var projectAuditedOldMetrics = new ProjectMetrics();
- projectAuditedOldMetrics.setProjectId(projectAudited.getId());
- projectAuditedOldMetrics.setHigh(666);
- projectAuditedOldMetrics.setFirstOccurrence(Date.from(Instant.now()));
- projectAuditedOldMetrics.setLastOccurrence(Date.from(Instant.now()));
- dao.createProjectMetrics(projectAuditedOldMetrics);
-
- final var projectSuppressedOldMetrics = new ProjectMetrics();
- projectSuppressedOldMetrics.setProjectId(projectSuppressed.getId());
- projectSuppressedOldMetrics.setMedium(666);
- projectSuppressedOldMetrics.setFirstOccurrence(Date.from(Instant.now()));
- projectSuppressedOldMetrics.setLastOccurrence(Date.from(Instant.now()));
- dao.createProjectMetrics(projectSuppressedOldMetrics);
- });
-
new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent());
final PortfolioMetrics metrics = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics());
@@ -276,10 +203,10 @@ public void testUpdateMetricsPolicyViolations() {
var projectUnaudited = new Project();
projectUnaudited.setName("acme-app-a");
qm.createProject(projectUnaudited, List.of(), false);
-
+
// Create risk score configproperties
createTestConfigProperties();
-
+
var componentUnaudited = new Component();
componentUnaudited.setProject(projectUnaudited);
componentUnaudited.setName("acme-lib-a");
@@ -290,7 +217,7 @@ public void testUpdateMetricsPolicyViolations() {
var projectAudited = new Project();
projectAudited.setName("acme-app-b");
qm.createProject(projectAudited, List.of(), false);
-
+
var componentAudited = new Component();
componentAudited.setProject(projectAudited);
componentAudited.setName("acme-lib-b");
@@ -302,7 +229,7 @@ public void testUpdateMetricsPolicyViolations() {
var projectSuppressed = new Project();
projectSuppressed.setName("acme-app-c");
qm.createProject(projectSuppressed, List.of(), false);
-
+
var componentSuppressed = new Component();
componentSuppressed.setProject(projectSuppressed);
componentSuppressed.setName("acme-lib-c");
@@ -310,34 +237,6 @@ public void testUpdateMetricsPolicyViolations() {
final var violationSuppressed = createPolicyViolation(componentSuppressed, Policy.ViolationState.INFO, PolicyViolation.Type.SECURITY);
qm.makeViolationAnalysis(componentSuppressed, violationSuppressed, ViolationAnalysisState.REJECTED, true);
- // Create "old" metrics data points for all three projects.
- // When the calculating portfolio metrics, only the latest data point for each project
- // must be considered. Because the update task calculates new project metrics data points,
- // the ones created below must be ignored.
- useJdbiHandle(handle -> {
- var dao = handle.attach(MetricsTestDao.class);
- final var projectUnauditedOldMetrics = new ProjectMetrics();
- projectUnauditedOldMetrics.setProjectId(projectUnaudited.getId());
- projectUnauditedOldMetrics.setPolicyViolationsFail(666);
- projectUnauditedOldMetrics.setFirstOccurrence(Date.from(Instant.now()));
- projectUnauditedOldMetrics.setLastOccurrence(Date.from(Instant.now()));
- dao.createProjectMetrics(projectUnauditedOldMetrics);
-
- final var projectAuditedOldMetrics = new ProjectMetrics();
- projectAuditedOldMetrics.setProjectId(projectAudited.getId());
- projectAuditedOldMetrics.setPolicyViolationsWarn(666);
- projectAuditedOldMetrics.setFirstOccurrence(Date.from(Instant.now()));
- projectAuditedOldMetrics.setLastOccurrence(Date.from(Instant.now()));
- dao.createProjectMetrics(projectAuditedOldMetrics);
-
- final var projectSuppressedOldMetrics = new ProjectMetrics();
- projectSuppressedOldMetrics.setProjectId(projectSuppressed.getId());
- projectSuppressedOldMetrics.setPolicyViolationsInfo(666);
- projectSuppressedOldMetrics.setFirstOccurrence(Date.from(Instant.now()));
- projectSuppressedOldMetrics.setLastOccurrence(Date.from(Instant.now()));
- dao.createProjectMetrics(projectSuppressedOldMetrics);
- });
-
new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent());
final PortfolioMetrics metrics = withJdbiHandle(handle -> handle.attach(MetricsDao.class).getMostRecentPortfolioMetrics());
@@ -382,6 +281,64 @@ public void testUpdateMetricsPolicyViolations() {
assertThat(componentSuppressed.getLastInheritedRiskScore()).isZero();
}
+ @Test
+ public void shouldNotUpdateMetricsForProjectsWithRecentMetrics() {
+ createTestConfigProperties();
+
+ final var projectA = new Project();
+ projectA.setName("acme-app-a");
+ qm.persist(projectA);
+ final var componentA = new Component();
+ componentA.setProject(projectA);
+ componentA.setName("acme-lib-a");
+ qm.persist(componentA);
+
+ final var projectB = new Project();
+ projectB.setName("acme-app-b");
+ qm.persist(projectB);
+ final var componentB = new Component();
+ componentB.setProject(projectB);
+ componentB.setName("acme-lib-b");
+ qm.persist(componentB);
+
+ final var inactiveProject = new Project();
+ inactiveProject.setName("inactive-project");
+ inactiveProject.setInactiveSince(new Date());
+ qm.persist(inactiveProject);
+
+ // Create a metrics data point for projectA, where it has no components.
+ // Despite this difference, we expect no metrics refresh to be performed
+ // for it, because a data point for the current day is already present.
+ useJdbiTransaction(handle -> {
+ var dao = handle.attach(MetricsTestDao.class);
+ final var projectAMetrics = new ProjectMetrics();
+ projectAMetrics.setProjectId(projectA.getId());
+ projectAMetrics.setComponents(0);
+ projectAMetrics.setFirstOccurrence(new Date());
+ projectAMetrics.setLastOccurrence(new Date());
+ dao.createProjectMetrics(projectAMetrics);
+ });
+
+ new PortfolioMetricsUpdateTask().inform(new PortfolioMetricsUpdateEvent());
+
+ final List recentProjectMetrics = withJdbiHandle(
+ handle -> handle.attach(MetricsDao.class)
+ .getMostRecentProjectMetrics(
+ List.of(projectA.getId(), projectB.getId(), inactiveProject.getId())));
+
+ assertThat(recentProjectMetrics).satisfiesExactlyInAnyOrder(
+ metrics -> {
+ assertThat(metrics.getProjectId()).isEqualTo(projectA.getId());
+ assertThat(metrics.getComponents()).isEqualTo(0); // Old value.
+ },
+ metrics -> {
+ assertThat(metrics.getProjectId()).isEqualTo(projectB.getId());
+ assertThat(metrics.getComponents()).isEqualTo(1);
+ }
+ // No metrics for inactiveProject.
+ );
+ }
+
@Test
public void testPartitionWithNull() {
final List list = null;
diff --git a/coverage-report/pom.xml b/coverage-report/pom.xml
index a1df1a2da0..8d8a2df8a1 100644
--- a/coverage-report/pom.xml
+++ b/coverage-report/pom.xml
@@ -25,33 +25,39 @@
org.dependencytrack
alpine-common
+ ${project.version}
provided
org.dependencytrack
alpine-model
+ ${project.version}
provided
org.dependencytrack
alpine-infra
+ ${project.version}
provided
org.dependencytrack
alpine-server
+ ${project.version}
provided
org.dependencytrack
apiserver
+ ${project.version}
classes
provided
org.dependencytrack
datanucleus-plugin
+ ${project.version}
provided
@@ -62,11 +68,13 @@
org.dependencytrack
liquibase-support
+ ${project.version}
provided
org.dependencytrack
persistence-migration
+ ${project.version}
provided