diff --git a/src/main/java/org/dependencytrack/event/HistoricalRiskScoreUpdateEvent.java b/src/main/java/org/dependencytrack/event/HistoricalRiskScoreUpdateEvent.java
new file mode 100644
index 000000000..3373ed246
--- /dev/null
+++ b/src/main/java/org/dependencytrack/event/HistoricalRiskScoreUpdateEvent.java
@@ -0,0 +1,46 @@
+/*
+ * This file is part of Dependency-Track.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright (c) OWASP Foundation. All Rights Reserved.
+ */
+package org.dependencytrack.event;
+
+import alpine.event.framework.Event;
+import alpine.event.framework.SingletonCapableEvent;
+
+/**
+ * Defines an {@link Event} used to trigger historical risk score metrics updates.
+ *
+ * @since 5.0.0
+ */
+public class HistoricalRiskScoreUpdateEvent extends SingletonCapableEvent {
+
+ private final boolean weightHistoryEnabled;
+
+ public HistoricalRiskScoreUpdateEvent() {
+ this(true);
+ }
+
+ public HistoricalRiskScoreUpdateEvent(final boolean weightHistoryEnabled) {
+ this.setSingleton(true);
+ this.weightHistoryEnabled = weightHistoryEnabled;
+ }
+
+ public boolean isWeightHistoryEnabled() {
+ return weightHistoryEnabled;
+ }
+
+}
diff --git a/src/main/java/org/dependencytrack/metrics/Metrics.java b/src/main/java/org/dependencytrack/metrics/Metrics.java
index 1164a5c16..6c2f61b42 100644
--- a/src/main/java/org/dependencytrack/metrics/Metrics.java
+++ b/src/main/java/org/dependencytrack/metrics/Metrics.java
@@ -61,6 +61,17 @@ public static void updatePortfolioMetrics() {
useJdbiHandle(handle -> handle.createCall("CALL \"UPDATE_PORTFOLIO_METRICS\"()").invoke());
}
+
+ /**
+ * Update historical risk scores for the entire portfolio.
+ *
+ *
+ * @since 5.0.0
+ */
+ public static void updateHistoricalRiskScores() {
+ StoredProcedures.execute(Procedure.UPDATE_HISTORICAL_RISK_SCORES);
+ }
+
/**
* Update metrics for a given {@link Project}.
*
diff --git a/src/main/java/org/dependencytrack/persistence/StoredProcedures.java b/src/main/java/org/dependencytrack/persistence/StoredProcedures.java
new file mode 100644
index 000000000..437ad1014
--- /dev/null
+++ b/src/main/java/org/dependencytrack/persistence/StoredProcedures.java
@@ -0,0 +1,80 @@
+/*
+ * 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.datanucleus.api.jdo.JDOQuery;
+import org.datanucleus.store.rdbms.query.StoredProcedureQuery;
+
+import javax.jdo.Query;
+import java.util.function.Consumer;
+
+/**
+ * Utility class to work with database stored procedures.
+ *
+ * @since 5.0.0
+ */
+public final class StoredProcedures {
+
+ public enum Procedure {
+ UPDATE_HISTORICAL_RISK_SCORES,
+ UPDATE_COMPONENT_METRICS,
+ UPDATE_PROJECT_METRICS,
+ UPDATE_PORTFOLIO_METRICS;
+
+ private String quotedName() {
+ // We use quoted identifiers by convention.
+ return "\"%s\"".formatted(name());
+ }
+ }
+
+ private StoredProcedures() {
+ }
+
+ /**
+ * Execute a given stored procedure.
+ *
+ * @param procedure The {@link Procedure} to execute
+ * @since 5.0.0
+ */
+ public static void execute(final Procedure procedure) {
+ execute(procedure, query -> {
+ });
+ }
+
+ /**
+ * Execute a given stored procedure and customize the execution.
+ *
+ * @param procedure The {@link Procedure} to execute
+ * @param queryConsumer {@link Consumer} for customizing the {@link StoredProcedureQuery}
+ * @since 5.0.0
+ */
+ public static void execute(final Procedure procedure, final Consumer queryConsumer) {
+ try (final var qm = new QueryManager()) {
+ final Query> query = qm.getPersistenceManager().newQuery("STOREDPROC", procedure.quotedName());
+ try {
+ final var spQuery = (StoredProcedureQuery) ((JDOQuery>) query).getInternalQuery();
+ queryConsumer.accept(spQuery);
+ spQuery.execute();
+ } finally {
+ query.closeAll();
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java b/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java
index f0af0b8cb..3c76e8fd2 100644
--- a/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java
+++ b/src/main/java/org/dependencytrack/resources/v1/MetricsResource.java
@@ -41,6 +41,7 @@
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.event.ComponentMetricsUpdateEvent;
import org.dependencytrack.event.PortfolioMetricsUpdateEvent;
+import org.dependencytrack.event.HistoricalRiskScoreUpdateEvent;
import org.dependencytrack.event.ProjectMetricsUpdateEvent;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.DependencyMetrics;
@@ -92,6 +93,23 @@ public Response getVulnerabilityMetrics() {
}
}
+ @GET
+ @Path("/riskscore/refresh")
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Requests a refresh of the historical risk score metrics",
+ response = PortfolioMetrics.class,
+ notes = "Requires permission PORTFOLIO_MANAGEMENT
"
+ )
+ @ApiResponses(value = {
+ @ApiResponse(code = 401, message = "Unauthorized")
+ })
+ @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT)
+ public Response RefreshHistoricalRiskScoreMetrics() {
+ Event.dispatch(new HistoricalRiskScoreUpdateEvent());
+ return Response.ok().build();
+ }
+
@GET
@Path("/portfolio/current")
@Produces(MediaType.APPLICATION_JSON)
diff --git a/src/main/java/org/dependencytrack/tasks/metrics/HistoricalRiskScoreUpdateTask.java b/src/main/java/org/dependencytrack/tasks/metrics/HistoricalRiskScoreUpdateTask.java
new file mode 100644
index 000000000..6c1d5e39d
--- /dev/null
+++ b/src/main/java/org/dependencytrack/tasks/metrics/HistoricalRiskScoreUpdateTask.java
@@ -0,0 +1,74 @@
+/*
+ * 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.tasks.metrics;
+
+import alpine.common.logging.Logger;
+import alpine.event.framework.Event;
+import alpine.event.framework.Subscriber;
+import net.javacrumbs.shedlock.core.LockingTaskExecutor;
+import org.dependencytrack.event.HistoricalRiskScoreUpdateEvent;
+import org.dependencytrack.metrics.Metrics;
+import org.dependencytrack.util.LockProvider;
+
+import java.time.Duration;
+import java.util.UUID;
+
+import static org.dependencytrack.tasks.LockName.PORTFOLIO_METRICS_TASK_LOCK;
+
+
+/**
+ * A {@link Subscriber} task that updates portfolio metrics.
+ *
+ * @since 4.6.0
+ */
+public class HistoricalRiskScoreUpdateTask implements Subscriber {
+
+ private static final Logger LOGGER = Logger.getLogger(PortfolioMetricsUpdateTask.class);
+
+ @Override
+ public void inform(final Event e) {
+ if (e instanceof final HistoricalRiskScoreUpdateEvent event) {
+ try {
+ LockProvider.executeWithLock(PORTFOLIO_METRICS_TASK_LOCK, (LockingTaskExecutor.Task)() -> updateMetrics(event.isWeightHistoryEnabled()));
+ } catch (Throwable ex) {
+ LOGGER.error("Error in acquiring lock and executing portfolio metrics task", ex);
+ }
+ }
+ }
+
+ private void updateMetrics(final boolean weightHistoryEnabled) throws Exception {
+ LOGGER.info("Executing historical risk score metrics update");
+ final long startTimeNs = System.nanoTime();
+
+ try {
+ if (weightHistoryEnabled) {
+ LOGGER.info("Refreshing historical risk score metrics");
+ Metrics.updateHistoricalRiskScores();
+ }
+
+ Metrics.updatePortfolioMetrics();
+ } finally {
+ LOGGER.info("Completed historical risk score metrics update in " + Duration.ofNanos(System.nanoTime() - startTimeNs));
+ }
+ }
+
+ public record ProjectProjection(long id, UUID uuid) {
+ }
+
+}
diff --git a/src/main/resources/migration/procedures/procedure_update_historical_risk_scores.sql b/src/main/resources/migration/procedures/procedure_update_historical_risk_scores.sql
new file mode 100644
index 000000000..0d1e275a1
--- /dev/null
+++ b/src/main/resources/migration/procedures/procedure_update_historical_risk_scores.sql
@@ -0,0 +1,15 @@
+CREATE OR REPLACE PROCEDURE "UPDATE_HISTORICAL_RISK_SCORES"()
+ LANGUAGE "plpgsql"
+AS
+$$
+DECLARE
+ "v_critical" INT; -- Number of vulnerabilities with critical severity
+ "v_high" INT; -- Number of vulnerabilities with high severity
+ "v_medium" INT; -- Number of vulnerabilities with medium severity
+ "v_low" INT; -- Number of vulnerabilities with low severity
+ "v_unassigned" INT; -- Number of vulnerabilities with unassigned severity
+ "v_risk_score" NUMERIC; -- Inherited risk score
+BEGIN
+ UPDATE "DEPENDENCYMETRICS" SET "v_risk_score" = "CALC_RISK_SCORE"("v_critical", "v_high", "v_medium", "v_low", "v_unassigned");
+END;
+$$;
\ No newline at end of file