From efa57742728be1f89308864f571a2e6cd6f4f88f Mon Sep 17 00:00:00 2001 From: nfebe Date: Tue, 23 Dec 2025 14:29:42 +0100 Subject: [PATCH 1/2] feat(security): Add UI for configurable detection thresholds - Add form fields for detection window and thresholds in SettingsView - Update reactive state and save function to include new fields - Load threshold values from API response - Add subsection styling for detection thresholds group --- src/views/SettingsView.vue | 104 +++++++++++++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 9 deletions(-) diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index 6a43e3e..dac88e3 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -539,23 +539,74 @@
- - Number of security events before auto-blocking an IP + + How long to block IPs automatically (e.g., 24h, 1h, 30m)
- - How long to block IPs automatically (e.g., 24h, 1h, 30m) + + Time window for counting violations (e.g., 2m, 5m) +
+
+ +

Detection Thresholds

+

+ IPs exceeding these thresholds within the detection window will be auto-blocked. +

+ +
+
+ + Max 404 errors (path probing) + +
+ +
+ + Max 401/403 responses + +
+ +
+ + Max different paths (scanning) + +
+ +
+ + Max hits to same path (hammering) +
@@ -827,6 +878,11 @@ const securitySettings = reactive({ auto_block_enabled: false, auto_block_threshold: 50, auto_block_duration: "24h", + detection_window: "2m", + not_found_threshold: 10, + auth_failure_threshold: 5, + unique_paths_threshold: 20, + repeated_hits_threshold: 30, }); const savingSecurity = ref(false); @@ -1002,6 +1058,11 @@ const fetchSettings = async () => { securitySettings.auto_block_enabled = data.security.auto_block_enabled ?? false; securitySettings.auto_block_threshold = data.security.auto_block_threshold || 50; securitySettings.auto_block_duration = data.security.auto_block_duration || "24h"; + securitySettings.detection_window = data.security.detection_window || "2m"; + securitySettings.not_found_threshold = data.security.not_found_threshold || 10; + securitySettings.auth_failure_threshold = data.security.auth_failure_threshold || 5; + securitySettings.unique_paths_threshold = data.security.unique_paths_threshold || 20; + securitySettings.repeated_hits_threshold = data.security.repeated_hits_threshold || 30; } } catch (e: any) { notifications.error("Error", "Failed to load settings"); @@ -1092,6 +1153,11 @@ const saveSecuritySettings = async () => { auto_block_enabled: securitySettings.auto_block_enabled, auto_block_threshold: securitySettings.auto_block_threshold, auto_block_duration: securitySettings.auto_block_duration, + detection_window: securitySettings.detection_window, + not_found_threshold: securitySettings.not_found_threshold, + auth_failure_threshold: securitySettings.auth_failure_threshold, + unique_paths_threshold: securitySettings.unique_paths_threshold, + repeated_hits_threshold: securitySettings.repeated_hits_threshold, }); const data = response.data; @@ -1122,6 +1188,11 @@ const saveSecuritySettings = async () => { securitySettings.auto_block_enabled = data.security.auto_block_enabled; securitySettings.auto_block_threshold = data.security.auto_block_threshold; securitySettings.auto_block_duration = data.security.auto_block_duration; + securitySettings.detection_window = data.security.detection_window; + securitySettings.not_found_threshold = data.security.not_found_threshold; + securitySettings.auth_failure_threshold = data.security.auth_failure_threshold; + securitySettings.unique_paths_threshold = data.security.unique_paths_threshold; + securitySettings.repeated_hits_threshold = data.security.repeated_hits_threshold; } } catch (e: any) { const errorMsg = e.response?.data?.error || "Failed to save security settings"; @@ -1548,6 +1619,21 @@ onMounted(() => { color: #9ca3af; } +.subsection-title { + font-size: 0.875rem; + font-weight: 600; + color: #374151; + margin: 1.25rem 0 0.25rem; + padding-top: 1rem; + border-top: 1px solid #f3f4f6; +} + +.subsection-hint { + font-size: 0.75rem; + color: #6b7280; + margin-bottom: 1rem; +} + .form-input { padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; From 9f11f6e409ec6c3d6ae202ffa2ee43cefc5cb930 Mon Sep 17 00:00:00 2001 From: nfebe Date: Wed, 24 Dec 2025 01:52:13 +0100 Subject: [PATCH 2/2] refactor(stats): Optimize polling to reduce API requests - Remove redundant API calls from stats store (networks, certs, etc.) - Pages now fetch their own data on mount - Polling interval changed from 10s to 15s - Reduces requests from 54/min to 8/min Signed-off-by: nfebe --- src/layouts/DashboardLayout.vue | 2 +- src/stores/stats.ts | 53 ++------------------------------- src/views/SettingsView.vue | 7 +---- 3 files changed, 5 insertions(+), 57 deletions(-) diff --git a/src/layouts/DashboardLayout.vue b/src/layouts/DashboardLayout.vue index f055839..ea32cf4 100644 --- a/src/layouts/DashboardLayout.vue +++ b/src/layouts/DashboardLayout.vue @@ -381,7 +381,7 @@ const handleLogout = () => { onMounted(() => { statsStore.fetchAll(); - setInterval(() => statsStore.fetchAll(), 10000); + setInterval(() => statsStore.fetchAll(), 15000); }); diff --git a/src/stores/stats.ts b/src/stores/stats.ts index 3d77473..84e96ab 100644 --- a/src/stores/stats.ts +++ b/src/stores/stats.ts @@ -1,15 +1,6 @@ import { defineStore } from "pinia"; import { ref, reactive } from "vue"; -import { - healthApi, - networksApi, - certificatesApi, - pluginsApi, - portsApi, - systemServicesApi, - containersApi, - infrastructureApi, -} from "@/services/api"; +import { healthApi } from "@/services/api"; export const useStatsStore = defineStore("stats", () => { const loading = ref(false); @@ -77,6 +68,8 @@ export const useStatsStore = defineStore("stats", () => { containers.stopped = data.containers?.stopped || 0; docker.images = data.images?.total || 0; docker.volumes = data.volumes?.total || 0; + docker.networks = data.networks?.total || 0; + docker.ports = data.ports?.total || 0; } if (statsRes.data?.system) { @@ -86,46 +79,6 @@ export const useStatsStore = defineStore("stats", () => { resources.disk = Math.round((sys.disk?.usage_percent || 0) * 10) / 10; } - const [networksRes, certsRes, pluginsRes, portsRes, servicesRes, containersRes, infraRes] = - await Promise.allSettled([ - networksApi.list(), - certificatesApi.list(), - pluginsApi.list(), - portsApi.list(), - systemServicesApi.list(), - containersApi.list(), - infrastructureApi.list(), - ]); - - if (networksRes.status === "fulfilled") { - docker.networks = networksRes.value.data.networks?.length || 0; - } - if (certsRes.status === "fulfilled") { - system.certificates = certsRes.value.data.certificates?.length || 0; - } - if (pluginsRes.status === "fulfilled") { - system.apps = pluginsRes.value.data.plugins?.length || 0; - } - if (portsRes.status === "fulfilled") { - system.ports = portsRes.value.data.ports?.length || 0; - } - if (servicesRes.status === "fulfilled") { - system.services = servicesRes.value.data.services?.length || 0; - } - if (containersRes.status === "fulfilled") { - const containersList = containersRes.value.data.containers || []; - let portCount = 0; - for (const container of containersList) { - if (Array.isArray(container.ports)) { - portCount += container.ports.length; - } - } - docker.ports = portCount; - } - if (infraRes.status === "fulfilled") { - system.infrastructure = infraRes.value.data.services?.length || 0; - } - try { const dbConnections = localStorage.getItem("db_connections"); if (dbConnections) { diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index dac88e3..de6d00c 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -552,12 +552,7 @@
Time window for counting violations (e.g., 2m, 5m) - +