diff --git a/src/main/java/org/dependencytrack/common/ConfigKey.java b/src/main/java/org/dependencytrack/common/ConfigKey.java index 0b7116812..3a62a6ccf 100644 --- a/src/main/java/org/dependencytrack/common/ConfigKey.java +++ b/src/main/java/org/dependencytrack/common/ConfigKey.java @@ -21,6 +21,7 @@ import alpine.Config; import java.time.Duration; +import java.util.concurrent.TimeUnit; public enum ConfigKey implements Config.Key { @@ -49,12 +50,11 @@ public enum ConfigKey implements Config.Key { CRON_EXPRESSION_FOR_LDAP_SYNC_TASK("task.cron.ldapSync", "0 */6 * * *"), CRON_EXPRESSION_FOR_REPO_META_ANALYSIS_TASK("task.cron.repoMetaAnalysis", "0 1 * * *"), CRON_EXPRESSION_FOR_VULN_ANALYSIS_TASK("task.cron.vulnAnalysis", "0 6 * * *"), - CRON_EXPRESSION_FOR_VULN_SCAN_CLEANUP_TASK("task.cron.vulnScanCleanUp", "5 8 * * 4"), CRON_EXPRESSION_FOR_FORTIFY_SSC_SYNC("task.cron.fortify.ssc.sync", "0 2 * * *"), CRON_EXPRESSION_FOR_DEFECT_DOJO_SYNC("task.cron.defectdojo.sync", "0 2 * * *"), CRON_EXPRESSION_FOR_KENNA_SYNC("task.cron.kenna.sync", "0 2 * * *"), - CRON_EXPRESSION_FOR_WORKFLOW_STATE_CLEANUP_TASK("task.cron.workflow.state.cleanup", "*/15 * * * *"), CRON_EXPRESSION_FOR_INTEGRITY_META_INITIALIZER_TASK("task.cron.integrityInitializer", "0 */12 * * *"), + CRON_EXPRESSION_FOR_HOUSEKEEPING_TASK("task.cron.housekeeping", "45 * * * *"), TASK_SCHEDULER_INITIAL_DELAY("task.scheduler.initial.delay", "180000"), TASK_SCHEDULER_POLLING_INTERVAL("task.scheduler.polling.interval", "60000"), TASK_PORTFOLIO_LOCK_AT_MOST_FOR("task.metrics.portfolio.lockAtMostForInMillis", "900000"), @@ -67,14 +67,14 @@ public enum ConfigKey implements Config.Key { TASK_COMPONENT_IDENTIFICATION_LOCK_AT_LEAST_FOR("task.componentIdentification.lockAtLeastForInMillis", "90000"), TASK_LDAP_SYNC_LOCK_AT_MOST_FOR("task.ldapSync.lockAtMostForInMillis", "900000"), TASK_LDAP_SYNC_LOCK_AT_LEAST_FOR("task.ldapSync.lockAtLeastForInMillis", "90000"), - TASK_WORKFLOW_STEP_CLEANUP_LOCK_AT_MOST_FOR("task.workflow.state.cleanup.lockAtMostForInMillis", String.valueOf(Duration.ofMinutes(15).toMillis())), - TASK_WORKFLOW_STEP_CLEANUP_LOCK_AT_LEAST_FOR("task.workflow.state.cleanup.lockAtLeastForInMillis", String.valueOf(Duration.ofMinutes(15).toMillis())), TASK_PORTFOLIO_REPO_META_ANALYSIS_LOCK_AT_MOST_FOR("task.portfolio.repoMetaAnalysis.lockAtMostForInMillis", String.valueOf(Duration.ofMinutes(15).toMillis())), TASK_PORTFOLIO_REPO_META_ANALYSIS_LOCK_AT_LEAST_FOR("task.portfolio.repoMetaAnalysis.lockAtLeastForInMillis", String.valueOf(Duration.ofMinutes(5).toMillis())), TASK_PORTFOLIO_VULN_ANALYSIS_LOCK_AT_MOST_FOR("task.portfolio.vulnAnalysis.lockAtMostForInMillis", String.valueOf(Duration.ofMinutes(15).toMillis())), TASK_PORTFOLIO_VULN_ANALYSIS_LOCK_AT_LEAST_FOR("task.portfolio.vulnAnalysis.lockAtLeastForInMillis", String.valueOf(Duration.ofMinutes(5).toMillis())), TASK_VULNERABILITY_POLICY_BUNDLE_FETCH_LOCK_AT_MOST_FOR("task.vulnerability.policy.bundle.fetch.lockAtMostForInMillis", String.valueOf(Duration.ofMinutes(5).toMillis())), TASK_VULNERABILITY_POLICY_BUNDLE_FETCH_LOCK_AT_LEAST_FOR("task.vulnerability.policy.bundle.fetch.lockAtLeastForInMillis", String.valueOf(Duration.ofSeconds(5).toMillis())), + TASK_HOUSEKEEPING_LOCK_AT_MOST_FOR("task.housekeeping.lockAtMostForInMillis", TimeUnit.MINUTES.toMillis(15)), + TASK_HOUSEKEEPING_LOCK_AT_LEAST_FOR("task.housekeeping.lockAtLeastForInMillis", TimeUnit.MINUTES.toMillis(5)), BOM_UPLOAD_PROCESSING_TRX_FLUSH_THRESHOLD("bom.upload.processing.trx.flush.threshold", "10000"), WORKFLOW_RETENTION_DURATION("workflow.retention.duration", "P3D"), WORKFLOW_STEP_TIMEOUT_DURATION("workflow.step.timeout.duration", "PT1H"), diff --git a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java index 61d48bf5a..030a440ba 100644 --- a/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java +++ b/src/main/java/org/dependencytrack/event/EventSubsystemInitializer.java @@ -22,6 +22,8 @@ import alpine.common.logging.Logger; import alpine.event.LdapSyncEvent; import alpine.event.framework.EventService; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; import org.dependencytrack.common.ConfigKey; import org.dependencytrack.tasks.BomUploadProcessingTask; import org.dependencytrack.tasks.CallbackTask; @@ -30,6 +32,7 @@ import org.dependencytrack.tasks.EpssMirrorTask; import org.dependencytrack.tasks.FortifySscUploadTask; import org.dependencytrack.tasks.GitHubAdvisoryMirrorTask; +import org.dependencytrack.tasks.HouseKeepingTask; import org.dependencytrack.tasks.IntegrityAnalysisTask; import org.dependencytrack.tasks.IntegrityMetaInitializerTask; import org.dependencytrack.tasks.InternalComponentIdentificationTask; @@ -42,15 +45,11 @@ import org.dependencytrack.tasks.TaskScheduler; import org.dependencytrack.tasks.VexUploadProcessingTask; import org.dependencytrack.tasks.VulnerabilityAnalysisTask; -import org.dependencytrack.tasks.vulnerabilitypolicy.VulnerabilityPolicyFetchTask; -import org.dependencytrack.tasks.VulnerabilityScanCleanupTask; -import org.dependencytrack.tasks.WorkflowStateCleanupTask; import org.dependencytrack.tasks.metrics.PortfolioMetricsUpdateTask; import org.dependencytrack.tasks.metrics.ProjectMetricsUpdateTask; import org.dependencytrack.tasks.metrics.VulnerabilityMetricsUpdateTask; +import org.dependencytrack.tasks.vulnerabilitypolicy.VulnerabilityPolicyFetchTask; -import jakarta.servlet.ServletContextEvent; -import jakarta.servlet.ServletContextListener; import java.time.Duration; /** @@ -93,15 +92,14 @@ public void contextInitialized(final ServletContextEvent event) { EVENT_SERVICE.subscribe(KennaSecurityUploadEventAbstract.class, KennaSecurityUploadTask.class); EVENT_SERVICE.subscribe(InternalComponentIdentificationEvent.class, InternalComponentIdentificationTask.class); EVENT_SERVICE.subscribe(CallbackEvent.class, CallbackTask.class); - EVENT_SERVICE.subscribe(VulnerabilityScanCleanupEvent.class, VulnerabilityScanCleanupTask.class); EVENT_SERVICE.subscribe(NistMirrorEvent.class, NistMirrorTask.class); EVENT_SERVICE.subscribe(VulnerabilityPolicyFetchEvent.class, VulnerabilityPolicyFetchTask.class); EVENT_SERVICE.subscribe(EpssMirrorEvent.class, EpssMirrorTask.class); EVENT_SERVICE.subscribe(ComponentPolicyEvaluationEvent.class, PolicyEvaluationTask.class); EVENT_SERVICE.subscribe(ProjectPolicyEvaluationEvent.class, PolicyEvaluationTask.class); - EVENT_SERVICE.subscribe(WorkflowStateCleanupEvent.class, WorkflowStateCleanupTask.class); EVENT_SERVICE.subscribe(IntegrityMetaInitializerEvent.class, IntegrityMetaInitializerTask.class); EVENT_SERVICE.subscribe(IntegrityAnalysisEvent.class, IntegrityAnalysisTask.class); + EVENT_SERVICE.subscribe(HouseKeepingEvent.class, HouseKeepingTask.class); TaskScheduler.getInstance(); } @@ -130,14 +128,13 @@ public void contextDestroyed(final ServletContextEvent event) { EVENT_SERVICE.unsubscribe(KennaSecurityUploadTask.class); EVENT_SERVICE.unsubscribe(InternalComponentIdentificationTask.class); EVENT_SERVICE.unsubscribe(CallbackTask.class); - EVENT_SERVICE.unsubscribe(VulnerabilityScanCleanupTask.class); EVENT_SERVICE.unsubscribe(NistMirrorTask.class); EVENT_SERVICE.unsubscribe(EpssMirrorTask.class); EVENT_SERVICE.unsubscribe(PolicyEvaluationTask.class); - EVENT_SERVICE.unsubscribe(WorkflowStateCleanupTask.class); EVENT_SERVICE.unsubscribe(IntegrityMetaInitializerTask.class); EVENT_SERVICE.unsubscribe(IntegrityAnalysisTask.class); EVENT_SERVICE.unsubscribe(VulnerabilityPolicyFetchTask.class); + EVENT_SERVICE.unsubscribe(HouseKeepingTask.class); EVENT_SERVICE.shutdown(DRAIN_TIMEOUT_DURATION); } } diff --git a/src/main/java/org/dependencytrack/event/WorkflowStateCleanupEvent.java b/src/main/java/org/dependencytrack/event/HouseKeepingEvent.java similarity index 91% rename from src/main/java/org/dependencytrack/event/WorkflowStateCleanupEvent.java rename to src/main/java/org/dependencytrack/event/HouseKeepingEvent.java index 365d47085..452fb3d76 100644 --- a/src/main/java/org/dependencytrack/event/WorkflowStateCleanupEvent.java +++ b/src/main/java/org/dependencytrack/event/HouseKeepingEvent.java @@ -20,5 +20,8 @@ import alpine.event.framework.Event; -public class WorkflowStateCleanupEvent implements Event { -} +/** + * @since 5.6.0 + */ +public class HouseKeepingEvent implements Event { +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/event/VulnerabilityScanCleanupEvent.java b/src/main/java/org/dependencytrack/event/VulnerabilityScanCleanupEvent.java deleted file mode 100644 index cb73da811..000000000 --- a/src/main/java/org/dependencytrack/event/VulnerabilityScanCleanupEvent.java +++ /dev/null @@ -1,31 +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.event; - -import alpine.event.framework.Event; -import org.dependencytrack.model.VulnerabilityScan; - -/** - * Defines an {@link Event} that is triggered to clean up {@link VulnerabilityScan}s - * that have not been updated for a certain amount of time. - * - * @since 5.0.0 - */ -public record VulnerabilityScanCleanupEvent() implements Event { -} diff --git a/src/main/java/org/dependencytrack/persistence/QueryManager.java b/src/main/java/org/dependencytrack/persistence/QueryManager.java index 5a9592da1..c7d86eb39 100644 --- a/src/main/java/org/dependencytrack/persistence/QueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/QueryManager.java @@ -1899,10 +1899,6 @@ public void updateWorkflowStateToFailed(WorkflowState workflowState, String fail getWorkflowStateQueryManager().updateWorkflowStateToFailed(workflowState, failureReason); } - public boolean hasWorkflowStepWithStatus(final UUID token, final WorkflowStep step, final WorkflowStatus status) { - return getWorkflowStateQueryManager().hasWorkflowStepWithStatus(token, step, status); - } - public IntegrityMetaComponent getIntegrityMetaComponent(String purl) { return getIntegrityMetaQueryManager().getIntegrityMetaComponent(purl); } diff --git a/src/main/java/org/dependencytrack/persistence/WorkflowStateQueryManager.java b/src/main/java/org/dependencytrack/persistence/WorkflowStateQueryManager.java index 11e1650c4..b98709057 100644 --- a/src/main/java/org/dependencytrack/persistence/WorkflowStateQueryManager.java +++ b/src/main/java/org/dependencytrack/persistence/WorkflowStateQueryManager.java @@ -35,7 +35,6 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.Map; import java.util.UUID; public class WorkflowStateQueryManager extends QueryManager implements IQueryManager { @@ -329,20 +328,4 @@ public void createWorkflowSteps(UUID token) { } } - public boolean hasWorkflowStepWithStatus(final UUID token, final WorkflowStep step, final WorkflowStatus status) { - final Query stateQuery = pm.newQuery(WorkflowState.class); - stateQuery.setFilter("token == :token && step == :step && status == :status"); - stateQuery.setNamedParameters(Map.of( - "token", token, - "step", step, - "status", status - )); - stateQuery.setResult("count(this)"); - try { - return stateQuery.executeResultUnique(Long.class) > 0; - } finally { - stateQuery.closeAll(); - } - } - } diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/VulnerabilityScanDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/VulnerabilityScanDao.java index c10f4d36d..b1d3a7a5f 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/VulnerabilityScanDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/VulnerabilityScanDao.java @@ -24,7 +24,9 @@ import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; import org.jdbi.v3.sqlobject.statement.SqlBatch; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import java.time.Duration; import java.util.List; public interface VulnerabilityScanDao extends SqlObject { @@ -54,9 +56,18 @@ THEN CASE WHEN (CAST("SCAN_FAILED" + :scannerResultsFailed AS DOUBLE PRECISION) """) @RegisterBeanMapper(VulnerabilityScan.class) @GetGeneratedKeys({"TOKEN", "STATUS", "TARGET_TYPE", "TARGET_IDENTIFIER", "FAILURE_REASON"}) - List updateAll(@Bind("token") List tokens, - @Bind List resultsTotal, - @Bind List scannerResultsTotal, - @Bind List scannerResultsFailed); + List updateAll( + @Bind("token") List tokens, + @Bind List resultsTotal, + @Bind List scannerResultsTotal, + @Bind List scannerResultsFailed + ); + + @SqlUpdate(""" + DELETE + FROM "VULNERABILITYSCAN" + WHERE AGE(NOW(), "UPDATED_AT") >= :duration + """) + int deleteAllForRetentionDuration(@Bind Duration duration); } diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/WorkflowDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/WorkflowDao.java index e79b4265d..ab61e5150 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/WorkflowDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/WorkflowDao.java @@ -21,19 +21,22 @@ import org.dependencytrack.model.WorkflowState; import org.dependencytrack.model.WorkflowStatus; import org.dependencytrack.model.WorkflowStep; +import org.jdbi.v3.sqlobject.SqlObject; import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; import org.jdbi.v3.sqlobject.customizer.Bind; import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; import org.jdbi.v3.sqlobject.statement.SqlBatch; import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; -public interface WorkflowDao { +public interface WorkflowDao extends SqlObject { @SqlBatch(""" UPDATE "WORKFLOW_STATE" @@ -46,15 +49,19 @@ public interface WorkflowDao { """) @GetGeneratedKeys("*") @RegisterBeanMapper(WorkflowState.class) - List updateAllStates(@Bind WorkflowStep step, - @Bind("token") List tokens, - @Bind("status") List statuses, - @Bind("failureReason") List failureReasons); + List updateAllStates( + @Bind WorkflowStep step, + @Bind("token") List tokens, + @Bind("status") List statuses, + @Bind("failureReason") List failureReasons + ); - default Optional updateState(final WorkflowStep step, - final String token, - final WorkflowStatus status, - final String failureReason) { + default Optional updateState( + final WorkflowStep step, + final String token, + final WorkflowStatus status, + final String failureReason + ) { final List updatedStates = updateAllStates(step, List.of(token), List.of(status), Collections.singletonList(failureReason)); if (updatedStates.isEmpty()) { return Optional.empty(); @@ -70,9 +77,11 @@ default Optional updateState(final WorkflowStep step, AND "STATUS" = :status AND "TOKEN" = ANY(:tokens) """) - Set getTokensByStepAndStateAndTokenAnyOf(@Bind WorkflowStep step, - @Bind WorkflowStatus status, - @Bind Collection tokens); + Set getTokensByStepAndStateAndTokenAnyOf( + @Bind WorkflowStep step, + @Bind WorkflowStatus status, + @Bind Collection tokens + ); @SqlBatch(""" WITH RECURSIVE @@ -95,8 +104,83 @@ Set getTokensByStepAndStateAndTokenAnyOf(@Bind WorkflowStep step, UPDATE "WORKFLOW_STATE" SET "STATUS" = 'CANCELLED' , "UPDATED_AT" = NOW() - WHERE "ID" IN (SELECT "ID" FROM "CTE_CHILDREN") + WHERE "ID" = ANY(SELECT "ID" FROM "CTE_CHILDREN") """) void cancelAllChildren(@Bind WorkflowStep step, @Bind("token") List tokens); + /** + * @since 5.6.0 + */ + @SqlBatch(""" + WITH RECURSIVE + "CTE_CHILDREN" ("ID") AS ( + SELECT "ID" + FROM "WORKFLOW_STATE" + WHERE "PARENT_STEP_ID" = :parentId + UNION ALL + SELECT "CHILD"."ID" + FROM "WORKFLOW_STATE" AS "CHILD" + INNER JOIN "CTE_CHILDREN" AS "PARENT" + ON "PARENT"."ID" = "CHILD"."PARENT_STEP_ID" + ) + UPDATE "WORKFLOW_STATE" + SET "STATUS" = 'CANCELLED' + , "UPDATED_AT" = NOW() + WHERE "ID" = ANY(SELECT "ID" FROM "CTE_CHILDREN") + """) + int[] cancelAllChildrenByParentStepIdAnyOf(@Bind("parentId") List parentIds); + + /** + * @since 5.6.0 + */ + @SqlUpdate(""" + UPDATE "WORKFLOW_STATE" + SET "STATUS" = 'TIMED_OUT' + , "UPDATED_AT" = NOW() + WHERE "STATUS" = 'PENDING' + AND AGE(NOW(), "UPDATED_AT") >= :timeoutDuration + """) + int transitionAllPendingStepsToTimedOutForTimeout(@Bind Duration timeoutDuration); + + /** + * @since 5.6.0 + */ + default List transitionAllTimedOutStepsToFailedForTimeout(final Duration timeoutDuration) { + // NB: Can't use interface method here due to https://github.com/jdbi/jdbi/issues/1807. + return getHandle().createUpdate(""" + UPDATE "WORKFLOW_STATE" + SET "STATUS" = 'FAILED' + , "FAILURE_REASON" = 'Timed out' + , "UPDATED_AT" = NOW() + WHERE "STATUS" = 'TIMED_OUT' + AND AGE(NOW(), "UPDATED_AT") >= :timeoutDuration + RETURNING "ID" + """) + .bind("timeoutDuration", timeoutDuration) + .executeAndReturnGeneratedKeys() + .mapTo(Long.class) + .list(); + } + + /** + * @since 5.6.0 + */ + @SqlUpdate(""" + WITH "CTE_ELIGIBLE_TOKENS" AS ( + SELECT "TOKEN" + FROM "WORKFLOW_STATE" AS "WFS_PARENT" + WHERE NOT EXISTS( + SELECT 1 + FROM "WORKFLOW_STATE" AS "WFS" + WHERE "WFS"."TOKEN" = "WFS_PARENT"."TOKEN" + AND "WFS"."STATUS" IN ('PENDING', 'TIMED_OUT')) + GROUP BY "TOKEN" + HAVING AGE(NOW(), MAX("UPDATED_AT")) >= :retentionDuration + ) + DELETE + FROM "WORKFLOW_STATE" + WHERE "TOKEN" = ANY(SELECT "TOKEN" FROM "CTE_ELIGIBLE_TOKENS") + """) + int deleteAllForRetention(@Bind Duration retentionDuration); + } diff --git a/src/main/java/org/dependencytrack/tasks/HouseKeepingTask.java b/src/main/java/org/dependencytrack/tasks/HouseKeepingTask.java new file mode 100644 index 000000000..3e8a48a8c --- /dev/null +++ b/src/main/java/org/dependencytrack/tasks/HouseKeepingTask.java @@ -0,0 +1,149 @@ +/* + * 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; + +import alpine.Config; +import alpine.common.logging.Logger; +import alpine.event.framework.Event; +import alpine.event.framework.Subscriber; +import org.dependencytrack.event.HouseKeepingEvent; +import org.dependencytrack.model.WorkflowStatus; +import org.dependencytrack.persistence.jdbi.VulnerabilityScanDao; +import org.dependencytrack.persistence.jdbi.WorkflowDao; +import org.dependencytrack.util.LockProvider; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; + +import static org.dependencytrack.common.ConfigKey.WORKFLOW_RETENTION_DURATION; +import static org.dependencytrack.common.ConfigKey.WORKFLOW_STEP_TIMEOUT_DURATION; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.inJdbiTransaction; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.useJdbiHandle; +import static org.dependencytrack.tasks.LockName.HOUSEKEEPING_TASK_LOCK; + +/** + * @since 5.6.0 + */ +public class HouseKeepingTask implements Subscriber { + + private static final Logger LOGGER = Logger.getLogger(HouseKeepingTask.class); + + private final Config config; + + @SuppressWarnings("unused") // Called by Alpine's event system + public HouseKeepingTask() { + this(Config.getInstance()); + } + + HouseKeepingTask(final Config config) { + this.config = config; + } + + @Override + public void inform(final Event event) { + if (!(event instanceof HouseKeepingEvent)) { + return; + } + + + LOGGER.info("Starting housekeeping activities"); + final long startTimeNs = System.nanoTime(); + + try { + LockProvider.executeWithLock(HOUSEKEEPING_TASK_LOCK, (Runnable) this::informLocked); + + final var taskDuration = Duration.ofNanos(System.nanoTime() - startTimeNs); + LOGGER.info("Housekeeping completed in %s".formatted(taskDuration)); + } catch (Throwable t) { + final var taskDuration = Duration.ofNanos(System.nanoTime() - startTimeNs); + LOGGER.error("Housekeeping failed to complete after %s".formatted(taskDuration), t); + } + } + + private void informLocked() { + try { + performVulnerabilityScanHouseKeeping(); + } catch (RuntimeException e) { + LOGGER.error("Failed to perform housekeeping of vulnerability scans", e); + } + + try { + performWorkflowHouseKeeping(); + } catch (RuntimeException e) { + LOGGER.error("Failed to perform housekeeping of workflows", e); + } + + // TODO: Enforce retention for metrics? + // TODO: Remove RepositoryMetaComponent records for which no matching Component exists anymore? + // TODO: Remove IntegrityMetaComponent records for which no matching Component exists anymore? + // TODO: Remove VulnerableSoftware records that are no longer associated with any vulnerability? + } + + private void performVulnerabilityScanHouseKeeping() { + final Duration retentionDuration = Duration.ofDays(1); // TODO: Make configurable? + + final int scansDeleted = inJdbiTransaction(handle -> { + final var dao = handle.attach(VulnerabilityScanDao.class); + return dao.deleteAllForRetentionDuration(retentionDuration); + }); + if (scansDeleted > 0) { + LOGGER.info("Deleted %s vulnerability scan(s) for retention duration %s" + .formatted(scansDeleted, retentionDuration)); + } + } + + private void performWorkflowHouseKeeping() { + final Duration timeoutDuration = Duration.parse(config.getProperty(WORKFLOW_STEP_TIMEOUT_DURATION)); + final Duration retentionDuration = Duration.parse(config.getProperty(WORKFLOW_RETENTION_DURATION)); + + useJdbiHandle(handle -> { + final var dao = handle.attach(WorkflowDao.class); + + final int numTimedOut = dao.transitionAllPendingStepsToTimedOutForTimeout(timeoutDuration); + if (numTimedOut > 0) { + LOGGER.warn("Transitioned %d workflow step(s) from %s to %s for timeout %s" + .formatted(numTimedOut, WorkflowStatus.PENDING, WorkflowStatus.TIMED_OUT, timeoutDuration)); + } + + handle.useTransaction(ignored -> { + final List failedStepIds = dao.transitionAllTimedOutStepsToFailedForTimeout(timeoutDuration); + if (failedStepIds.isEmpty()) { + return; + } + + LOGGER.warn("Transitioned %d workflow step(s) from %s to %s for timeout %s" + .formatted(failedStepIds.size(), WorkflowStatus.TIMED_OUT, WorkflowStatus.FAILED, timeoutDuration)); + + final int numCancelled = Arrays.stream(dao.cancelAllChildrenByParentStepIdAnyOf(failedStepIds)).sum(); + if (numCancelled > 0) { + LOGGER.warn("Transitioned %d workflow step(s) to %s because their parent steps transitioned to %s" + .formatted(numCancelled, WorkflowStatus.CANCELLED, WorkflowStatus.FAILED)); + } + }); + + final int numDeleted = dao.deleteAllForRetention(retentionDuration); + if (numDeleted > 0) { + LOGGER.info("Deleted %s workflow(s) that have not been updated within %s" + .formatted(numDeleted, retentionDuration)); + } + }); + } + +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/tasks/LockName.java b/src/main/java/org/dependencytrack/tasks/LockName.java index e94a215b1..52c1e651b 100644 --- a/src/main/java/org/dependencytrack/tasks/LockName.java +++ b/src/main/java/org/dependencytrack/tasks/LockName.java @@ -24,9 +24,9 @@ public enum LockName { EPSS_MIRROR_TASK_LOCK, VULNERABILITY_METRICS_TASK_LOCK, INTERNAL_COMPONENT_IDENTIFICATION_TASK_LOCK, - WORKFLOW_STEP_CLEANUP_TASK_LOCK, PORTFOLIO_REPO_META_ANALYSIS_TASK_LOCK, PORTFOLIO_VULN_ANALYSIS_TASK_LOCK, INTEGRITY_META_INITIALIZER_LOCK, - VULNERABILITY_POLICY_BUNDLE_FETCH_TASK_LOCK + VULNERABILITY_POLICY_BUNDLE_FETCH_TASK_LOCK, + HOUSEKEEPING_TASK_LOCK } diff --git a/src/main/java/org/dependencytrack/tasks/TaskScheduler.java b/src/main/java/org/dependencytrack/tasks/TaskScheduler.java index 8a86d789c..c71ab154f 100644 --- a/src/main/java/org/dependencytrack/tasks/TaskScheduler.java +++ b/src/main/java/org/dependencytrack/tasks/TaskScheduler.java @@ -30,6 +30,7 @@ import org.dependencytrack.event.EpssMirrorEvent; import org.dependencytrack.event.FortifySscUploadEventAbstract; import org.dependencytrack.event.GitHubAdvisoryMirrorEvent; +import org.dependencytrack.event.HouseKeepingEvent; import org.dependencytrack.event.IntegrityMetaInitializerEvent; import org.dependencytrack.event.InternalComponentIdentificationEvent; import org.dependencytrack.event.KennaSecurityUploadEventAbstract; @@ -40,8 +41,6 @@ import org.dependencytrack.event.PortfolioVulnerabilityAnalysisEvent; import org.dependencytrack.event.VulnerabilityMetricsUpdateEvent; import org.dependencytrack.event.VulnerabilityPolicyFetchEvent; -import org.dependencytrack.event.VulnerabilityScanCleanupEvent; -import org.dependencytrack.event.WorkflowStateCleanupEvent; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.persistence.QueryManager; @@ -53,6 +52,7 @@ import static org.dependencytrack.common.ConfigKey.CRON_EXPRESSION_FOR_COMPONENT_IDENTIFICATION_TASK; import static org.dependencytrack.common.ConfigKey.CRON_EXPRESSION_FOR_EPSS_MIRRORING_TASK; import static org.dependencytrack.common.ConfigKey.CRON_EXPRESSION_FOR_GITHUB_MIRRORING_TASK; +import static org.dependencytrack.common.ConfigKey.CRON_EXPRESSION_FOR_HOUSEKEEPING_TASK; import static org.dependencytrack.common.ConfigKey.CRON_EXPRESSION_FOR_INTEGRITY_META_INITIALIZER_TASK; import static org.dependencytrack.common.ConfigKey.CRON_EXPRESSION_FOR_LDAP_SYNC_TASK; import static org.dependencytrack.common.ConfigKey.CRON_EXPRESSION_FOR_NIST_MIRRORING_TASK; @@ -62,8 +62,6 @@ import static org.dependencytrack.common.ConfigKey.CRON_EXPRESSION_FOR_VULNERABILITY_METRICS_TASK; import static org.dependencytrack.common.ConfigKey.CRON_EXPRESSION_FOR_VULNERABILITY_POLICY_BUNDLE_FETCH_TASK; import static org.dependencytrack.common.ConfigKey.CRON_EXPRESSION_FOR_VULN_ANALYSIS_TASK; -import static org.dependencytrack.common.ConfigKey.CRON_EXPRESSION_FOR_VULN_SCAN_CLEANUP_TASK; -import static org.dependencytrack.common.ConfigKey.CRON_EXPRESSION_FOR_WORKFLOW_STATE_CLEANUP_TASK; import static org.dependencytrack.model.ConfigPropertyConstants.DEFECTDOJO_ENABLED; import static org.dependencytrack.model.ConfigPropertyConstants.FORTIFY_SSC_ENABLED; import static org.dependencytrack.model.ConfigPropertyConstants.KENNA_ENABLED; @@ -95,10 +93,9 @@ private TaskScheduler() { Map.entry(new VulnerabilityMetricsUpdateEvent(), Schedule.create(configInstance.getProperty(CRON_EXPRESSION_FOR_VULNERABILITY_METRICS_TASK))), Map.entry(new InternalComponentIdentificationEvent(), Schedule.create(configInstance.getProperty(CRON_EXPRESSION_FOR_COMPONENT_IDENTIFICATION_TASK))), Map.entry(new PortfolioVulnerabilityAnalysisEvent(), Schedule.create(configInstance.getProperty(CRON_EXPRESSION_FOR_VULN_ANALYSIS_TASK))), - Map.entry(new VulnerabilityScanCleanupEvent(), Schedule.create(configInstance.getProperty(CRON_EXPRESSION_FOR_VULN_SCAN_CLEANUP_TASK))), Map.entry(new PortfolioRepositoryMetaAnalysisEvent(), Schedule.create(configInstance.getProperty(CRON_EXPRESSION_FOR_REPO_META_ANALYSIS_TASK))), - Map.entry(new WorkflowStateCleanupEvent(), Schedule.create(configInstance.getProperty(CRON_EXPRESSION_FOR_WORKFLOW_STATE_CLEANUP_TASK))), - Map.entry(new IntegrityMetaInitializerEvent(), Schedule.create(configInstance.getProperty(CRON_EXPRESSION_FOR_INTEGRITY_META_INITIALIZER_TASK))) + Map.entry(new IntegrityMetaInitializerEvent(), Schedule.create(configInstance.getProperty(CRON_EXPRESSION_FOR_INTEGRITY_META_INITIALIZER_TASK))), + Map.entry(new HouseKeepingEvent(), Schedule.create(configInstance.getProperty(CRON_EXPRESSION_FOR_HOUSEKEEPING_TASK))) ); if (isTaskEnabled(FORTIFY_SSC_ENABLED)) { diff --git a/src/main/java/org/dependencytrack/tasks/VulnerabilityScanCleanupTask.java b/src/main/java/org/dependencytrack/tasks/VulnerabilityScanCleanupTask.java deleted file mode 100644 index 8a5aa2adf..000000000 --- a/src/main/java/org/dependencytrack/tasks/VulnerabilityScanCleanupTask.java +++ /dev/null @@ -1,68 +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.tasks; - -import alpine.common.logging.Logger; -import alpine.event.framework.Event; -import alpine.event.framework.Subscriber; -import org.dependencytrack.event.VulnerabilityScanCleanupEvent; -import org.dependencytrack.model.VulnerabilityScan; -import org.dependencytrack.persistence.QueryManager; - -import javax.jdo.Query; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Date; - -/** - * A {@link Subscriber} task that deletes {@link VulnerabilityScan}s from the database - * that have not been updated for over a day. - *

- * The purpose of this task is to prevent {@link VulnerabilityScan}s to increase in numbers indefinitely. - * - * @since 5.0.0 - */ -public class VulnerabilityScanCleanupTask implements Subscriber { - - private static final Logger LOGGER = Logger.getLogger(VulnerabilityScanCleanupTask.class); - - /** - * {@inheritDoc} - */ - @Override - public void inform(final Event event) { - if (event instanceof VulnerabilityScanCleanupEvent) { - final Instant threshold = Instant.now().minus(1, ChronoUnit.DAYS); - LOGGER.info("Cleaning up vulnerability scans that received no update since %s".formatted(threshold)); - - try (final var qm = new QueryManager(); - final Query query = qm.getPersistenceManager().newQuery(""" - DELETE FROM org.dependencytrack.model.VulnerabilityScan - WHERE this.updatedAt < :threshold - """)) { - query.setParameters(Date.from(threshold)); - final long numDeleted = query.executeResultUnique(Long.class); - LOGGER.info("Deleted %d vulnerability scans".formatted(numDeleted)); - } catch (Exception e) { - LOGGER.error("Cleaning up vulnerability scans failed", e); - } - } - } - -} diff --git a/src/main/java/org/dependencytrack/tasks/WorkflowStateCleanupTask.java b/src/main/java/org/dependencytrack/tasks/WorkflowStateCleanupTask.java deleted file mode 100644 index 87eff5a87..000000000 --- a/src/main/java/org/dependencytrack/tasks/WorkflowStateCleanupTask.java +++ /dev/null @@ -1,228 +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.tasks; - -import alpine.Config; -import alpine.common.logging.Logger; -import alpine.event.framework.Event; -import alpine.event.framework.Subscriber; -import org.apache.commons.collections4.ListUtils; -import org.dependencytrack.common.ConfigKey; -import org.dependencytrack.event.WorkflowStateCleanupEvent; -import org.dependencytrack.model.WorkflowState; -import org.dependencytrack.model.WorkflowStatus; -import org.dependencytrack.persistence.QueryManager; - -import javax.jdo.Query; -import java.time.Duration; -import java.time.Instant; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import static org.apache.commons.lang3.time.DateFormatUtils.ISO_8601_EXTENDED_DATETIME_FORMAT; -import static org.dependencytrack.tasks.LockName.WORKFLOW_STEP_CLEANUP_TASK_LOCK; -import static org.dependencytrack.util.LockProvider.executeWithLock; - -public class WorkflowStateCleanupTask implements Subscriber { - - private static final Logger LOGGER = Logger.getLogger(WorkflowStateCleanupTask.class); - - private final Duration stepTimeoutDuration; - private final Duration retentionDuration; - - - @SuppressWarnings("unused") // Called by Alpine's event system - public WorkflowStateCleanupTask() { - this( - Duration.parse(Config.getInstance().getProperty(ConfigKey.WORKFLOW_STEP_TIMEOUT_DURATION)), - Duration.parse(Config.getInstance().getProperty(ConfigKey.WORKFLOW_RETENTION_DURATION)) - ); - } - - WorkflowStateCleanupTask(final Duration stepTimeoutDuration, final Duration retentionDuration) { - this.stepTimeoutDuration = stepTimeoutDuration; - this.retentionDuration = retentionDuration; - } - - /** - * {@inheritDoc} - */ - @Override - public void inform(final Event e) { - if (e instanceof WorkflowStateCleanupEvent) { - final Instant now = Instant.now(); - final Date timeoutCutoff = Date.from(now.minus(stepTimeoutDuration)); - final Date retentionCutoff = Date.from(now.minus(retentionDuration)); - - executeWithLock(WORKFLOW_STEP_CLEANUP_TASK_LOCK, (Runnable) () -> { - try (final var qm = new QueryManager()) { - transitionPendingStepsToTimedOut(qm, timeoutCutoff); - transitionTimedOutStepsToFailed(qm, timeoutCutoff); - deleteExpiredWorkflows(qm, retentionCutoff); - } - }); - } - } - - /** - * Transition steps to {@link WorkflowStatus#TIMED_OUT}, if: - * - their state is non-terminal, and - * - they have not been updated for the threshold time frame - *

- * Because {@link WorkflowStatus#TIMED_OUT} states can still eventually become {@link WorkflowStatus#COMPLETED} - * or {@link WorkflowStatus#FAILED}, child steps do not have to be cancelled. - *

- * TODO: Change this to bulk update query once https://github.com/datanucleus/datanucleus-rdbms/issues/474 - * is resolved. Fetching all records and updating them individually is SUPER inefficient. - * - * @param qm The {@link QueryManager} to use - * @param timeoutCutoff The timeout cutoff - */ - private static void transitionPendingStepsToTimedOut(final QueryManager qm, final Date timeoutCutoff) { - final Query timeoutQuery = qm.getPersistenceManager().newQuery(WorkflowState.class); - timeoutQuery.setFilter("status == :status && updatedAt < :cutoff"); - timeoutQuery.setNamedParameters(Map.of( - "status", WorkflowStatus.PENDING, - "cutoff", timeoutCutoff - )); - int stepsTimedOut = 0; - try { - for (final WorkflowState state : timeoutQuery.executeList()) { - qm.runInTransaction(() -> { - state.setStatus(WorkflowStatus.TIMED_OUT); - state.setUpdatedAt(new Date()); - }); - stepsTimedOut++; - } - } finally { - timeoutQuery.closeAll(); - } - - if (stepsTimedOut > 0) { - LOGGER.warn("Transitioned %d workflow steps to %s state".formatted(stepsTimedOut, WorkflowStatus.TIMED_OUT)); - } else { - LOGGER.info("No workflow steps to transition to %s state".formatted(WorkflowStatus.TIMED_OUT)); - } - } - - /** - * Transition states to {@link WorkflowStatus#FAILED}, if their current status is {@link WorkflowStatus#TIMED_OUT}, - * and they have not been updated for the threshold time frame. - * - * @param qm The {@link QueryManager} to use - * @param timeoutCutoff The timeout cutoff - */ - private static void transitionTimedOutStepsToFailed(final QueryManager qm, final Date timeoutCutoff) { - final Query failedQuery = qm.getPersistenceManager().newQuery(WorkflowState.class); - failedQuery.setFilter("status == :status && updatedAt < :cutoff"); - failedQuery.setNamedParameters(Map.of( - "status", WorkflowStatus.TIMED_OUT, - "cutoff", timeoutCutoff - )); - int stepsFailed = 0; - int stepsCancelled = 0; - try { - for (final WorkflowState state : failedQuery.executeList()) { - stepsCancelled += qm.callInTransaction(() -> { - final Date now = new Date(); - state.setStatus(WorkflowStatus.FAILED); - state.setFailureReason("Timed out"); - state.setUpdatedAt(now); - return qm.updateAllDescendantStatesOfParent(state, WorkflowStatus.CANCELLED, now); - }); - stepsFailed++; - } - } finally { - failedQuery.closeAll(); - } - - if (stepsFailed > 0) { - LOGGER.warn("Transitioned %d %s workflow steps to %s status, and cancelled %d follow-up steps" - .formatted(stepsFailed, WorkflowStatus.TIMED_OUT, WorkflowStatus.FAILED, stepsCancelled)); - } else { - LOGGER.info("No %s workflow steps to transition to %s status" - .formatted(WorkflowStatus.TIMED_OUT, WorkflowStatus.FAILED)); - } - } - - /** - * Delete all {@link WorkflowState}s grouped by the same {@code token}, given all of their - * steps are in a terminal state, and their last update timestamp falls below {@code retentionCutoff}. - * - * @param qm The {@link QueryManager} to use - * @param retentionCutoff The retention cutoff time - */ - private static void deleteExpiredWorkflows(final QueryManager qm, final Date retentionCutoff) { - // Find all workflow tokens for which the most recently updated step falls below the - // retention cutoff time. - // TODO: There's likely a better way to do this. What we really want is all tokens where - // all steps' statuses are terminal, and "max(updatedAt) < :cutoff" is true. In JDO, - // the HAVING clause can only contain aggregates, but not individual fields, and we - // cannot aggregate the status field. - final Query deletionCandidatesQuery = qm.getPersistenceManager().newQuery(Query.JDOQL, """ - SELECT token FROM org.dependencytrack.model.WorkflowState - GROUP BY token - HAVING max(updatedAt) < :cutoff - """); - deletionCandidatesQuery.setNamedParameters(Map.of( - "cutoff", retentionCutoff - )); - final List deletableWorkflows; - try { - deletableWorkflows = List.copyOf(deletionCandidatesQuery.executeResultList(UUID.class)); - } finally { - deletionCandidatesQuery.closeAll(); - } - - // Bulk delete workflows based on the previously collected tokens. - // Do so in batches of up to 100 tokens in order to keep the query size manageable. - // Workflows are only deleted when all of their steps are in a terminal status. - long stepsDeleted = 0; - final List> tokenBatches = ListUtils.partition(deletableWorkflows, 100); - for (final List tokenBatch : tokenBatches) { - final Query workflowDeleteQuery = qm.getPersistenceManager().newQuery(Query.JDOQL, """ - DELETE FROM org.dependencytrack.model.WorkflowState - WHERE :tokens.contains(token) && ( - SELECT FROM org.dependencytrack.model.WorkflowState w - WHERE w.token == this.token && :nonTerminalStatuses.contains(w.status) - ).isEmpty() - """); - try { - stepsDeleted += qm.callInTransaction(() -> (long) workflowDeleteQuery.executeWithMap(Map.of( - "tokens", tokenBatch, - "nonTerminalStatuses", Set.of(WorkflowStatus.PENDING, WorkflowStatus.TIMED_OUT) - ))); - } finally { - workflowDeleteQuery.closeAll(); - } - } - - if (stepsDeleted > 0) { - LOGGER.info("Deleted %d workflow steps falling below retention cutoff %s" - .formatted(stepsDeleted, ISO_8601_EXTENDED_DATETIME_FORMAT.format(retentionCutoff))); - } else { - LOGGER.info("No workflows to delete for retention cutoff %s" - .formatted(ISO_8601_EXTENDED_DATETIME_FORMAT.format(retentionCutoff))); - } - } - -} diff --git a/src/main/java/org/dependencytrack/util/LockProvider.java b/src/main/java/org/dependencytrack/util/LockProvider.java index 9b401ec05..2d9e16de4 100644 --- a/src/main/java/org/dependencytrack/util/LockProvider.java +++ b/src/main/java/org/dependencytrack/util/LockProvider.java @@ -40,6 +40,8 @@ import static org.dependencytrack.common.ConfigKey.INTEGRITY_META_INITIALIZER_LOCK_AT_MOST_FOR; import static org.dependencytrack.common.ConfigKey.TASK_COMPONENT_IDENTIFICATION_LOCK_AT_LEAST_FOR; import static org.dependencytrack.common.ConfigKey.TASK_COMPONENT_IDENTIFICATION_LOCK_AT_MOST_FOR; +import static org.dependencytrack.common.ConfigKey.TASK_HOUSEKEEPING_LOCK_AT_LEAST_FOR; +import static org.dependencytrack.common.ConfigKey.TASK_HOUSEKEEPING_LOCK_AT_MOST_FOR; import static org.dependencytrack.common.ConfigKey.TASK_LDAP_SYNC_LOCK_AT_LEAST_FOR; import static org.dependencytrack.common.ConfigKey.TASK_LDAP_SYNC_LOCK_AT_MOST_FOR; import static org.dependencytrack.common.ConfigKey.TASK_METRICS_VULNERABILITY_LOCK_AT_LEAST_FOR; @@ -54,9 +56,8 @@ import static org.dependencytrack.common.ConfigKey.TASK_PORTFOLIO_VULN_ANALYSIS_LOCK_AT_MOST_FOR; import static org.dependencytrack.common.ConfigKey.TASK_VULNERABILITY_POLICY_BUNDLE_FETCH_LOCK_AT_LEAST_FOR; import static org.dependencytrack.common.ConfigKey.TASK_VULNERABILITY_POLICY_BUNDLE_FETCH_LOCK_AT_MOST_FOR; -import static org.dependencytrack.common.ConfigKey.TASK_WORKFLOW_STEP_CLEANUP_LOCK_AT_LEAST_FOR; -import static org.dependencytrack.common.ConfigKey.TASK_WORKFLOW_STEP_CLEANUP_LOCK_AT_MOST_FOR; import static org.dependencytrack.tasks.LockName.EPSS_MIRROR_TASK_LOCK; +import static org.dependencytrack.tasks.LockName.HOUSEKEEPING_TASK_LOCK; import static org.dependencytrack.tasks.LockName.INTEGRITY_META_INITIALIZER_LOCK; import static org.dependencytrack.tasks.LockName.INTERNAL_COMPONENT_IDENTIFICATION_TASK_LOCK; import static org.dependencytrack.tasks.LockName.LDAP_SYNC_TASK_LOCK; @@ -65,7 +66,6 @@ import static org.dependencytrack.tasks.LockName.PORTFOLIO_VULN_ANALYSIS_TASK_LOCK; import static org.dependencytrack.tasks.LockName.VULNERABILITY_METRICS_TASK_LOCK; import static org.dependencytrack.tasks.LockName.VULNERABILITY_POLICY_BUNDLE_FETCH_TASK_LOCK; -import static org.dependencytrack.tasks.LockName.WORKFLOW_STEP_CLEANUP_TASK_LOCK; public class LockProvider { @@ -138,46 +138,66 @@ private static DataSource getDataSourceUsingReflection(final Object connectionFa public static LockConfiguration getLockConfigurationByLockName(LockName lockName) { return switch (lockName) { - case PORTFOLIO_METRICS_TASK_LOCK -> new LockConfiguration(Instant.now(), + case PORTFOLIO_METRICS_TASK_LOCK -> new LockConfiguration( + Instant.now(), PORTFOLIO_METRICS_TASK_LOCK.name(), Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_PORTFOLIO_LOCK_AT_MOST_FOR)), - Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_PORTFOLIO_LOCK_AT_LEAST_FOR))); - case LDAP_SYNC_TASK_LOCK -> new LockConfiguration(Instant.now(), + Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_PORTFOLIO_LOCK_AT_LEAST_FOR)) + ); + case LDAP_SYNC_TASK_LOCK -> new LockConfiguration( + Instant.now(), LDAP_SYNC_TASK_LOCK.name(), Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_LDAP_SYNC_LOCK_AT_MOST_FOR)), - Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_LDAP_SYNC_LOCK_AT_LEAST_FOR))); - case EPSS_MIRROR_TASK_LOCK -> new LockConfiguration(Instant.now(), + Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_LDAP_SYNC_LOCK_AT_LEAST_FOR)) + ); + case EPSS_MIRROR_TASK_LOCK -> new LockConfiguration( + Instant.now(), EPSS_MIRROR_TASK_LOCK.name(), Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_MIRROR_EPSS_LOCK_AT_MOST_FOR)), - Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_MIRROR_EPSS_LOCK_AT_LEAST_FOR))); - case VULNERABILITY_METRICS_TASK_LOCK -> new LockConfiguration(Instant.now(), + Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_MIRROR_EPSS_LOCK_AT_LEAST_FOR)) + ); + case VULNERABILITY_METRICS_TASK_LOCK -> new LockConfiguration( + Instant.now(), VULNERABILITY_METRICS_TASK_LOCK.name(), Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_METRICS_VULNERABILITY_LOCK_AT_MOST_FOR)), - Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_METRICS_VULNERABILITY_LOCK_AT_LEAST_FOR))); - case INTERNAL_COMPONENT_IDENTIFICATION_TASK_LOCK -> new LockConfiguration(Instant.now(), + Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_METRICS_VULNERABILITY_LOCK_AT_LEAST_FOR)) + ); + case INTERNAL_COMPONENT_IDENTIFICATION_TASK_LOCK -> new LockConfiguration( + Instant.now(), INTERNAL_COMPONENT_IDENTIFICATION_TASK_LOCK.name(), Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_COMPONENT_IDENTIFICATION_LOCK_AT_MOST_FOR)), - Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_COMPONENT_IDENTIFICATION_LOCK_AT_LEAST_FOR))); - case WORKFLOW_STEP_CLEANUP_TASK_LOCK -> new LockConfiguration(Instant.now(), - WORKFLOW_STEP_CLEANUP_TASK_LOCK.name(), - Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_WORKFLOW_STEP_CLEANUP_LOCK_AT_MOST_FOR)), - Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_WORKFLOW_STEP_CLEANUP_LOCK_AT_LEAST_FOR))); - case PORTFOLIO_REPO_META_ANALYSIS_TASK_LOCK -> new LockConfiguration(Instant.now(), + Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_COMPONENT_IDENTIFICATION_LOCK_AT_LEAST_FOR)) + ); + case PORTFOLIO_REPO_META_ANALYSIS_TASK_LOCK -> new LockConfiguration( + Instant.now(), PORTFOLIO_REPO_META_ANALYSIS_TASK_LOCK.name(), Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_PORTFOLIO_REPO_META_ANALYSIS_LOCK_AT_MOST_FOR)), - Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_PORTFOLIO_REPO_META_ANALYSIS_LOCK_AT_LEAST_FOR))); - case PORTFOLIO_VULN_ANALYSIS_TASK_LOCK -> new LockConfiguration(Instant.now(), + Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_PORTFOLIO_REPO_META_ANALYSIS_LOCK_AT_LEAST_FOR)) + ); + case PORTFOLIO_VULN_ANALYSIS_TASK_LOCK -> new LockConfiguration( + Instant.now(), PORTFOLIO_VULN_ANALYSIS_TASK_LOCK.name(), Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_PORTFOLIO_VULN_ANALYSIS_LOCK_AT_MOST_FOR)), - Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_PORTFOLIO_VULN_ANALYSIS_LOCK_AT_LEAST_FOR))); - case INTEGRITY_META_INITIALIZER_LOCK -> new LockConfiguration(Instant.now(), + Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_PORTFOLIO_VULN_ANALYSIS_LOCK_AT_LEAST_FOR)) + ); + case INTEGRITY_META_INITIALIZER_LOCK -> new LockConfiguration( + Instant.now(), INTEGRITY_META_INITIALIZER_LOCK.name(), Duration.ofMillis(Config.getInstance().getPropertyAsInt(INTEGRITY_META_INITIALIZER_LOCK_AT_MOST_FOR)), - Duration.ofMillis(Config.getInstance().getPropertyAsInt(INTEGRITY_META_INITIALIZER_LOCK_AT_LEAST_FOR))); - case VULNERABILITY_POLICY_BUNDLE_FETCH_TASK_LOCK -> new LockConfiguration(Instant.now(), + Duration.ofMillis(Config.getInstance().getPropertyAsInt(INTEGRITY_META_INITIALIZER_LOCK_AT_LEAST_FOR)) + ); + case VULNERABILITY_POLICY_BUNDLE_FETCH_TASK_LOCK -> new LockConfiguration( + Instant.now(), VULNERABILITY_POLICY_BUNDLE_FETCH_TASK_LOCK.name(), Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_VULNERABILITY_POLICY_BUNDLE_FETCH_LOCK_AT_MOST_FOR)), - Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_VULNERABILITY_POLICY_BUNDLE_FETCH_LOCK_AT_LEAST_FOR))); + Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_VULNERABILITY_POLICY_BUNDLE_FETCH_LOCK_AT_LEAST_FOR)) + ); + case HOUSEKEEPING_TASK_LOCK -> new LockConfiguration( + Instant.now(), + HOUSEKEEPING_TASK_LOCK.name(), + Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_HOUSEKEEPING_LOCK_AT_MOST_FOR)), + Duration.ofMillis(Config.getInstance().getPropertyAsInt(TASK_HOUSEKEEPING_LOCK_AT_LEAST_FOR)) + ); }; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 57d90f4d8..4701f5c2b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1012,42 +1012,48 @@ task.ldapSync.lockAtLeastForInMillis=90000 # @category: Task Scheduling # @type: integer # @required -task.workflow.state.cleanup.lockAtMostForInMillis=900000 +task.portfolio.repoMetaAnalysis.lockAtMostForInMillis=900000 # @category: Task Scheduling # @type: integer # @required -task.workflow.state.cleanup.lockAtLeastForInMillis=900000 +task.portfolio.repoMetaAnalysis.lockAtLeastForInMillis=90000 # @category: Task Scheduling # @type: integer # @required -task.portfolio.repoMetaAnalysis.lockAtMostForInMillis=900000 +task.portfolio.vulnAnalysis.lockAtMostForInMillis=900000 # @category: Task Scheduling # @type: integer # @required -task.portfolio.repoMetaAnalysis.lockAtLeastForInMillis=90000 +task.portfolio.vulnAnalysis.lockAtLeastForInMillis=90000 # @category: Task Scheduling # @type: integer # @required -task.portfolio.vulnAnalysis.lockAtMostForInMillis=900000 +integrityMetaInitializer.lockAtMostForInMillis=900000 # @category: Task Scheduling # @type: integer # @required -task.portfolio.vulnAnalysis.lockAtLeastForInMillis=90000 +integrityMetaInitializer.lockAtLeastForInMillis=90000 +# Defines for how long the housekeeping task will hold its lock at most. +# Per default, the lock will be held for at most 15min. +# # @category: Task Scheduling # @type: integer # @required -integrityMetaInitializer.lockAtMostForInMillis=900000 +task.housekeeping.lockAtMostForInMillis=900000 +# Defines for how long the housekeeping task will hold its lock at least. +# Per default, the lock will be held for at least 5min. +# # @category: Task Scheduling # @type: integer # @required -integrityMetaInitializer.lockAtLeastForInMillis=90000 +task.housekeeping.lockAtLeastForInMillis=300000 # Schedule task for 10th minute of every hour # @@ -1112,13 +1118,6 @@ task.cron.repoMetaAnalysis=0 1 * * * # @required task.cron.vulnAnalysis=0 6 * * * -# Schedule task at 8:05 UTC on Wednesday every week -# -# @category: Task Scheduling -# @type: cron -# @required -task.cron.vulnScanCleanUp=5 8 * * 4 - # Schedule task every 5 minutes # # @category: Task Scheduling @@ -1147,19 +1146,20 @@ task.cron.defectdojo.sync=0 2 * * * # @required task.cron.kenna.sync=0 2 * * * -# Schedule task every 15 minutes +# Schedule task at 0 min past every 12th hr # # @category: Task Scheduling # @type: cron # @required -task.cron.workflow.state.cleanup=*/15 * * * * +task.cron.integrityInitializer=0 */12 * * * -# Schedule task at 0 min past every 12th hr +# Cron expression for scheduling the housekeeping task. +# Per default, the task will execute at the 45th minute of each hour. # # @category: Task Scheduling # @type: cron # @required -task.cron.integrityInitializer=0 */12 * * * +task.cron.housekeeping=45 * * * * # Defines the number of write operations to perform during BOM processing before changes are flushed to the database. # Smaller values may lower memory usage of the API server, whereas higher values will improve performance as fewer diff --git a/src/test/java/org/dependencytrack/tasks/WorkflowStateCleanupTaskTest.java b/src/test/java/org/dependencytrack/tasks/HouseKeepingTaskTest.java similarity index 74% rename from src/test/java/org/dependencytrack/tasks/WorkflowStateCleanupTaskTest.java rename to src/test/java/org/dependencytrack/tasks/HouseKeepingTaskTest.java index c7b19d679..176ddf15e 100644 --- a/src/test/java/org/dependencytrack/tasks/WorkflowStateCleanupTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/HouseKeepingTaskTest.java @@ -18,8 +18,10 @@ */ package org.dependencytrack.tasks; +import alpine.Config; import org.dependencytrack.PersistenceCapableTest; -import org.dependencytrack.event.WorkflowStateCleanupEvent; +import org.dependencytrack.event.HouseKeepingEvent; +import org.dependencytrack.model.VulnerabilityScan; import org.dependencytrack.model.WorkflowState; import org.dependencytrack.model.WorkflowStatus; import org.dependencytrack.model.WorkflowStep; @@ -35,11 +37,34 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.dependencytrack.common.ConfigKey.WORKFLOW_RETENTION_DURATION; +import static org.dependencytrack.common.ConfigKey.WORKFLOW_STEP_TIMEOUT_DURATION; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; -public class WorkflowStateCleanupTaskTest extends PersistenceCapableTest { +public class HouseKeepingTaskTest extends PersistenceCapableTest { @Test - public void testTransitionToTimedOut() { + public void testVulnerabilityScanHouseKeeping() { + qm.createVulnerabilityScan(VulnerabilityScan.TargetType.PROJECT, UUID.randomUUID(), "token-123", 5); + final var scanB = qm.createVulnerabilityScan(VulnerabilityScan.TargetType.PROJECT, UUID.randomUUID(), "token-xyz", 1); + qm.runInTransaction(() -> scanB.setUpdatedAt(Date.from(Instant.now().minus(25, ChronoUnit.HOURS)))); + final var scanC = qm.createVulnerabilityScan(VulnerabilityScan.TargetType.PROJECT, UUID.randomUUID(), "token-1y3", 3); + qm.runInTransaction(() -> scanC.setUpdatedAt(Date.from(Instant.now().minus(13, ChronoUnit.HOURS)))); + + final var configMock = spy(Config.class); + + final var task = new HouseKeepingTask(configMock); + assertThatNoException().isThrownBy(() -> task.inform(new HouseKeepingEvent())); + + assertThat(qm.getVulnerabilityScan("token-123")).isNotNull(); + assertThat(qm.getVulnerabilityScan("token-xyz")).isNull(); + assertThat(qm.getVulnerabilityScan("token-1y3")).isNotNull(); + } + + @Test + public void testWorkflowHouseKeepingWithTransitionToTimedOut() { final Duration timeoutDuration = Duration.ofHours(6); final Duration retentionDuration = Duration.ofHours(666); // Not relevant for this test. final Instant now = Instant.now(); @@ -61,7 +86,12 @@ public void testTransitionToTimedOut() { childState.setUpdatedAt(Date.from(timeoutCutoff.plus(1, ChronoUnit.HOURS))); qm.persist(childState); - new WorkflowStateCleanupTask(timeoutDuration, retentionDuration).inform(new WorkflowStateCleanupEvent()); + final var configMock = spy(Config.class); + doReturn(retentionDuration.toString()).when(configMock).getProperty(eq(WORKFLOW_RETENTION_DURATION)); + doReturn(timeoutDuration.toString()).when(configMock).getProperty(eq(WORKFLOW_STEP_TIMEOUT_DURATION)); + + final var task = new HouseKeepingTask(configMock); + assertThatNoException().isThrownBy(() -> task.inform(new HouseKeepingEvent())); qm.getPersistenceManager().refreshAll(parentState, childState); assertThat(parentState.getStatus()).isEqualTo(WorkflowStatus.TIMED_OUT); @@ -71,7 +101,7 @@ public void testTransitionToTimedOut() { } @Test - public void testTransitionTimedOutToFailed() { + public void testWorkflowHouseKeepingWithTransitionTimedOutToFailed() { final Duration timeoutDuration = Duration.ofHours(6); final Duration retentionDuration = Duration.ofHours(666); // Not relevant for this test. final Instant now = Instant.now(); @@ -93,7 +123,12 @@ public void testTransitionTimedOutToFailed() { childState.setUpdatedAt(Date.from(timeoutCutoff.plus(1, ChronoUnit.HOURS))); qm.persist(childState); - new WorkflowStateCleanupTask(timeoutDuration, retentionDuration).inform(new WorkflowStateCleanupEvent()); + final var configMock = spy(Config.class); + doReturn(retentionDuration.toString()).when(configMock).getProperty(eq(WORKFLOW_RETENTION_DURATION)); + doReturn(timeoutDuration.toString()).when(configMock).getProperty(eq(WORKFLOW_STEP_TIMEOUT_DURATION)); + + final var task = new HouseKeepingTask(configMock); + assertThatNoException().isThrownBy(() -> task.inform(new HouseKeepingEvent())); qm.getPersistenceManager().refreshAll(parentState, childState); assertThat(parentState.getStatus()).isEqualTo(WorkflowStatus.FAILED); @@ -105,7 +140,7 @@ public void testTransitionTimedOutToFailed() { } @Test - public void testDeleteExpiredWorkflows() { + public void testWorkflowHouseKeepingWithDeleteExpired() { final Duration timeoutDuration = Duration.ofHours(666); // Not relevant for this test. final Duration retentionDuration = Duration.ofHours(6); final Instant now = Instant.now(); @@ -166,7 +201,12 @@ public void testDeleteExpiredWorkflows() { childStateC.setUpdatedAt(Date.from(retentionCutoff.minus(1, ChronoUnit.HOURS))); qm.persist(childStateC); - new WorkflowStateCleanupTask(timeoutDuration, retentionDuration).inform(new WorkflowStateCleanupEvent()); + final var configMock = spy(Config.class); + doReturn(retentionDuration.toString()).when(configMock).getProperty(eq(WORKFLOW_RETENTION_DURATION)); + doReturn(timeoutDuration.toString()).when(configMock).getProperty(eq(WORKFLOW_STEP_TIMEOUT_DURATION)); + + final var task = new HouseKeepingTask(configMock); + assertThatNoException().isThrownBy(() -> task.inform(new HouseKeepingEvent())); // Workflow A must've been deleted, because all steps are in terminal status, and fall below the retention cutoff. assertThatExceptionOfType(JDOObjectNotFoundException.class).isThrownBy(() -> qm.getObjectById(WorkflowState.class, childStateA.getId())); diff --git a/src/test/java/org/dependencytrack/tasks/TaskSchedulerTest.java b/src/test/java/org/dependencytrack/tasks/TaskSchedulerTest.java index 8b3e15066..02d234cb1 100644 --- a/src/test/java/org/dependencytrack/tasks/TaskSchedulerTest.java +++ b/src/test/java/org/dependencytrack/tasks/TaskSchedulerTest.java @@ -19,10 +19,11 @@ package org.dependencytrack.tasks; import alpine.Config; +import alpine.event.framework.Event; import alpine.event.framework.EventService; +import alpine.event.framework.Subscriber; import org.dependencytrack.PersistenceCapableTest; -import org.dependencytrack.event.VulnerabilityScanCleanupEvent; -import org.dependencytrack.model.VulnerabilityScan; +import org.dependencytrack.event.PortfolioMetricsUpdateEvent; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -31,62 +32,66 @@ import org.junit.Test; import org.junit.contrib.java.lang.system.EnvironmentVariables; -import java.sql.Date; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.UUID; +import java.time.Duration; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; public class TaskSchedulerTest extends PersistenceCapableTest { - TaskScheduler taskScheduler; @Rule public EnvironmentVariables environmentVariables = new EnvironmentVariables(); + private static final Queue EVENTS = new ConcurrentLinkedQueue<>(); + + public static class TestSubscriber implements Subscriber { + + @Override + public void inform(final Event event) { + EVENTS.add(event); + } + + } + @BeforeClass public static void setUpClass() { Config.enableUnitTests(); - EventService.getInstance().subscribe(VulnerabilityScanCleanupEvent.class, VulnerabilityScanCleanupTask.class); + + EventService.getInstance().subscribe(PortfolioMetricsUpdateEvent.class, TestSubscriber.class); } @AfterClass public static void tearDownClass() { - EventService.getInstance().unsubscribe(VulnerabilityScanCleanupTask.class); + EventService.getInstance().unsubscribe(TestSubscriber.class); } @Before public void before() throws Exception { - environmentVariables.set("TASK_CRON_VULNSCANCLEANUP", "* * * * * *"); + environmentVariables.set("TASK_CRON_METRICS_PORTFOLIO", "* * * * * *"); environmentVariables.set("TASK_SCHEDULER_INITIAL_DELAY", "5"); environmentVariables.set("TASK_SCHEDULER_POLLING_INTERVAL", "1000"); + super.before(); + + // Force initialization of TaskScheduler. + final var ignored = TaskScheduler.getInstance(); } @After public void after() { - environmentVariables.clear("TASK_CRON_VULNSCANCLEANUP", - "TASK_SCHEDULER_INITIAL_DELAY", - "TASK_SCHEDULER_POLLING_INTERVAL"); - taskScheduler.shutdown(); + TaskScheduler.getInstance().shutdown(); + EVENTS.clear(); + super.after(); } @Test public void test() throws Exception { - - taskScheduler = TaskScheduler.getInstance(); - - qm.createVulnerabilityScan(VulnerabilityScan.TargetType.PROJECT, UUID.randomUUID(), "token-123", 5); - final var scanB = qm.createVulnerabilityScan(VulnerabilityScan.TargetType.PROJECT, UUID.randomUUID(), "token-xyz", 1); - qm.runInTransaction(() -> scanB.setUpdatedAt(Date.from(Instant.now().minus(25, ChronoUnit.HOURS)))); - final var scanC = qm.createVulnerabilityScan(VulnerabilityScan.TargetType.PROJECT, UUID.randomUUID(), "token-1y3", 3); - qm.runInTransaction(() -> scanC.setUpdatedAt(Date.from(Instant.now().minus(13, ChronoUnit.HOURS)))); - //Sleeping for 500ms after initial delay so event would be sent - Thread.sleep(1000); - - assertThat(qm.getVulnerabilityScan("token-123")).isNotNull(); - assertThat(qm.getVulnerabilityScan("token-xyz")).isNull(); - assertThat(qm.getVulnerabilityScan("token-1y3")).isNotNull(); + await("Event Dispatch") + .atMost(Duration.ofSeconds(3)) + .untilAsserted(() -> assertThat(EVENTS).hasSize(1)); } -} + +} \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/tasks/VulnerabilityScanCleanupTaskTest.java b/src/test/java/org/dependencytrack/tasks/VulnerabilityScanCleanupTaskTest.java deleted file mode 100644 index 0d5b9ca71..000000000 --- a/src/test/java/org/dependencytrack/tasks/VulnerabilityScanCleanupTaskTest.java +++ /dev/null @@ -1,50 +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.tasks; - -import org.dependencytrack.PersistenceCapableTest; -import org.dependencytrack.event.VulnerabilityScanCleanupEvent; -import org.dependencytrack.model.VulnerabilityScan; -import org.junit.Test; - -import java.sql.Date; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -public class VulnerabilityScanCleanupTaskTest extends PersistenceCapableTest { - - @Test - public void testInform() { - qm.createVulnerabilityScan(VulnerabilityScan.TargetType.PROJECT, UUID.randomUUID(), "token-123", 5); - final var scanB = qm.createVulnerabilityScan(VulnerabilityScan.TargetType.PROJECT, UUID.randomUUID(), "token-xyz", 1); - qm.runInTransaction(() -> scanB.setUpdatedAt(Date.from(Instant.now().minus(25, ChronoUnit.HOURS)))); - final var scanC = qm.createVulnerabilityScan(VulnerabilityScan.TargetType.PROJECT, UUID.randomUUID(), "token-1y3", 3); - qm.runInTransaction(() -> scanC.setUpdatedAt(Date.from(Instant.now().minus(13, ChronoUnit.HOURS)))); - - new VulnerabilityScanCleanupTask().inform(new VulnerabilityScanCleanupEvent()); - - assertThat(qm.getVulnerabilityScan("token-123")).isNotNull(); - assertThat(qm.getVulnerabilityScan("token-xyz")).isNull(); - assertThat(qm.getVulnerabilityScan("token-1y3")).isNotNull(); - } - -} \ No newline at end of file