main
vue 186 lines 5.24 KB
Raw
1 <template>
2 <div class="flex flex-col gap-4">
3 <n-alert type="info">
4 Vulnerability Overview provides real-time vulnerability data from Wazuh Indexer with EPSS scoring and
5 detailed package information.
6 </n-alert>
7
8 <VulnerabilityStats :filters class="my-8" @update:severity="selectSeverity" @update:package="selectPackage" />
9
10 <div ref="header" class="flex items-center justify-end gap-2">
11 <n-pagination
12 v-model:page="currentPage"
13 v-model:page-size="pageSize"
14 :page-slot
15 :page-sizes
16 :item-count="totalCount"
17 :show-size-picker
18 />
19 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
20 <n-button size="small" secondary @click="showFiltersView = !showFiltersView">
21 <template #icon>
22 <Icon :name="FilterIcon" />
23 </template>
24 </n-button>
25 </n-badge>
26 </div>
27
28 <CollapseKeepAlive :show="showFiltersView" embedded arrow="top-right">
29 <ListFilters class="p-3" @submit="applyFilters" @mounted="filtersCTX = $event" />
30 </CollapseKeepAlive>
31
32 <!-- Vulnerability List -->
33 <n-spin :show="loading">
34 <div class="my-3">
35 <div v-if="list.length" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
36 <VulnerabilityCard v-for="item of list" :key="JSON.stringify(item)" :vulnerability="item" />
37 </div>
38 <template v-else>
39 <n-empty v-if="!loading" description="No vulnerabilities found" class="h-48 justify-center" />
40 </template>
41 </div>
42 </n-spin>
43
44 <div v-if="list.length >= 9" class="flex justify-end">
45 <n-pagination
46 v-model:page="currentPage"
47 v-model:page-size="pageSize"
48 :page-slot
49 :page-sizes
50 :item-count="totalCount"
51 :show-size-picker
52 />
53 </div>
54 </div>
55 </template>
56
57 <script setup lang="ts">
58 import type { VulnerabilitiesListFilter } from "./types"
59 import type {
60 VulnerabilitySearchItem,
61 VulnerabilitySearchQuery,
62 VulnerabilitySeverity
63 } from "@/types/vulnerabilities.d"
64 import { useResizeObserver, useStorage, watchDebounced } from "@vueuse/core"
65 import axios from "axios"
66 import { NAlert, NBadge, NButton, NEmpty, NPagination, NSpin, useMessage } from "naive-ui"
67 import { computed, ref } from "vue"
68 import Api from "@/api"
69 import CollapseKeepAlive from "@/components/common/CollapseKeepAlive.vue"
70 import Icon from "@/components/common/Icon.vue"
71 import ListFilters from "./ListFilters.vue"
72 import VulnerabilityCard from "./VulnerabilityCard.vue"
73 import VulnerabilityStats from "./VulnerabilityStats.vue"
74
75 const loading = ref(false)
76 const message = useMessage()
77 const list = ref<VulnerabilitySearchItem[]>([])
78 const pageSizes = [10, 25, 50, 100]
79 const pageSize = ref(pageSizes[1])
80 const pageSlot = ref(8)
81 const showSizePicker = ref(true)
82 const header = ref()
83 const showFiltersView = useStorage<boolean>("agents-vulnerability-list-filters-view-state", false, localStorage)
84
85 const filtersCTX = ref<{ setFilter: (payload: VulnerabilitiesListFilter[]) => void } | null>(null)
86 const filters = ref<VulnerabilitiesListFilter[]>([])
87
88 const filtered = computed<boolean>(() => {
89 return !!filters.value.length
90 })
91
92 const totalCount = ref(0)
93 const currentPage = ref(1)
94
95 const FilterIcon = "carbon:filter-edit"
96
97 let abortController: AbortController | null = null
98
99 function getList() {
100 abortController?.abort()
101 abortController = new AbortController()
102
103 loading.value = true
104
105 const query: VulnerabilitySearchQuery = {
106 page: currentPage.value,
107 page_size: pageSize.value,
108 customer_code: filters.value.find(o => o.type === "customer_code")?.value || undefined,
109 severity: (filters.value.find(o => o.type === "severity")?.value as VulnerabilitySeverity) || undefined,
110 cve_id: filters.value.find(o => o.type === "cve_id")?.value || undefined,
111 agent_name: filters.value.find(o => o.type === "agent_name")?.value || undefined,
112 package_name: filters.value.find(o => o.type === "package_name")?.value || undefined,
113 include_epss: true
114 }
115
116 Api.vulnerabilities
117 .searchVulnerabilities(query, abortController.signal)
118 .then(res => {
119 loading.value = false
120
121 if (res.data.success) {
122 list.value = res.data?.vulnerabilities || []
123 totalCount.value = res.data?.total_count || 0
124 currentPage.value = res.data?.page || 1
125 } else {
126 message.warning(res.data?.message || "An error occurred. Please try again later.")
127 }
128 })
129 .catch(err => {
130 if (!axios.isCancel(err)) {
131 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
132 loading.value = false
133 }
134 })
135 }
136
137 function selectSeverity(value: VulnerabilitySeverity) {
138 showFiltersView.value = true
139 filtersCTX.value?.setFilter([{ type: "severity", value }])
140 }
141
142 function selectPackage(value: string) {
143 showFiltersView.value = true
144 filtersCTX.value?.setFilter([{ type: "package_name", value }])
145 }
146
147 function applyFilters(newFilters: VulnerabilitiesListFilter[]) {
148 filters.value = newFilters
149 }
150
151 watchDebounced(
152 currentPage,
153 () => {
154 getList()
155 },
156 { debounce: 300 }
157 )
158
159 watchDebounced(
160 pageSize,
161 () => {
162 currentPage.value = 1
163 getList()
164 },
165 { debounce: 300 }
166 )
167
168 watchDebounced(
169 filters,
170 () => {
171 currentPage.value = 1
172 getList()
173 },
174 { deep: true, debounce: 300, immediate: true }
175 )
176
177 useResizeObserver(header, entries => {
178 const entry = entries[0]
179 if (!entry) return
180
181 const { width } = entry.contentRect
182
183 pageSlot.value = width < 700 ? 5 : 8
184 showSizePicker.value = width > 550
185 })
186 </script>