main
vue 224 lines 6.28 KB
Raw
1 <template>
2 <SegmentedPage toolbar-height="60px" toolbar-height-mobile="50px" padding="16px" enable-resize>
3 <template #sidebar-header>
4 <n-button v-if="areAllTacticsSelected" :focusable="false" @click="toggleAllTactics(false)">
5 <template #icon>
6 <Icon name="carbon:checkbox" :size="16" />
7 </template>
8 Unselect all
9 </n-button>
10 <n-button v-else type="primary" :focusable="false" @click="toggleAllTactics(true)">
11 <template #icon>
12 <Icon name="carbon:checkbox-checked" :size="16" />
13 </template>
14 Select all
15 </n-button>
16 </template>
17 <template #sidebar-content>
18 <n-spin :show="loading">
19 <div class="flex flex-col gap-4">
20 <div v-for="tactic of tacticsList" :key="tactic.id" class="flex items-center gap-3">
21 <n-checkbox
22 :checked="isTacticSelected(tactic.id)"
23 @update-checked="toggleTacticSelect(tactic.id)"
24 >
25 <div class="flex items-center gap-2">
26 <span>{{ tactic.name }}</span>
27 <code class="whitespace-nowrap">{{ tactic.count }}</code>
28 </div>
29 </n-checkbox>
30 </div>
31 </div>
32 <n-empty v-if="!tacticsList.length" description="No tactics available" class="h-48 justify-center" />
33 </n-spin>
34 </template>
35 <template #main-toolbar>
36 <div class="flex items-center gap-4">
37 <n-input v-model:value="textFilter" placeholder="Search by technique name" clearable>
38 <template #prefix>
39 <Icon name="carbon:search" :size="16" />
40 </template>
41 </n-input>
42 <div v-if="hasNoCountAlerts" class="max-w-32 min-w-32">
43 <n-checkbox v-model:checked="hideNoAlertsTechniques" class="items-center!" size="large">
44 <span class="text-xs/tight">Hide techniques with no alerts</span>
45 </n-checkbox>
46 </div>
47 </div>
48 </template>
49 <template #main-content>
50 <n-spin :show="loading">
51 <div class="grid-auto-fill-250 grid gap-2">
52 <TechniqueAlertCard
53 v-for="technique of filteredTechniques"
54 :key="technique.technique_id"
55 :entity="technique"
56 class="flex"
57 />
58 </div>
59 <n-empty v-if="!filteredTechniques.length" description="No items found" class="h-48 justify-center" />
60 </n-spin>
61 </template>
62 </SegmentedPage>
63 </template>
64
65 <script setup lang="ts">
66 import type { MitreTechniquesAlertsQuery, MitreTechniquesAlertsQueryTimeRange } from "@/api/endpoints/wazuh/mitre"
67 import type { MitreTechnique } from "@/types/mitre.d"
68 import { watchDebounced } from "@vueuse/core"
69 import axios from "axios"
70 import { NButton, NCheckbox, NEmpty, NInput, NSpin, useMessage } from "naive-ui"
71 import { computed, ref, toRefs, watch } from "vue"
72 import Api from "@/api"
73 import Icon from "@/components/common/Icon.vue"
74 import SegmentedPage from "@/components/common/SegmentedPage.vue"
75 import TechniqueAlertCard from "../TechniqueAlert/TechniqueAlertCard.vue"
76
77 const props = defineProps<{
78 filters?: { type: string; value: string }[]
79 }>()
80
81 const { filters } = toRefs(props)
82 const loading = ref(false)
83 const message = useMessage()
84 const techniquesList = ref<MitreTechnique[]>([])
85 const currentPage = ref(1)
86 const hideNoAlertsTechniques = ref(false)
87 const textFilter = ref<string | null>(null)
88 let abortController: AbortController | null = null
89
90 const selectedTactics = ref<string[]>([])
91
92 const tacticsList = computed(() => {
93 const list: { name: string; id: string; count: number }[] = []
94
95 for (const technique of techniquesList.value) {
96 for (const tactic of technique.tactics) {
97 const savedTactic = list.find(o => o.id === tactic.id)
98 if (savedTactic) {
99 savedTactic.count += technique.count
100 } else {
101 list.push({
102 name: tactic.name,
103 id: tactic.id,
104 count: technique.count
105 })
106 }
107 }
108 }
109
110 return list
111 })
112
113 const filteredTechniques = computed(() => {
114 return techniquesList.value
115 .filter(a => {
116 for (const tactic of a.tactics) {
117 if (selectedTactics.value.includes(tactic.id)) {
118 return true
119 }
120 }
121
122 return false
123 })
124 .filter(a => !textFilter.value || a.technique_name.toLowerCase().includes(textFilter.value.toLowerCase()))
125 .filter(a => !hideNoAlertsTechniques.value || (hideNoAlertsTechniques.value && a.count))
126 })
127
128 const areAllTacticsSelected = computed(() => {
129 return selectedTactics.value.length === tacticsList.value.length
130 })
131
132 const hasNoCountAlerts = computed(() => !!techniquesList.value.filter(o => !o.count).length)
133
134 function isTacticSelected(id: string) {
135 return selectedTactics.value.includes(id)
136 }
137
138 function toggleTacticSelect(id: string) {
139 const index = selectedTactics.value.findIndex(o => o === id)
140
141 if (selectedTactics.value.includes(id)) {
142 selectedTactics.value.splice(index, 1)
143 } else {
144 selectedTactics.value.push(id)
145 }
146 }
147
148 function toggleAllTactics(state: boolean) {
149 if (state) {
150 selectedTactics.value = tacticsList.value.map(o => o.id)
151 } else {
152 selectedTactics.value = []
153 }
154 }
155
156 function resetList() {
157 techniquesList.value = []
158 currentPage.value = 1
159 getList()
160 }
161
162 function nextPage() {
163 currentPage.value++
164 getList()
165 }
166
167 function getList() {
168 abortController?.abort()
169 abortController = new AbortController()
170
171 loading.value = true
172
173 const query: MitreTechniquesAlertsQuery = {
174 time_range: filters.value?.find(o => o.type === "time_range")?.value as
175 | MitreTechniquesAlertsQueryTimeRange
176 | undefined,
177 size: 300,
178 page: currentPage.value,
179 rule_level: filters.value?.find(o => o.type === "rule_level")?.value,
180 rule_group: filters.value?.find(o => o.type === "rule_group")?.value,
181 mitre_field: filters.value?.find(o => o.type === "mitre_field")?.value,
182 index_pattern: filters.value?.find(o => o.type === "index_pattern")?.value
183 }
184
185 Api.wazuh.mitre
186 .getMitreTechniquesAlerts(query, abortController.signal)
187 .then(res => {
188 loading.value = false
189
190 if (res.data.success) {
191 techniquesList.value = [...techniquesList.value, ...res.data.techniques]
192 if (res.data.total_pages > currentPage.value) {
193 nextPage()
194 }
195 } else {
196 message.warning(res.data?.message || "An error occurred. Please try again later.")
197 }
198 })
199 .catch(err => {
200 if (!axios.isCancel(err)) {
201 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
202 loading.value = false
203 }
204 })
205 }
206
207 watch(
208 tacticsList,
209 () => {
210 toggleAllTactics(true)
211 },
212 { deep: true, immediate: true }
213 )
214
215 watchDebounced(filters, resetList, {
216 deep: true,
217 debounce: 300,
218 immediate: true
219 })
220 // MOCK
221 /*
222 techniquesList.value = techniques
223 */
224 </script>