Skip to content

Commit

Permalink
feat(ips): rule management (#485)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tbaile committed Jan 22, 2025
1 parent 24aa7f1 commit 855db17
Show file tree
Hide file tree
Showing 6 changed files with 473 additions and 30 deletions.
99 changes: 99 additions & 0 deletions src/components/standalone/security/ips/IpsDisableRuleDrawer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script lang="ts" setup>
import { NeButton, NeSideDrawer, NeTextInput } from '@nethesis/vue-components'
import { ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import { MessageBag } from '@/lib/validation'
import { ubusCall, ValidationError } from '@/lib/standalone/ubus'
const { visible = false } = defineProps<{
visible: boolean
}>()
const emit = defineEmits(['close', 'save'])
const { t } = useI18n()
const gid = ref('')
const sid = ref('')
const description = ref('')
const validationErrors = ref(new MessageBag())
const loading = ref(false)
const error = ref<Error>()
function closeHandler() {
if (!loading.value) {
emit('close')
}
}
function save() {
loading.value = true
error.value = undefined
validationErrors.value.clear()
ubusCall('ns.snort', 'disable-rule', {
gid: gid.value,
sid: sid.value,
description: description.value
})
.then(() => {
emit('save')
})
.catch((reason) => {
if (reason instanceof ValidationError) {
validationErrors.value = reason.errorBag
} else {
error.value = reason
}
})
.finally(() => {
loading.value = false
})
}
watchEffect(() => {
if (visible) {
gid.value = ''
sid.value = ''
description.value = ''
}
})
</script>

<template>
<NeSideDrawer :is-shown="visible" :title="t('standalone.ips.disable_rule')" @close="closeHandler">
<form class="space-y-8" @submit.prevent="save">
<NeTextInput
v-model="gid"
:disabled="loading"
:invalid-message="t(validationErrors.getFirstI18nKeyFor('gid'))"
:label="t('standalone.ips.rule_gid')"
required
type="number"
/>
<NeTextInput
v-model="sid"
:disabled="loading"
:invalid-message="t(validationErrors.getFirstI18nKeyFor('sid'))"
:label="t('standalone.ips.rule_sid')"
required
type="number"
/>
<NeTextInput
v-model="description"
:disabled="loading"
:invalid-message="t(validationErrors.getFirstI18nKeyFor('description'))"
:label="t('standalone.ips.rule_description')"
required
/>
<hr />
<div class="flex flex-wrap justify-end gap-6">
<NeButton :disabled="loading" kind="tertiary" size="lg" @click="closeHandler">
{{ t('common.cancel') }}
</NeButton>
<NeButton :disabled="loading" :loading="loading" kind="primary" size="lg" type="submit">
{{ t('standalone.ips.disable_rule') }}
</NeButton>
</div>
</form>
</NeSideDrawer>
</template>
232 changes: 232 additions & 0 deletions src/components/standalone/security/ips/IpsDisabledRules.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
<script lang="ts" setup>
import IpsEnabledBadge from '@/components/standalone/security/ips/IpsEnabledBadge.vue'
import { useI18n } from 'vue-i18n'
import { computed, onMounted, ref } from 'vue'
import { ubusCall } from '@/lib/standalone/ubus'
import {
getAxiosErrorMessage,
NeButton,
NeDropdown,
type NeDropdownItem,
NeEmptyState,
NeInlineNotification,
NePaginator,
NeSkeleton,
NeTable,
NeTableBody,
NeTableCell,
NeTableHead,
NeTableHeadCell,
NeTableRow,
NeTextInput,
useItemPagination,
useSort
} from '@nethesis/vue-components'
import type { AxiosResponse } from 'axios'
import { faCircleInfo, faShield, faXmarkCircle } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import IpsDisableRuleDrawer from '@/components/standalone/security/ips/IpsDisableRuleDrawer.vue'
import { useUciPendingChangesStore } from '@/stores/standalone/uciPendingChanges'
import IpsEnableRuleModal from '@/components/standalone/security/ips/IpsEnableRuleModal.vue'
export type Rule = {
description: string
id: string
gid: string
sid: string
}
type ListRuleResponse = AxiosResponse<{
rules: Rule[]
}>
const changes = useUciPendingChangesStore()
const { t } = useI18n()
const error = ref<Error>()
const loading = ref(true)
const rules = ref<Rule[]>([])
function listDisabledRules() {
loading.value = true
ubusCall('ns.snort', 'list-disabled-rules', {})
.then((response: ListRuleResponse) => {
rules.value = response.data.rules
})
.catch((reason) => {
error.value = reason
})
.finally(() => {
loading.value = false
})
}
onMounted(() => {
listDisabledRules()
})
const filter = ref('')
const filteredRules = computed((): Rule[] => {
return rules.value.filter((rule) => {
return rule.description.includes(filter.value) || rule.id.includes(filter.value)
})
})
const sortKey = ref<keyof Rule>('id')
const sortDescending = ref(false)
const { sortedItems } = useSort(filteredRules, sortKey, sortDescending, {})
const pageSize = ref(10)
const { currentPage, paginatedItems } = useItemPagination(sortedItems, {
itemsPerPage: pageSize
})
// FIXME: when types from library are fixed, use proper type
const onSort = (payload: any) => {
sortKey.value = payload.key
sortDescending.value = payload.descending
}
const disablingRule = ref(false)
function handleSave() {
listDisabledRules()
changes.getChanges()
disablingRule.value = false
}
function dropDownActions(rule: Rule): NeDropdownItem[] {
return [
{
id: 'delete',
label: t('common.delete'),
icon: 'trash',
iconStyle: 'fas',
danger: true,
action: () => {
ruleToEnable.value = rule
}
}
]
}
const ruleToEnable = ref<Rule>()
function handleEnabled() {
listDisabledRules()
changes.getChanges()
ruleToEnable.value = undefined
}
</script>

<template>
<div class="space-y-8">
<div class="flex flex-wrap items-start justify-between gap-4">
<p class="max-w-lg">{{ t('standalone.ips.disabled_rules_description') }}</p>
<IpsEnabledBadge />
</div>

<NeInlineNotification
v-if="error"
:description="t(getAxiosErrorMessage(error))"
:title="t('standalone.ips.error_loading_disabled_rules')"
kind="error"
/>
<NeSkeleton v-if="loading" :lines="10" />
<template v-else-if="rules.length > 0">
<div class="flex flex-col flex-wrap justify-between gap-4 md:flex-row">
<NeTextInput v-model.trim="filter" :placeholder="t('common.filter')" is-search />
<NeButton v-if="rules.length > 0" kind="secondary" size="lg" @click="disablingRule = true">
<template #prefix>
<FontAwesomeIcon :icon="faXmarkCircle" aria-hidden="true" class="h-4 w-4" />
</template>
{{ t('standalone.ips.disable_rule') }}
</NeButton>
</div>
<NeTable
:ariaLabel="t('standalone.ips.rules_table')"
:skeleton-columns="7"
:skeleton-rows="5"
:sortDescending="sortDescending"
:sortKey="sortKey"
card-breakpoint="xl"
>
<NeTableHead>
<NeTableHeadCell column-key="description" sortable @sort="onSort">
{{ t('standalone.ips.rule_description') }}
</NeTableHeadCell>
<NeTableHeadCell column-key="id" sortable @sort="onSort">
{{ t('standalone.ips.rule_id') }}
</NeTableHeadCell>
<NeTableHeadCell>
<!-- no header for actions -->
</NeTableHeadCell>
</NeTableHead>
<NeTableBody>
<NeTableRow v-if="filteredRules.length < 1">
<NeTableCell colspan="4">
<NeEmptyState
:description="t('common.try_changing_search_filters')"
:icon="faCircleInfo"
:title="t('standalone.ips.no_rules_found')"
class="bg-white dark:bg-gray-950"
>
<NeButton kind="tertiary" @click="filter = ''">
{{ t('common.clear_filters') }}
</NeButton>
</NeEmptyState>
</NeTableCell>
</NeTableRow>
<NeTableRow v-for="item in paginatedItems" v-else :key="`${item.id}`">
<NeTableCell :data-label="t('standalone.ips.rule_description')">
{{ item.description }}
</NeTableCell>
<NeTableCell :data-label="t('standalone.ips.rule_id')">
{{ item.id }}
</NeTableCell>
<NeTableCell :data-label="t('common.actions')">
<div class="flex justify-end">
<NeDropdown :align-to-right="true" :items="dropDownActions(item)" />
</div>
</NeTableCell>
</NeTableRow>
</NeTableBody>
<template #paginator>
<NePaginator
:current-page="currentPage"
:nav-pagination-label="t('ne_table.pagination')"
:next-label="t('ne_table.go_to_next_page')"
:page-size="pageSize"
:page-size-label="t('ne_table.show')"
:previous-label="t('ne_table.go_to_previous_page')"
:range-of-total-label="t('ne_table.of')"
:total-rows="rules.length"
@selectPageSize="(size: number) => { pageSize = size }"
@select-page="(page: number) => { currentPage = page }"
/>
</template>
</NeTable>
</template>
<NeEmptyState
v-else
:description="t('standalone.ips.no_disabled_rules_description')"
:icon="faShield"
:title="t('standalone.ips.no_disabled_rules')"
>
<NeButton kind="primary" size="lg" @click="disablingRule = true">
<template #prefix>
<FontAwesomeIcon :icon="faXmarkCircle" aria-hidden="true" class="h-4 w-4" />
</template>
{{ t('standalone.ips.disable_rule') }}
</NeButton>
</NeEmptyState>
<IpsDisableRuleDrawer
@save="handleSave"
:visible="disablingRule"
@close="disablingRule = false"
/>
<IpsEnableRuleModal
:rule="ruleToEnable"
@enabled="handleEnabled"
@close="ruleToEnable = undefined"
/>
</div>
</template>
Loading

0 comments on commit 855db17

Please sign in to comment.