main
vue 359 lines 9.38 KB
Raw
1 <template>
2 <div class="alerts-list">
3 <div class="header flex items-center justify-end gap-2">
4 <div class="info flex grow gap-5">
5 <n-popover overlap placement="bottom-start">
6 <template #trigger>
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="cursor-help!">
9 <template #icon>
10 <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
14 </template>
15 <div class="flex flex-col gap-2">
16 <div class="box">
17 Total Summaries:
18 <code>{{ totalAlertsSummary }}</code>
19 </div>
20 <div class="box">
21 Total Alerts:
22 <code>{{ totalAlerts }}</code>
23 </div>
24 </div>
25 </n-popover>
26 </div>
27 <div class="actions flex items-center gap-2">
28 <n-button v-if="!isFilterPreselected" size="small" @click="showStatsDrawer = true">
29 <template #icon>
30 <Icon :name="StatsIcon" :size="14" />
31 </template>
32 Stats
33 </n-button>
34 <n-button size="small" @click="showFiltersDrawer = true">
35 <template #icon>
36 <Icon :name="FilterIcon" :size="15" />
37 </template>
38 Filters
39 </n-button>
40 <ThreatIntelButton size="small" type="primary" />
41 </div>
42 </div>
43 <n-spin :show="loading">
44 <template #description>Alerts are being fetched, this may take up to 1 minute.</template>
45
46 <div class="my-3 flex min-h-52 flex-col gap-2">
47 <template v-if="alertsSummaryList.length">
48 <AlertsSummaryItem
49 v-for="alertsSummary of alertsSummaryList"
50 :key="alertsSummary.index_name"
51 :alerts-summary
52 class="item-appear item-appear-bottom item-appear-005"
53 />
54 </template>
55 <template v-else>
56 <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" />
57 </template>
58 </div>
59 </n-spin>
60
61 <n-drawer
62 v-model:show="showStatsDrawer"
63 :width="700"
64 style="max-width: 90vw"
65 :trap-focus="false"
66 display-directive="show"
67 >
68 <n-drawer-content title="Alerts stats" closable body-content-style="padding:0" :native-scrollbar="false">
69 <AlertsStats :filters @mounted="alertsStatsCTX = $event" />
70 </n-drawer-content>
71 </n-drawer>
72
73 <n-drawer
74 v-model:show="showFiltersDrawer"
75 display-directive="show"
76 :trap-focus="false"
77 style="max-width: 90vw; width: 500px"
78 :show-mask="loadingFilters ? 'transparent' : undefined"
79 :class="{ 'opacity-0': loadingFilters }"
80 >
81 <n-drawer-content title="Alerts filters" closable :native-scrollbar="false">
82 <AlertsFilters :filters @search="startSearch(true)">
83 <div v-if="!isFilterPreselected" class="mb-6">
84 <n-select
85 v-model:value="filterType"
86 :options="[
87 {
88 label: 'Filter by Agent Hostname',
89 value: 'agentHostname'
90 },
91 {
92 label: 'Filter by Index name',
93 value: 'indexName'
94 }
95 ]"
96 placeholder="Filter by Agent or Index"
97 clearable
98 @update:value="
99 () => {
100 filters.agentHostname = undefined
101 filters.indexName = undefined
102 }
103 "
104 />
105 <n-select
106 v-if="filterType === 'agentHostname'"
107 v-model:value="filters.agentHostname"
108 :options="agentHostnameOptions"
109 placeholder="Agents list"
110 clearable
111 filterable
112 :loading="loadingAgents"
113 class="mt-2"
114 />
115 <n-select
116 v-if="filterType === 'indexName'"
117 v-model:value="filters.indexName"
118 :options="indexNameOptions"
119 clearable
120 filterable
121 placeholder="Indices list"
122 :loading="loadingIndex"
123 class="mt-2"
124 />
125 </div>
126 </AlertsFilters>
127 </n-drawer-content>
128 </n-drawer>
129 </div>
130 </template>
131
132 <script setup lang="ts">
133 // MOCK
134 // import { alerts_summary } from "./mock"
135 // import type { AlertsSummary } from "@/types/alerts.d"
136
137 import type { AlertsStatsCTX } from "./AlertsStats.vue"
138 import type { AlertsSummaryExt } from "./AlertsSummary.vue"
139 import type { AlertsSummaryQuery } from "@/api/endpoints/alerts"
140 import type { Agent } from "@/types/agents.d"
141 import type { IndexStats } from "@/types/indices.d"
142 import axios from "axios"
143 import { NButton, NDrawer, NDrawerContent, NEmpty, NPopover, NSelect, NSpin, useMessage } from "naive-ui"
144 import { computed, defineAsyncComponent, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, toRefs } from "vue"
145 import Api from "@/api"
146 import Icon from "@/components/common/Icon.vue"
147 import AlertsFilters from "./AlertsFilters.vue"
148 import AlertsStats from "./AlertsStats.vue"
149 import AlertsSummaryItem from "./AlertsSummary.vue"
150
151 const props = defineProps<{ agentHostname?: string; indexName?: string }>()
152
153 const ThreatIntelButton = defineAsyncComponent(() => import("@/components/threatIntel/ThreatIntelButton.vue"))
154
155 const { agentHostname, indexName } = toRefs(props)
156
157 const message = useMessage()
158 const loadingIndex = ref(false)
159 const loadingAgents = ref(false)
160 const loading = ref(false)
161 const indices = ref<IndexStats[]>([])
162 const agents = ref<Agent[]>([])
163 const alertsSummaryList = ref<AlertsSummaryExt[]>([])
164 const loadingFilters = ref(true)
165 const showFiltersDrawer = ref(true)
166 const showStatsDrawer = ref(false)
167 let abortController: AbortController | null = null
168
169 const InfoIcon = "carbon:information"
170 const FilterIcon = "carbon:filter-edit"
171 const StatsIcon = "carbon:chart-column"
172
173 const alertsStatsCTX = ref<AlertsStatsCTX | null>(null)
174
175 const totalAlertsSummary = computed<number>(() => {
176 return alertsSummaryList.value.length || 0
177 })
178 const totalAlerts = computed<number>(() => {
179 return alertsSummaryList.value.reduce((acc: number, val: AlertsSummaryExt) => {
180 return acc + val.alerts.length
181 }, 0)
182 })
183
184 const filters = ref<AlertsSummaryQuery>({})
185
186 const filterType = ref<string | null>(null)
187
188 const isFilterPreselected = computed(() => {
189 return !!agentHostname?.value || !!indexName?.value
190 })
191
192 const agentHostnameOptions = computed(() => {
193 if (agentHostname?.value) {
194 return [{ value: agentHostname.value, label: agentHostname.value }]
195 }
196 return (agents.value || []).map(o => ({ value: o.hostname, label: o.hostname }))
197 })
198
199 const indexNameOptions = computed(() => {
200 if (indexName?.value) {
201 return [{ value: indexName.value, label: indexName.value }]
202 }
203 return (indices.value || []).map(o => ({ value: o.index, label: o.index }))
204 })
205
206 function addIndexInfo() {
207 if (indices.value?.length && alertsSummaryList.value.length) {
208 for (const alert of alertsSummaryList.value) {
209 const index = indices.value.find(o => o.index === alert.index_name)
210 alert.indexStats = index
211 }
212 }
213 }
214
215 function getData() {
216 loading.value = true
217
218 abortController = new AbortController()
219
220 Api.alerts
221 .getAll(filters.value, abortController.signal)
222 .then(res => {
223 alertsSummaryList.value = res.data?.alerts_summary || []
224
225 if (res.data.success) {
226 nextTick(() => {
227 addIndexInfo()
228 })
229 } else {
230 message.warning(res.data?.message || "An error occurred. Please try again later.")
231 }
232 })
233 .catch(err => {
234 if (!axios.isCancel(err)) {
235 alertsSummaryList.value = []
236
237 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
238 }
239 })
240 .finally(() => {
241 loading.value = false
242 })
243 }
244
245 function getIndices() {
246 loadingIndex.value = true
247
248 Api.wazuh.indices
249 .getIndices()
250 .then(res => {
251 if (res.data.success) {
252 indices.value = res.data.indices_stats || []
253
254 nextTick(() => {
255 addIndexInfo()
256 })
257 } else {
258 message.error(res.data?.message || "An error occurred. Please try again later.")
259 }
260 })
261 .catch(err => {
262 if (err.response?.status === 401) {
263 message.error(
264 err.response?.data?.message ||
265 "Wazuh-Indexer returned Unauthorized. Please check your connector credentials."
266 )
267 } else if (err.response?.status === 404) {
268 message.error(err.response?.data?.message || "No indices were found.")
269 } else {
270 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
271 }
272 })
273 .finally(() => {
274 loadingIndex.value = false
275 })
276 }
277
278 function getAgents() {
279 loadingAgents.value = true
280
281 Api.agents
282 .getAgents()
283 .then(res => {
284 if (res.data.success) {
285 agents.value = res.data.agents || []
286 } else {
287 message.error(res.data?.message || "An error occurred. Please try again later.")
288 }
289 })
290 .catch(err => {
291 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
292 })
293 .finally(() => {
294 loadingAgents.value = false
295 })
296 }
297
298 function getStatsFiltersString() {
299 return [filters.value.alertField, filters.value.alertValue, filters.value.maxAlerts, filters.value.timerange].join(
300 ","
301 )
302 }
303
304 let lastStatsFilters = ""
305
306 function startSearch(closeDrawer?: boolean) {
307 cancelSearch()
308
309 setTimeout(() => {
310 getData()
311
312 const statsFiltersString = getStatsFiltersString()
313 if (lastStatsFilters !== statsFiltersString) {
314 alertsStatsCTX.value?.startSearch()
315 lastStatsFilters = statsFiltersString
316 }
317 }, 200)
318
319 if (closeDrawer) {
320 showFiltersDrawer.value = false
321 }
322 }
323
324 function cancelSearch() {
325 abortController?.abort()
326 }
327
328 onBeforeMount(() => {
329 if (agentHostname?.value) {
330 filters.value.agentHostname = agentHostname.value
331 }
332 if (indexName?.value) {
333 filters.value.indexName = indexName.value
334 }
335
336 nextTick(() => {
337 if (!isFilterPreselected.value) {
338 getAgents()
339 getIndices()
340 }
341
342 // MOCK
343 // alertsSummaryList.value = alerts_summary as AlertsSummary[]
344 startSearch()
345 })
346 })
347
348 onMounted(() => {
349 showFiltersDrawer.value = false
350
351 setTimeout(() => {
352 loadingFilters.value = false
353 }, 1000)
354 })
355
356 onBeforeUnmount(() => {
357 cancelSearch()
358 })
359 </script>