diff --git a/src/components/standalone/dashboard/ThreatShieldIpCard.vue b/src/components/standalone/dashboard/ThreatShieldIpCard.vue index 52bbcb05c..b8b1f9528 100644 --- a/src/components/standalone/dashboard/ThreatShieldIpCard.vue +++ b/src/components/standalone/dashboard/ThreatShieldIpCard.vue @@ -18,12 +18,14 @@ import { faWarning } from '@fortawesome/free-solid-svg-icons' import router from '@/router' import { getStandaloneRoutePrefix } from '@/lib/router' import { useRoute } from 'vue-router' +import { useQuery } from '@tanstack/vue-query' import { DASHBOARD_REFRESH_INTERVAL, useDashboardOverview } from '@/composables/useDashboardOverview' import { useThreatShieldSettings } from '@/composables/useThreatShieldSettings' import { getStatusBadge } from '@/lib/standalone/dashboard' +import { vmQuery } from '@/lib/standalone/victoriaMetrics' const { t } = useI18n() const route = useRoute() @@ -42,20 +44,39 @@ const { error: settingsError } = useThreatShieldSettings({ refetchInterval: DASHBOARD_REFRESH_INTERVAL }) +// the blocked IPs counter is read straight from VictoriaMetrics, since banIP +// exposes it as a metric instead of a ubus counter +const { + data: serviceCounter, + isPending: isCounterPending, + isError: isCounterError, + error: counterError +} = useQuery({ + queryKey: ['dashboard', 'threat-shield-ip-counter'], + queryFn: () => vmQuery('sum(count_over_time(banip_blocked_bytes[1h]))'), + select: (res) => { + const value = res.data?.result?.[0]?.value?.[1] + return value !== undefined ? Math.trunc(Number(value)) : 0 + }, + refetchInterval: DASHBOARD_REFRESH_INTERVAL +}) + const badge = computed(() => getStatusBadge(overview.value?.services.banip)) -const serviceCounter = computed(() => overview.value?.counters.threat_shield_ip) const isLoggingDisabled = computed( () => - !tsSettings.value?.ban_logforwardlan && - !tsSettings.value?.ban_logforwardwan && - !tsSettings.value?.ban_loginput && + !tsSettings.value?.ban_logoutbound && + !tsSettings.value?.ban_loginbound && !tsSettings.value?.ban_logprerouting ) -const isPending = computed(() => isOverviewPending.value || isSettingsPending.value) -const isError = computed(() => isOverviewError.value || isSettingsError.value) -const error = computed(() => overviewError.value ?? settingsError.value) +const isPending = computed( + () => isOverviewPending.value || isSettingsPending.value || isCounterPending.value +) +const isError = computed( + () => isOverviewError.value || isSettingsError.value || isCounterError.value +) +const error = computed(() => overviewError.value ?? settingsError.value ?? counterError.value) const errorTitle = computed(() => (isError.value ? t('error.cannot_retrieve_service_status') : '')) const errorDescription = computed(() => (isError.value ? t(getAxiosErrorMessage(error.value)) : '')) diff --git a/src/components/standalone/security/threat_shield/SettingsTab.vue b/src/components/standalone/security/threat_shield/SettingsTab.vue index a7bcc1a29..4d7e4e0dc 100644 --- a/src/components/standalone/security/threat_shield/SettingsTab.vue +++ b/src/components/standalone/security/threat_shield/SettingsTab.vue @@ -26,6 +26,8 @@ import { onMounted } from 'vue' import { useUciPendingChangesStore } from '@/stores/standalone/uciPendingChanges' import NeMultiTextInput from '../../NeMultiTextInput.vue' import { MessageBag } from '@/lib/validation' +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' +import { faFloppyDisk } from '@fortawesome/free-solid-svg-icons' const { t } = useI18n() const uciChangesStore = useUciPendingChangesStore() @@ -33,9 +35,8 @@ const queryClient = useQueryClient() const isThreatShieldEnabled = ref(false) const isLogPreroutingEnabled = ref(false) -const isLogInputEnabled = ref(false) -const isLogForwardEnabled = ref(false) -const isLogForwardLanEnabled = ref(false) +const isLogInboundEnabled = ref(false) +const isLogOutboundEnabled = ref(false) const isBlockBruteForceEnabled = ref(false) const banTime = ref('') const maxFailedAccesses = ref('') @@ -78,9 +79,8 @@ type ListSettingsResponse = { data: { ban_icmplimit: number ban_logcount: string - ban_logforwardlan: boolean - ban_logforwardwan: boolean - ban_loginput: boolean + ban_logoutbound: boolean + ban_loginbound: boolean ban_loglimit: boolean ban_logprerouting: boolean ban_logterm: string[] @@ -104,9 +104,8 @@ async function fetchSettings() { const threatShieldConfig = res.data.data isThreatShieldEnabled.value = threatShieldConfig.enabled isLogPreroutingEnabled.value = threatShieldConfig.ban_logprerouting - isLogInputEnabled.value = threatShieldConfig.ban_loginput - isLogForwardEnabled.value = threatShieldConfig.ban_logforwardwan - isLogForwardLanEnabled.value = threatShieldConfig.ban_logforwardlan + isLogInboundEnabled.value = threatShieldConfig.ban_loginbound + isLogOutboundEnabled.value = threatShieldConfig.ban_logoutbound isBlockBruteForceEnabled.value = threatShieldConfig.ban_loglimit banTime.value = threatShieldConfig.ban_nftexpiry maxFailedAccesses.value = threatShieldConfig.ban_logcount.toString() @@ -221,9 +220,8 @@ async function saveSettings() { await ubusCall('ns.threatshield', 'edit-settings', { enabled: isThreatShieldEnabled.value, ban_logprerouting: isLogPreroutingEnabled.value, - ban_loginput: isLogInputEnabled.value, - ban_logforwardwan: isLogForwardEnabled.value, - ban_logforwardlan: isLogForwardLanEnabled.value, + ban_loginbound: isLogInboundEnabled.value, + ban_logoutbound: isLogOutboundEnabled.value, ban_loglimit: isBlockBruteForceEnabled.value, ban_nftexpiry: banTime.value, ban_logcount: Number(maxFailedAccesses.value), @@ -288,12 +286,7 @@ onMounted(() => {
{ :disabled="loading.editSettings" /> -
@@ -496,11 +483,7 @@ onMounted(() => { @click="saveSettings()" > {{ t('common.save') }} diff --git a/src/composables/useThreatShieldSettings.ts b/src/composables/useThreatShieldSettings.ts index 53b0700ff..7019883f9 100644 --- a/src/composables/useThreatShieldSettings.ts +++ b/src/composables/useThreatShieldSettings.ts @@ -8,10 +8,9 @@ import { useQuery } from '@tanstack/vue-query' // list-settings payload, so all are optional here. export type ThreatShieldSettings = { enabled?: boolean - ban_logforwardlan?: boolean - ban_logforwardwan?: boolean - ban_loginput?: boolean ban_logprerouting?: boolean + ban_loginbound: boolean + ban_logoutbound: boolean } // api-server wraps the ubus result under `data`, and ns.threatshield wraps its diff --git a/src/i18n/en.json b/src/i18n/en.json index 5c74de9c4..69904e9c6 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1770,7 +1770,6 @@ "logging_description": "Threats monitoring will take account and show only threats that have been logged.", "log_prerouting_chain": "Log packets blocked in pre-routing chain", "log_input_chain": "Log packets blocked in input chain", - "log_forward_chain": "Log packets blocked in forward chain", "log_forward_lan_chain": "Log packets blocked forwarded from LAN", "block_brute_force": "Block brute force attacks", "block_brute_force_description": "Brute force attack protection safeguards against repeated unauthorized login attempts by blocking or limiting access after multiple failed attempts.", diff --git a/src/i18n/it.json b/src/i18n/it.json index 8dfc30850..121655868 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -2112,7 +2112,6 @@ "logging_description": "Il monitoraggio delle minacce terrà conto e mostrerà solo le minacce che sono state salvate nel log.", "log_prerouting_chain": "Log dei pacchetti bloccati nella chain di pre-routing", "log_input_chain": "Log dei pacchetti bloccati nella chain di input", - "log_forward_chain": "Log dei pacchetti bloccati nella chain di forward", "log_forward_lan_chain": "Log dei pacchetti bloccati inoltrati dalla LAN", "ban_after_n_attempts_tooltip": "Il numero di volte che un IP deve apparire nel log prima di essere contrassegnato come sospetto e bloccato.", "block_dos_description": "Limita il traffico eccessivo relativo a protocolli specifici, bloccandolo fino a quando la situazione non torna alla normalità. Agisce su tutto il traffico in ingresso dalle interfacce WAN.", diff --git a/src/i18n/ta.json b/src/i18n/ta.json index 80ead1a95..bbb0e8a73 100644 --- a/src/i18n/ta.json +++ b/src/i18n/ta.json @@ -1725,7 +1725,6 @@ "logging_description": "அச்சுறுத்தல்கள் கண்காணிப்பு கணக்கில் எடுத்துக்கொள்ளப்படும் மற்றும் பதிவு செய்யப்பட்ட அச்சுறுத்தல்களை மட்டுமே காண்பிக்கும்.", "log_prerouting_chain": "ப்ரீ-ரூட்டிங் செயினில் பதிவு பாக்கெட்டுகள் தடுக்கப்பட்டுள்ளன", "log_input_chain": "உள்ளீட்டுச் சங்கிலியில் பதிவுப் பொட்டலங்கள் தடுக்கப்பட்டுள்ளன", - "log_forward_chain": "முன்னோக்கிச் சங்கிலியில் பதிவுப் பொட்டலங்கள் தடுக்கப்பட்டுள்ளன", "log_forward_lan_chain": "LAN இலிருந்து அனுப்பப்பட்ட பதிவு பாக்கெட்டுகள் தடுக்கப்பட்டன", "block_brute_force": "மிருகத்தனமான தாக்குதல்களைத் தடுக்கவும்", "block_brute_force_description": "ப்ரூட் ஃபோர்ச் தாக்குதல் பாதுகாப்பு பல முறை தோல்வியுற்ற முயற்சிகளுக்குப் பிறகு அணுகலைத் தடுப்பதன் மூலம் அல்லது கட்டுப்படுத்துவதன் மூலம் மறுநிகழ்வு அங்கீகரிக்கப்படாத உள்நுழைவு முயற்சிகளுக்கு எதிராகப் பாதுகாக்கிறது.", diff --git a/src/lib/standalone/victoriaMetrics.ts b/src/lib/standalone/victoriaMetrics.ts new file mode 100644 index 000000000..87893d18e --- /dev/null +++ b/src/lib/standalone/victoriaMetrics.ts @@ -0,0 +1,62 @@ +// Copyright (C) 2026 Nethesis S.r.l. +// SPDX-License-Identifier: GPL-3.0-or-later + +import axios from 'axios' +import { getStandaloneApiEndpoint } from '../config' +import { useLoginStore } from '@/stores/standalone/standaloneLogin' + +// single element of an instant-query result vector +export interface VmInstantResult { + metric: Record + // [ timestamp, value ] + value: [number, string] +} + +// single element of a range-query result matrix +export interface VmRangeResult { + metric: Record + // [ [ timestamp, value ], ... ] + values: [number, string][] +} + +// native VictoriaMetrics query response +export interface VmResponse { + status: string + data: { + resultType: string + result: T[] + } +} + +// instant query against the local VictoriaMetrics through the authenticated +// API server reverse proxy (GET /api/metrics/query) +export async function vmQuery(query: string) { + const loginStore = useLoginStore() + const res = await axios.get>( + `${getStandaloneApiEndpoint()}/metrics/query`, + { + params: { query }, + headers: { + Authorization: `Bearer ${loginStore.token}` + } + } + ) + return res.data +} + +// range query against the local VictoriaMetrics through the authenticated API +// server reverse proxy (GET /api/metrics/query_range) +// start/end are unix timestamps (seconds), step is the resolution in seconds +export async function vmQueryRange(query: string, start: number, end: number, step: number) { + const loginStore = useLoginStore() + const res = await axios.get>( + `${getStandaloneApiEndpoint()}/metrics/query_range`, + { + params: { query, start, end, step }, + headers: { + Authorization: `Bearer ${loginStore.token}` + } + } + ) + return res.data +}