main
vue 670 lines 17.6 KB
Raw
1 <template>
2 <n-spin :show="loading" class="creation-report-form">
3 <n-form ref="formRef" :model="form" :rules>
4 <div class="flex flex-col gap-8">
5 <div class="flex flex-col gap-2">
6 <n-form-item v-if="showIndexNameField" label="Index name" path="index_name">
7 <n-select
8 v-model:value="form.index_name"
9 :options="indexNamesOptions"
10 placeholder="Select..."
11 clearable
12 filterable
13 to="body"
14 :disabled="disableIndexNameField"
15 :loading="loadingIndexNames"
16 />
17 </n-form-item>
18
19 <div v-if="showSourceField">
20 <n-form-item path="source" :show-require-mark="false" class="source-field">
21 <template #label>
22 <div class="flex items-end justify-between gap-2">
23 <span>
24 Source
25 <span class="n-form-item-label__asterisk">*</span>
26 </span>
27
28 <span v-if="isSocfortressRecommendsAvailable">
29 <n-button
30 :loading="loadingSocfortressRecommendsWazuh"
31 size="tiny"
32 ghost
33 type="primary"
34 @click="getSocfortressRecommendsWazuh()"
35 >
36 SOCFortress Recommends
37 </n-button>
38 </span>
39 </div>
40 </template>
41 <n-input
42 v-if="arbitrarySourceField"
43 v-model:value.trim="form.source"
44 placeholder="Please insert Source"
45 clearable
46 />
47 <n-input
48 v-else
49 v-model:value.trim="form.source"
50 placeholder="Please insert Source"
51 clearable
52 :disabled="disableSourceField"
53 :loading="loadingSource"
54 @update:value="resetIndexAvailable()"
55 />
56 </n-form-item>
57 </div>
58
59 <n-alert v-if="isSourceNotAllowed" title="Source already exists" type="warning" class="mb-5">
60 A configuration for
61 <strong>"{{ form.source }}"</strong>
62 already exists. Please select a different
63 <strong>Index name</strong>
64 to proceed.
65 </n-alert>
66
67 <n-alert
68 v-if="arbitrarySourceField"
69 title="Proceed with caution, incorrect settings can disrupt system operation"
70 type="warning"
71 class="mb-5"
72 ></n-alert>
73
74 <n-form-item label="Field names" path="field_names">
75 <n-select
76 v-model:value="form.field_names"
77 :options="availableMappingsOptions"
78 placeholder="Select..."
79 clearable
80 filterable
81 multiple
82 :tag="arbitrarySourceField"
83 to="body"
84 :disabled="!isFieldEnabled"
85 :loading="loadingAvailableMappings"
86 >
87 <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
88 </n-select>
89 </n-form-item>
90 <n-form-item label="IOC Field names" path="ioc_field_names">
91 <n-select
92 v-model:value="form.ioc_field_names"
93 :options="availableMappingsOptions"
94 placeholder="Select..."
95 clearable
96 filterable
97 multiple
98 :tag="arbitrarySourceField"
99 to="body"
100 :disabled="!isFieldEnabled"
101 :loading="loadingAvailableMappings"
102 >
103 <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
104 </n-select>
105 </n-form-item>
106 <n-form-item path="asset_name_array" :show-require-mark="false">
107 <template #label>
108 <div class="flex flex-col gap-1">
109 <span>
110 Asset name fields
111 <span class="n-form-item-label__asterisk">*</span>
112 </span>
113 <small class="text-secondary">
114 Add multiple fields. First field has priority (checked first).
115 </small>
116 </div>
117 </template>
118 <n-dynamic-tags
119 v-model:value="form.asset_name_array"
120 :max="10"
121 :disabled="!isFieldEnabled"
122 :render-tag="renderAssetTag"
123 >
124 <template #input="{ submit, deactivate }">
125 <n-auto-complete
126 v-model:value="assetNameInput"
127 :options="filteredAssetNameOptions"
128 :disabled="!isFieldEnabled"
129 placeholder="Type or select field name"
130 @select="handleAssetSelect($event, submit)"
131 @blur="deactivate"
132 @keyup.enter="handleAssetEnter(submit)"
133 />
134 </template>
135 <template #trigger="{ activate, disabled }">
136 <n-button
137 size="small"
138 type="primary"
139 dashed
140 :disabled="disabled || !isFieldEnabled"
141 @click="activate()"
142 >
143 <template #icon>
144 <Icon :name="AddIcon" />
145 </template>
146 Add Field
147 </n-button>
148 </template>
149 </n-dynamic-tags>
150 </n-form-item>
151 <n-form-item label="Timefield name" path="timefield_name">
152 <n-select
153 v-model:value="form.timefield_name"
154 :options="availableMappingsOptions"
155 placeholder="Select..."
156 clearable
157 filterable
158 :tag="arbitrarySourceField"
159 to="body"
160 :disabled="!isFieldEnabled"
161 :loading="loadingAvailableMappings"
162 >
163 <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
164 </n-select>
165 </n-form-item>
166 <n-form-item label="Alert title name" path="alert_title_name">
167 <n-select
168 v-model:value="form.alert_title_name"
169 :options="availableMappingsOptions"
170 placeholder="Select..."
171 clearable
172 filterable
173 :tag="arbitrarySourceField"
174 to="body"
175 :disabled="!isFieldEnabled"
176 :loading="loadingAvailableMappings"
177 >
178 <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
179 </n-select>
180 </n-form-item>
181 </div>
182 <div class="flex justify-between gap-3">
183 <div>
184 <slot name="additionalActions"></slot>
185 </div>
186 <div class="flex items-center gap-3">
187 <n-button :disabled="loading" @click="reset()">Reset</n-button>
188 <n-button
189 type="primary"
190 :disabled="!isValid"
191 :loading="submitting"
192 @click="validate(() => submitForm())"
193 >
194 Submit
195 </n-button>
196 </div>
197 </div>
198 </div>
199 </n-form>
200 </n-spin>
201 </template>
202
203 <script setup lang="ts">
204 import type { FormInst, FormItemRule, FormRules, FormValidationError, MessageReactive } from "naive-ui"
205 import type { SourceConfiguration, SourceConfigurationModel, SourceName } from "@/types/incidentManagement/sources.d"
206 import _intersection from "lodash/intersection"
207 import {
208 NAlert,
209 NAutoComplete,
210 NButton,
211 NDynamicTags,
212 NForm,
213 NFormItem,
214 NInput,
215 NSelect,
216 NSpin,
217 NTag,
218 useMessage
219 } from "naive-ui"
220 import { computed, h, onBeforeMount, onMounted, ref, toRefs, watch } from "vue"
221 import Api from "@/api"
222 import Icon from "@/components/common/Icon.vue"
223
224 const props = defineProps<{
225 sourceConfigurationModel?: SourceConfigurationModel
226 showSourceField?: boolean
227 arbitrarySourceField?: boolean
228 disableSourceField?: boolean
229 showIndexNameField?: boolean
230 disableIndexNameField?: boolean
231 applyFieldsSanitize?: boolean
232 disabledSources?: SourceName[]
233 }>()
234
235 const emit = defineEmits<{
236 (e: "submitted", value: SourceConfiguration): void
237 (
238 e: "mounted",
239 value: {
240 reset: () => void
241 toggleSubmittingFlag: () => boolean
242 }
243 ): void
244 }>()
245
246 const AddIcon = "carbon:add"
247
248 const {
249 sourceConfigurationModel,
250 showSourceField,
251 arbitrarySourceField,
252 disableSourceField,
253 showIndexNameField,
254 disableIndexNameField,
255 applyFieldsSanitize,
256 disabledSources
257 } = toRefs(props)
258
259 const submitting = ref(false)
260 const loadingSource = ref(false)
261 const loadingIndexNames = ref(false)
262 const loadingAvailableMappings = ref(false)
263 const loadingSocfortressRecommendsWazuh = ref(false)
264 const socfortressRecommendsWazuh = ref<SourceConfiguration | null>(null)
265 const loading = computed(() => loadingAvailableMappings.value || loadingIndexNames.value || loadingSource.value)
266 const message = useMessage()
267 const form = ref<SourceConfigurationModel>(getSourceConfigurationForm())
268 const formRef = ref<FormInst | null>(null)
269 const availableMappingsOptions = ref<{ label: string; value: string }[]>([])
270 const indexNamesOptions = ref<{ label: string; value: string }[]>([])
271 const assetNameInput = ref("")
272
273 const rules: FormRules = {
274 source: {
275 required: true,
276 message: "Please input the Source",
277 trigger: ["input", "blur"]
278 },
279 field_names: {
280 required: true,
281 validator: validateAtLeastOne,
282 trigger: ["input", "blur"]
283 },
284 asset_name_array: {
285 required: true,
286 validator: validateAtLeastOneAsset,
287 trigger: ["input", "blur", "change"]
288 },
289 timefield_name: {
290 required: true,
291 message: "Please input the timefield name",
292 trigger: ["input", "blur"]
293 },
294 alert_title_name: {
295 required: true,
296 message: "Please input the alert title name",
297 trigger: ["input", "blur"]
298 }
299 }
300
301 let validationMessage: MessageReactive | null = null
302
303 const isSourceNotAllowed = computed(
304 () => form.value.source && disabledSources.value?.length && disabledSources.value.includes(form.value.source)
305 )
306 const isFieldEnabled = computed(
307 () => (!!form.value.index_name && !isSourceNotAllowed.value) || arbitrarySourceField.value
308 )
309
310 const isValid = computed(() => {
311 if (
312 !form.value.field_names.length ||
313 !form.value.asset_name_array?.length ||
314 !form.value.timefield_name ||
315 !form.value.alert_title_name ||
316 !form.value.source ||
317 isSourceNotAllowed.value
318 ) {
319 return false
320 }
321
322 return true
323 })
324
325 const isSocfortressRecommendsAvailable = computed(() => form.value.source?.toLowerCase() === "wazuh")
326
327 const filteredAssetNameOptions = computed(() => {
328 return availableMappingsOptions.value.filter(option => !form.value.asset_name_array?.includes(option.value))
329 })
330
331 watch(sourceConfigurationModel, () => {
332 reset()
333 init()
334 })
335
336 watch(
337 () => form.value.index_name,
338 val => {
339 if (val) {
340 getAvailableMappings(val)
341 getSourceByIndex(val)
342 } else {
343 availableMappingsOptions.value = []
344 }
345 }
346 )
347
348 watch(
349 () => form.value.asset_name_array,
350 newVal => {
351 if (newVal && newVal.length > 0) {
352 form.value.asset_name = newVal.join(", ")
353 } else {
354 form.value.asset_name = null
355 }
356 },
357 { deep: true }
358 )
359
360 function renderAssetTag(tag: string, index: number) {
361 return h(
362 NTag,
363 {
364 type: index === 0 ? "primary" : "default",
365 closable: true,
366 onClose: () => {
367 form.value.asset_name_array?.splice(index, 1)
368 }
369 },
370 {
371 default: () => tag,
372 icon: () => (index === 0 ? h(Icon, { name: "carbon:star-filled", size: 14 }) : null)
373 }
374 )
375 }
376
377 function handleAssetSelect(value: string, submit: (value: string) => void) {
378 if (value && !form.value.asset_name_array?.includes(value)) {
379 submit(value)
380 assetNameInput.value = ""
381 }
382 }
383
384 function handleAssetEnter(submit: (value: string) => void) {
385 const value = assetNameInput.value.trim()
386 if (value && !form.value.asset_name_array?.includes(value)) {
387 submit(value)
388 assetNameInput.value = ""
389 }
390 }
391
392 function resetIndexAvailable() {
393 form.value.index_name = null
394 if (form.value.source) {
395 getAvailableIndices(form.value.source)
396 }
397 }
398
399 function validateAtLeastOne(_rule: FormItemRule, value: string[]) {
400 if (!value || !value.length) {
401 return new Error("Please select at least one option")
402 }
403
404 return true
405 }
406
407 function validateAtLeastOneAsset(_rule: FormItemRule, value: string[]) {
408 if (!value || !value.length) {
409 return new Error("Please add at least one asset name field")
410 }
411 return true
412 }
413
414 function validate(cb?: () => void) {
415 if (!formRef.value) return
416
417 formRef.value.validate((errors?: Array<FormValidationError>) => {
418 if (!errors) {
419 validationMessage?.destroy()
420 validationMessage = null
421 if (cb) cb()
422 } else {
423 if (!validationMessage) {
424 validationMessage = message.warning("You must fill in the required fields correctly.")
425 }
426 return false
427 }
428 })
429 }
430
431 function getSourceConfigurationForm(): SourceConfigurationModel {
432 const assetName = sourceConfigurationModel.value?.asset_name
433 const assetNameArray = assetName
434 ? assetName
435 .split(",")
436 .map(s => s.trim())
437 .filter(Boolean)
438 : []
439
440 return {
441 field_names: sourceConfigurationModel.value?.field_names || [],
442 ioc_field_names: sourceConfigurationModel.value?.ioc_field_names || [],
443 asset_name: sourceConfigurationModel.value?.asset_name || null,
444 asset_name_array: assetNameArray,
445 timefield_name: sourceConfigurationModel.value?.timefield_name || null,
446 alert_title_name: sourceConfigurationModel.value?.alert_title_name || null,
447 source: sourceConfigurationModel.value?.source || "",
448 index_name: sourceConfigurationModel.value?.index_name || null
449 }
450 }
451
452 function reset() {
453 if (!loading.value) {
454 resetForm()
455 formRef.value?.restoreValidation()
456 }
457 }
458
459 function resetForm() {
460 form.value = getSourceConfigurationForm()
461 }
462
463 function sanitizeFields() {
464 const availableMappings = availableMappingsOptions.value.map(o => o.value)
465
466 form.value.field_names = _intersection(availableMappings, form.value.field_names)
467 form.value.ioc_field_names = _intersection(availableMappings, form.value.ioc_field_names)
468
469 if (form.value.asset_name_array?.length) {
470 form.value.asset_name_array = form.value.asset_name_array.filter(
471 name => availableMappings.includes(name) || arbitrarySourceField.value
472 )
473 }
474
475 if (form.value.timefield_name && !availableMappings.includes(form.value.timefield_name)) {
476 form.value.timefield_name = null
477 }
478 if (form.value.alert_title_name && !availableMappings.includes(form.value.alert_title_name)) {
479 form.value.alert_title_name = null
480 }
481 }
482
483 function submitForm() {
484 const payload: SourceConfiguration = {
485 field_names: form.value?.field_names || [],
486 ioc_field_names: form.value?.ioc_field_names || [],
487 asset_name: form.value?.asset_name || "",
488 timefield_name: form.value?.timefield_name || "",
489 alert_title_name: form.value?.alert_title_name || "",
490 source: form.value?.source || ""
491 }
492 emit("submitted", payload)
493 }
494
495 function toggleSubmittingFlag(status?: boolean) {
496 if (status !== undefined) {
497 submitting.value = status
498 } else {
499 submitting.value = !submitting.value
500 }
501
502 return submitting.value
503 }
504
505 function resetSource() {
506 form.value.source = ""
507 }
508
509 function setSocfortressRecommendsWazuh() {
510 form.value.field_names = socfortressRecommendsWazuh.value?.field_names || []
511 form.value.ioc_field_names = socfortressRecommendsWazuh.value?.ioc_field_names || []
512
513 const assetName = socfortressRecommendsWazuh.value?.asset_name
514 form.value.asset_name_array = assetName
515 ? assetName
516 .split(",")
517 .map(s => s.trim())
518 .filter(Boolean)
519 : []
520
521 form.value.timefield_name = socfortressRecommendsWazuh.value?.timefield_name || null
522 form.value.alert_title_name = socfortressRecommendsWazuh.value?.alert_title_name || null
523 form.value.source = socfortressRecommendsWazuh.value?.source || ""
524 }
525
526 function getSocfortressRecommendsWazuh() {
527 if (socfortressRecommendsWazuh.value) {
528 setSocfortressRecommendsWazuh()
529 return
530 }
531
532 loadingSocfortressRecommendsWazuh.value = true
533
534 Api.incidentManagement.sources
535 .getSocfortressRecommendsWazuh()
536 .then(res => {
537 if (res.data.success) {
538 socfortressRecommendsWazuh.value = {
539 field_names: res.data.field_names,
540 ioc_field_names: res.data.ioc_field_names,
541 asset_name: res.data.asset_name,
542 timefield_name: res.data.timefield_name,
543 alert_title_name: res.data.alert_title_name,
544 source: res.data.source
545 }
546
547 setSocfortressRecommendsWazuh()
548 } else {
549 message.warning(res.data?.message || "An error occurred. Please try again later.")
550 }
551 })
552 .catch(err => {
553 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
554 })
555 .finally(() => {
556 loadingSocfortressRecommendsWazuh.value = false
557 })
558 }
559
560 function getAvailableMappings(indexName: string) {
561 loadingAvailableMappings.value = true
562
563 Api.incidentManagement.sources
564 .getAvailableMappings(indexName)
565 .then(res => {
566 if (res.data.success) {
567 availableMappingsOptions.value = (res.data?.available_mappings || []).map(o => ({
568 label: o,
569 value: o
570 }))
571 if (applyFieldsSanitize.value) {
572 sanitizeFields()
573 }
574 } else {
575 message.warning(res.data?.message || "An error occurred. Please try again later.")
576 }
577 })
578 .catch(err => {
579 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
580 })
581 .finally(() => {
582 loadingAvailableMappings.value = false
583 })
584 }
585
586 function getAvailableIndices(source: SourceName) {
587 loadingIndexNames.value = true
588
589 Api.incidentManagement.sources
590 .getAvailableIndices(source)
591 .then(res => {
592 if (res.data.success) {
593 indexNamesOptions.value = (res.data?.indices || []).map(o => ({
594 label: o,
595 value: o
596 }))
597 } else {
598 resetSource()
599 message.warning(res.data?.message || "An error occurred. Please try again later.")
600 }
601 })
602 .catch(err => {
603 resetSource()
604 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
605 })
606 .finally(() => {
607 loadingIndexNames.value = false
608 })
609 }
610
611 function getSourceByIndex(indexName: string) {
612 loadingSource.value = true
613
614 Api.incidentManagement.sources
615 .getSourceByIndex(indexName)
616 .then(res => {
617 if (res.data.success) {
618 form.value.source = res.data.source
619 } else {
620 resetSource()
621 message.warning(res.data?.message || "An error occurred. Please try again later.")
622 }
623 })
624 .catch(err => {
625 resetSource()
626 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
627 })
628 .finally(() => {
629 loadingSource.value = false
630 })
631 }
632
633 function init() {
634 if (sourceConfigurationModel.value?.index_name) {
635 getAvailableMappings(sourceConfigurationModel.value.index_name)
636
637 if (!sourceConfigurationModel.value?.source) {
638 getSourceByIndex(sourceConfigurationModel.value.index_name)
639 }
640 }
641 if (sourceConfigurationModel.value?.source) {
642 getAvailableIndices(sourceConfigurationModel.value.source)
643 }
644 }
645
646 onBeforeMount(() => {
647 init()
648 })
649
650 onMounted(() => {
651 emit("mounted", {
652 reset,
653 toggleSubmittingFlag
654 })
655 })
656 </script>
657
658 <style lang="scss" scoped>
659 .source-field {
660 :deep() {
661 .n-form-item-label__text {
662 width: 100%;
663 }
664 }
665 }
666
667 :deep(.n-dynamic-tags) {
668 width: 100%;
669 }
670 </style>