main
vue 338 lines 9.31 KB
Raw
1 <template>
2 <div class="page page-wrapped page-mobile-full page-without-footer flex flex-col">
3 <SegmentedPage
4 main-content-class="p-0! overflow-hidden grow flex flex-col h-full"
5 :use-main-scroll="false"
6 padding="18px"
7 enable-resize
8 toolbar-height="54px"
9 sidebar-content-class="p-0!"
10 >
11 <template #sidebar-header>
12 <div class="flex w-full items-center justify-between gap-3">
13 <n-input
14 v-model:value="filters.search"
15 size="small"
16 class="max-w-full grow"
17 clearable
18 placeholder="Search..."
19 >
20 <template #prefix>
21 <Icon :name="SearchIcon" :size="16" />
22 </template>
23 </n-input>
24
25 <n-tooltip>
26 <template #trigger>
27 <n-button secondary :loading="loadingManager" size="small" @click="reloadManager()">
28 <template #icon>
29 <Icon :name="RefreshIcon" />
30 </template>
31 </n-button>
32 </template>
33 <div>Restart Wazuh</div>
34 </n-tooltip>
35 </div>
36 </template>
37 <template #sidebar-content>
38 <n-spin :show="loadingList">
39 <template v-if="fileList.length">
40 <div class="divide-border flex flex-col divide-y">
41 <div
42 v-for="item of fileList"
43 :key="item.filename"
44 class="hover:text-warning cursor-pointer px-4.5 py-2.5 font-mono text-sm break-all"
45 :class="{ 'bg-warning/10': item.filename === currentFile?.filename }"
46 @click.stop="loadFile(item.filename)"
47 >
48 {{ item.filename }}
49 </div>
50 </div>
51 </template>
52 <template v-else>
53 <n-empty v-if="!loadingList" description="No items found" class="h-48 justify-center" />
54 </template>
55 </n-spin>
56 </template>
57 <template v-if="pagination.total" #sidebar-footer>
58 <div class="flex w-full items-center justify-center">
59 <n-pagination
60 v-model:page="pagination.current"
61 :page-size="pagination.size"
62 :page-slot="5"
63 :item-count="pagination.total"
64 simple
65 />
66 </div>
67 </template>
68 <template v-if="currentFile" #main-toolbar>
69 <div class="@container flex items-center justify-between">
70 <div class="flex items-center gap-2 md:gap-3">
71 <n-button
72 v-if="xmlEditorCTX"
73 size="small"
74 :disabled="!xmlEditorCTX.canUndo()"
75 @click="xmlEditorCTX.undo"
76 >
77 <div class="flex items-center gap-2">
78 <Icon :name="UndoIcon" />
79 <span class="hidden @sm:flex">Undo</span>
80 </div>
81 </n-button>
82 <n-button
83 v-if="xmlEditorCTX"
84 size="small"
85 :disabled="!xmlEditorCTX.canRedo()"
86 @click="xmlEditorCTX.redo"
87 >
88 <div class="flex items-center gap-2">
89 <span class="hidden @sm:flex">Redo</span>
90 <Icon :name="RedoIcon" />
91 </div>
92 </n-button>
93 </div>
94 <div class="flex items-center gap-2 md:gap-3">
95 <n-popover v-if="xmlErrors.length && xmlEditorCTX" class="p-1!">
96 <template #trigger>
97 <div class="flex items-center justify-end gap-2">
98 <Icon
99 name="carbon:warning-alt"
100 :size="20"
101 class="text-warning animate-fade cursor-help"
102 />
103 <span class="text-warning hidden font-mono text-xs @lg:flex">Errors detected</span>
104 </div>
105 </template>
106
107 <n-scrollbar class="max-h-100">
108 <div class="flex max-w-80 flex-col gap-1">
109 <div
110 v-for="item of xmlErrors"
111 :key="JSON.stringify(item)"
112 class="bg-secondary hover:bg-body flex cursor-pointer flex-col gap-0.5 rounded-sm p-1 font-mono"
113 @click="xmlEditorCTX.scrollToLine(item.line)"
114 >
115 <div class="text-secondary text-[8px]">line: {{ item.line }}</div>
116 <div class="text-xs">{{ item.message }}</div>
117 </div>
118 </div>
119 </n-scrollbar>
120 </n-popover>
121
122 <n-button
123 :loading="uploadingFile"
124 size="small"
125 type="primary"
126 :disabled="!isDirty"
127 @click="uploadFileFile()"
128 >
129 <div class="flex items-center gap-2">
130 <Icon :name="UploadIcon" />
131 <span class="hidden @xs:flex">Upload</span>
132 </div>
133 </n-button>
134 </div>
135 </div>
136 </template>
137 <template #main-content>
138 <div v-if="currentFile" class="px-4.5 py-2.5 font-mono text-sm break-all">
139 current file: {{ currentFile?.filename }}
140 </div>
141 <n-spin
142 :show="loadingFile || uploadingFile"
143 class="flex h-full w-full overflow-hidden"
144 content-class="flex h-full grow flex-col justify-center overflow-hidden"
145 >
146 <template v-if="currentFile">
147 <XMLEditor
148 v-model="currentFile.content"
149 class="scrollbar-styled text-sm"
150 @errors="xmlErrors = $event"
151 @mounted="xmlEditorCTX = $event"
152 />
153 </template>
154 <template v-else>
155 <n-empty v-if="!loadingFile" description="Select a customer" class="h-48 justify-center" />
156 </template>
157 </n-spin>
158 </template>
159 </SegmentedPage>
160 </div>
161 </template>
162
163 <script setup lang="ts">
164 import type { XMLEditorCtx, XMLError } from "@/components/common/XMLEditor.vue"
165 import type { WazuhFileDetails, WazuhFileItem } from "@/types/wazuh/rules.d"
166 import { watchDebounced } from "@vueuse/core"
167 import axios from "axios"
168 import _clone from "lodash/cloneDeep"
169 import { NButton, NEmpty, NInput, NPagination, NPopover, NScrollbar, NSpin, NTooltip, useMessage } from "naive-ui"
170 import { computed, ref, watch } from "vue"
171 import Api from "@/api"
172 import Icon from "@/components/common/Icon.vue"
173 import SegmentedPage from "@/components/common/SegmentedPage.vue"
174 import XMLEditor from "@/components/common/XMLEditor.vue"
175
176 const message = useMessage()
177 const loadingManager = ref(false)
178 const loadingList = ref(false)
179 const loadingFile = ref(false)
180 const uploadingFile = ref(false)
181 const fileList = ref<WazuhFileItem[]>([])
182 const currentFile = ref<WazuhFileDetails | null>(null)
183 const backupFile = ref<WazuhFileDetails | null>(null)
184 const xmlEditorCTX = ref<XMLEditorCtx | null>(null)
185 const UndoIcon = "carbon:undo"
186 const RedoIcon = "carbon:redo"
187 const SearchIcon = "ion:search-outline"
188 const RefreshIcon = "carbon:renew"
189 const UploadIcon = "carbon:cloud-upload"
190
191 const filters = ref({
192 search: null
193 })
194 const pagination = ref({
195 current: 1,
196 size: 30,
197 total: 0
198 })
199
200 const isDirty = computed(() => currentFile.value?.content !== backupFile.value?.content)
201 const xmlErrors = ref<XMLError[]>([])
202
203 let abortController: AbortController | null = null
204
205 function loadFile(filename: string) {
206 if (filename !== currentFile.value?.filename) {
207 getFile(filename)
208 }
209 }
210
211 function reloadManager() {
212 abortController?.abort()
213
214 loadingManager.value = true
215
216 Api.wazuh.rules
217 .restartManager()
218 .then(res => {
219 if (res.data.success) {
220 message.success(res.data?.message || "Wazuh Manager cluster restarted successfully")
221 getList()
222 } else {
223 message.error(res.data?.message || "An error occurred. Please try again later.")
224 }
225 })
226 .catch(err => {
227 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
228 })
229 .finally(() => {
230 loadingManager.value = false
231 })
232 }
233
234 function getList() {
235 abortController?.abort()
236 abortController = new AbortController()
237
238 loadingList.value = true
239
240 Api.wazuh.rules
241 .getRulesFileList(
242 {
243 search: filters.value.search || undefined,
244 pretty: false,
245 wait_for_complete: false,
246 distinct: false,
247 offset: (pagination.value.current - 1) * pagination.value.size,
248 limit: pagination.value.size
249 },
250 abortController.signal
251 )
252 .then(res => {
253 if (res.data.success) {
254 fileList.value = res.data.results || []
255 pagination.value.total = res.data.total_items
256 } else {
257 pagination.value.total = 0
258 message.error(res.data?.message || "An error occurred. Please try again later.")
259 }
260 loadingList.value = false
261 })
262 .catch(err => {
263 if (!axios.isCancel(err)) {
264 fileList.value = []
265
266 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
267 loadingList.value = false
268 }
269 })
270 }
271
272 function getFile(filename: string) {
273 loadingFile.value = true
274
275 Api.wazuh.rules
276 .getRulesFile(filename)
277 .then(res => {
278 if (res.data.success) {
279 currentFile.value = _clone(res.data)
280 backupFile.value = _clone(res.data)
281 } else {
282 message.error(res.data?.message || "An error occurred. Please try again later.")
283 }
284 })
285 .catch(err => {
286 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
287 })
288 .finally(() => {
289 loadingFile.value = false
290 })
291 }
292
293 function uploadFileFile() {
294 if (currentFile.value) {
295 uploadingFile.value = true
296
297 Api.wazuh.rules
298 .updateRulesFile(
299 currentFile.value.filename,
300 new File([currentFile.value.content], currentFile.value.filename, {
301 type: "text/xml;charset=utf-8"
302 })
303 )
304
305 .then(res => {
306 if (res.data.success) {
307 currentFile.value = _clone(currentFile.value)
308 backupFile.value = _clone(currentFile.value)
309 message.success("Detection rules uploaded Successfully")
310 } else {
311 message.error("An error occurred. Please try again later.")
312 }
313 })
314 .catch(err => {
315 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
316 })
317 .finally(() => {
318 uploadingFile.value = false
319 })
320 }
321 }
322
323 watch(
324 [filters],
325 () => {
326 pagination.value.current = 1
327 },
328 { deep: true }
329 )
330
331 watchDebounced(
332 [filters, () => pagination.value.current],
333 () => {
334 getList()
335 },
336 { debounce: 250, immediate: true, deep: true }
337 )
338 </script>