Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions src/components/standalone/dashboard/ThreatShieldIpCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)) : ''))
Expand Down
49 changes: 16 additions & 33 deletions src/components/standalone/security/threat_shield/SettingsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@ 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()
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('')
Expand Down Expand Up @@ -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[]
Expand All @@ -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()
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -288,12 +286,7 @@ onMounted(() => {
<div class="mb-8 space-y-6">
<!-- monitoring not working warning -->
<NeInlineNotification
v-if="
!isLogPreroutingEnabled &&
!isLogInputEnabled &&
!isLogForwardEnabled &&
!isLogForwardLanEnabled
"
v-if="!isLogPreroutingEnabled && !isLogInboundEnabled && !isLogOutboundEnabled"
kind="warning"
:title="t('standalone.threat_shield.threats_monitoring_disabled')"
:description="t('standalone.threat_shield.threats_monitoring_disabled_message')"
Expand All @@ -306,21 +299,15 @@ onMounted(() => {
:disabled="loading.editSettings"
/>
<NeToggle
v-model="isLogInputEnabled"
v-model="isLogInboundEnabled"
:top-label="t('standalone.threat_shield.log_input_chain')"
:label="isLogInputEnabled ? t('common.enabled') : t('common.disabled')"
:label="isLogInboundEnabled ? t('common.enabled') : t('common.disabled')"
:disabled="loading.editSettings"
/>
<NeToggle
v-model="isLogForwardEnabled"
:top-label="t('standalone.threat_shield.log_forward_chain')"
:label="isLogForwardEnabled ? t('common.enabled') : t('common.disabled')"
:disabled="loading.editSettings"
/>
<NeToggle
v-model="isLogForwardLanEnabled"
v-model="isLogOutboundEnabled"
:top-label="t('standalone.threat_shield.log_forward_lan_chain')"
:label="isLogForwardLanEnabled ? t('common.enabled') : t('common.disabled')"
:label="isLogOutboundEnabled ? t('common.enabled') : t('common.disabled')"
:disabled="loading.editSettings"
/>
</div>
Expand Down Expand Up @@ -496,11 +483,7 @@ onMounted(() => {
@click="saveSettings()"
>
<template #prefix>
<font-awesome-icon
:icon="['fas', 'floppy-disk']"
class="h-4 w-4"
aria-hidden="true"
/>
<FontAwesomeIcon :icon="faFloppyDisk" class="h-4 w-4" aria-hidden="true" />
</template>
{{ t('common.save') }}
</NeButton>
Expand Down
5 changes: 2 additions & 3 deletions src/composables/useThreatShieldSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 0 additions & 1 deletion src/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 0 additions & 1 deletion src/i18n/ta.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "ப்ரூட் ஃபோர்ச் தாக்குதல் பாதுகாப்பு பல முறை தோல்வியுற்ற முயற்சிகளுக்குப் பிறகு அணுகலைத் தடுப்பதன் மூலம் அல்லது கட்டுப்படுத்துவதன் மூலம் மறுநிகழ்வு அங்கீகரிக்கப்படாத உள்நுழைவு முயற்சிகளுக்கு எதிராகப் பாதுகாக்கிறது.",
Expand Down
62 changes: 62 additions & 0 deletions src/lib/standalone/victoriaMetrics.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>
// [ timestamp, value ]
value: [number, string]
}

// single element of a range-query result matrix
export interface VmRangeResult {
metric: Record<string, string>
// [ [ timestamp, value ], ... ]
values: [number, string][]
}

// native VictoriaMetrics query response
export interface VmResponse<T> {
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<VmResponse<VmInstantResult>>(
`${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<VmResponse<VmRangeResult>>(
`${getStandaloneApiEndpoint()}/metrics/query_range`,
{
params: { query, start, end, step },
headers: {
Authorization: `Bearer ${loginStore.token}`
}
}
)
return res.data
}
Loading