diff --git a/apps/dashboard/src/main/java/com/akto/action/threat_detection/ThreatConfiguration.java b/apps/dashboard/src/main/java/com/akto/action/threat_detection/ThreatConfiguration.java
index 81a3065cce..40cad38b0c 100644
--- a/apps/dashboard/src/main/java/com/akto/action/threat_detection/ThreatConfiguration.java
+++ b/apps/dashboard/src/main/java/com/akto/action/threat_detection/ThreatConfiguration.java
@@ -10,6 +10,7 @@ public class ThreatConfiguration {
private Actor actor;
private RatelimitConfig ratelimitConfig;
+ private Integer archivalDays;
@lombok.Getter
@lombok.Setter
diff --git a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/settings/threat_configuration/ArchivalConfigComponent.jsx b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/settings/threat_configuration/ArchivalConfigComponent.jsx
new file mode 100644
index 0000000000..2739fafc15
--- /dev/null
+++ b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/settings/threat_configuration/ArchivalConfigComponent.jsx
@@ -0,0 +1,87 @@
+import { useState, useEffect } from "react";
+import func from "@/util/func"
+import { LegacyCard, VerticalStack, Divider, Text, Button, Box } from "@shopify/polaris";
+import api from "../../../pages/threat_detection/api.js";
+import Dropdown from "../../../components/layouts/Dropdown.jsx";
+
+const ArchivalConfigComponent = ({ title, description }) => {
+ const [archivalDays, setArchivalDays] = useState(60);
+ const [isSaveDisabled, setIsSaveDisabled] = useState(true);
+
+ const fetchData = async () => {
+ const response = await api.fetchThreatConfiguration();
+ const days = response?.threatConfiguration?.archivalDays;
+ const value = days === 30 || days === 60 || days === 90 ? days : 60;
+ setArchivalDays(value);
+ setIsSaveDisabled(true);
+ };
+
+ const onSave = async () => {
+ const payload = {
+ archivalDays: archivalDays
+ };
+ await api.modifyThreatConfiguration(payload).then(() => {
+ try {
+ func.setToast(true, false, "Archival time saved successfully");
+ fetchData()
+ } catch (error) {
+ func.setToast(true, true, "Error saving archival time");
+ }
+ });
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const options = [
+ { value: 30, label: "30 days" },
+ { value: 60, label: "60 days" },
+ { value: 90, label: "90 days" },
+ ];
+
+ function TitleComponent({ title, description }) {
+ return (
+
+ {title}
+
+ {description}
+
+
+ )
+ }
+
+ const onChange = (val) => {
+ setArchivalDays(val);
+ setIsSaveDisabled(false);
+ };
+
+ return (
+ }
+ primaryFooterAction={{
+ content: 'Save',
+ onAction: onSave,
+ loading: false,
+ disabled: isSaveDisabled
+ }}
+ >
+
+
+
+
+ onChange(val)}
+ label="Archival Time"
+ initial={() => archivalDays}
+ />
+
+
+
+
+ );
+};
+
+export default ArchivalConfigComponent;
+
+
diff --git a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/settings/threat_configuration/ThreatConfiguration.jsx b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/settings/threat_configuration/ThreatConfiguration.jsx
index fc8f256711..706974be45 100644
--- a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/settings/threat_configuration/ThreatConfiguration.jsx
+++ b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/settings/threat_configuration/ThreatConfiguration.jsx
@@ -1,6 +1,7 @@
import PageWithMultipleCards from "../../../components/layouts/PageWithMultipleCards"
import ThreatActorConfigComponent from "./ThreatActorConfig.jsx"
import RatelimitConfigComponent from "./RatelimitConfigComponent.jsx"
+import ArchivalConfigComponent from "./ArchivalConfigComponent.jsx"
function ThreatConfiguration() {
@@ -14,6 +15,11 @@ function ThreatConfiguration() {
title={"Rate Limit Configuration"}
description={"Configure rate limiting rules to protect your APIs from abuse."}
key={"ratelimitConfig"}
+ />,
+
];
diff --git a/apps/threat-detection-backend/src/main/java/com/akto/threat/backend/Main.java b/apps/threat-detection-backend/src/main/java/com/akto/threat/backend/Main.java
index acac265e58..50c006e74c 100644
--- a/apps/threat-detection-backend/src/main/java/com/akto/threat/backend/Main.java
+++ b/apps/threat-detection-backend/src/main/java/com/akto/threat/backend/Main.java
@@ -14,6 +14,7 @@
import com.akto.threat.backend.service.ThreatActorService;
import com.akto.threat.backend.service.ThreatApiService;
import com.akto.threat.backend.tasks.FlushMessagesToDB;
+import com.akto.threat.backend.cron.ArchiveOldMaliciousEventsCron;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.ReadPreference;
@@ -73,6 +74,14 @@ public static void main(String[] args) throws Exception {
ApiDistributionDataService apiDistributionDataService = new ApiDistributionDataService(threatProtectionMongo);
new BackendVerticle(maliciousEventService, threatActorService, threatApiService, apiDistributionDataService).start();
+
+ try {
+ logger.infoAndAddToDb("Starting ArchiveOldMaliciousEventsCron for all databases", LoggerMaker.LogDb.RUNTIME);
+ new ArchiveOldMaliciousEventsCron(threatProtectionMongo).cron();
+ logger.infoAndAddToDb("Scheduled ArchiveOldMaliciousEventsCron", LoggerMaker.LogDb.RUNTIME);
+ } catch (Exception e) {
+ logger.errorAndAddToDb("Error starting ArchiveOldMaliciousEventsCron: " + e.getMessage(), LoggerMaker.LogDb.RUNTIME);
+ }
}
}
diff --git a/apps/threat-detection-backend/src/main/java/com/akto/threat/backend/cron/ArchiveOldMaliciousEventsCron.java b/apps/threat-detection-backend/src/main/java/com/akto/threat/backend/cron/ArchiveOldMaliciousEventsCron.java
new file mode 100644
index 0000000000..c8d0516d28
--- /dev/null
+++ b/apps/threat-detection-backend/src/main/java/com/akto/threat/backend/cron/ArchiveOldMaliciousEventsCron.java
@@ -0,0 +1,276 @@
+package com.akto.threat.backend.cron;
+
+import com.akto.log.LoggerMaker;
+import com.akto.dao.context.Context;
+import com.mongodb.MongoBulkWriteException;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoCursor;
+import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.Filters;
+import com.mongodb.client.model.InsertOneModel;
+import com.mongodb.client.model.BulkWriteOptions;
+import com.mongodb.client.model.Sorts;
+import com.mongodb.client.model.WriteModel;
+import org.bson.Document;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class ArchiveOldMaliciousEventsCron implements Runnable {
+
+ private static final LoggerMaker logger = new LoggerMaker(ArchiveOldMaliciousEventsCron.class);
+
+ private static final String SOURCE_COLLECTION = "malicious_events";
+ private static final String DEST_COLLECTION = "archived_malicious_events";
+ private static final int BATCH_SIZE = 5000;
+ private static final long DEFAULT_RETENTION_DAYS = 60L; // default, can be overridden from DB
+ private static final long MIN_RETENTION_DAYS = 30L;
+ private static final long MAX_RETENTION_DAYS = 90L;
+ private static final long MAX_SOURCE_DOCS = 400_000L; // cap size
+ private static final long MAX_DELETES_PER_ITERATION = 100_000L; // cap per cron iteration
+
+ private final MongoClient mongoClient;
+ private final ScheduledExecutorService scheduler;
+
+ public ArchiveOldMaliciousEventsCron(MongoClient mongoClient) {
+ this.mongoClient = mongoClient;
+ this.scheduler = Executors.newScheduledThreadPool(1);
+ }
+
+ public void cron() {
+ long initialDelaySeconds = 0;
+ long periodSeconds = Duration.ofHours(6).getSeconds();
+ scheduler.scheduleAtFixedRate(this, initialDelaySeconds, periodSeconds, TimeUnit.SECONDS);
+ logger.infoAndAddToDb("Scheduled ArchiveOldMaliciousEventsCron every 6 hours", LoggerMaker.LogDb.RUNTIME);
+ }
+
+ @Override
+ public void run() {
+ try {
+ runOnce();
+ } catch (Throwable t) {
+ logger.errorAndAddToDb("Archive cron failed unexpectedly: " + t.getMessage(), LoggerMaker.LogDb.RUNTIME);
+ }
+ }
+
+ public void runOnce() {
+ long nowSeconds = System.currentTimeMillis() / 1000L; // epoch seconds
+
+ try (MongoCursor dbNames = mongoClient.listDatabaseNames().cursor()) {
+ while (dbNames.hasNext()) {
+ String dbName = dbNames.next();
+ if (shouldSkipDatabase(dbName)) continue;
+
+ Integer accId = null;
+ try {
+ try {
+ accId = Integer.parseInt(dbName);
+ Context.accountId.set(accId);
+ } catch (Exception ignore) {
+ // leave context unset for non-numeric db names
+ }
+ if (accId != null) {
+ archiveOldMaliciousEvents(dbName, nowSeconds);
+ } else {
+ logger.infoAndAddToDb("Skipping archive for db as context wasn't set: " + dbName, LoggerMaker.LogDb.RUNTIME);
+ }
+ } catch (Exception e) {
+ logger.errorAndAddToDb("Error processing database: " + dbName + " : " + e.getMessage(), LoggerMaker.LogDb.RUNTIME);
+ } finally {
+ Context.resetContextThreadLocals();
+ }
+ }
+ }
+ }
+
+ private boolean shouldSkipDatabase(String dbName) {
+ return dbName == null || dbName.isEmpty()
+ || "admin".equals(dbName)
+ || "local".equals(dbName)
+ || "config".equals(dbName);
+ }
+
+ private void archiveOldMaliciousEvents(String dbName, long nowSeconds) {
+ MongoDatabase db = mongoClient.getDatabase(dbName);
+ if (!ensureCollectionExists(db, DEST_COLLECTION)) {
+ logger.infoAndAddToDb("Archive collection missing, skipping db: " + dbName, LoggerMaker.LogDb.RUNTIME);
+ return;
+ }
+
+ long retentionDays = fetchRetentionDays(db);
+ long threshold = nowSeconds - (retentionDays * 24 * 60 * 60);
+
+ MongoCollection source = db.getCollection(SOURCE_COLLECTION, Document.class);
+ MongoCollection dest = db.getCollection(DEST_COLLECTION, Document.class);
+
+ int totalMoved = 0;
+ long deletesThisIteration = 0L;
+
+ while (true) {
+ long iterationStartNanos = System.nanoTime();
+ List batch = new ArrayList<>(BATCH_SIZE);
+ try (MongoCursor cursor = source
+ .find(Filters.lte("detectedAt", threshold))
+ .sort(Sorts.ascending("detectedAt"))
+ .limit(BATCH_SIZE)
+ .cursor()) {
+ while (cursor.hasNext()) {
+ batch.add(cursor.next());
+ }
+ }
+
+ if (batch.isEmpty()) break;
+
+ Set