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