@cryptotaxi247 / CoPilot / commits / 35c8dad8

524 support for multiple asset name field (#534)

* Add support for resolving multiple asset name fields in alert payload * Add asset_name_array to SourceConfigurationModel for UI array support * Add support for multiple asset name fields in SourceConfigurationModel * Refactor asset name resolution to support multiple fields and improve logging

taylor_socfortress committed Nov 30, 2025 at 13:47 UTC 35c8dad811854153edc75e639351f87ebda49bf9
3 files changed +577 -435
backend/app/incidents/services/incident_alert.py
+35 -3
@@ -430,6 +430,34 @@ async def get_all_field_names(syslog_type: str, session: AsyncSession) -> FieldN
430 )
431
432
433 +async def resolve_asset_name_from_payload(asset_name_field: str, alert_payload: dict) -> Optional[str]:
434 + """
435 + Resolve the actual asset name value from the alert payload.
436 + Supports multiple asset name fields separated by commas.
437 +
438 + Args:
439 + asset_name_field (str): The asset name field(s) from config (can be comma-separated)
440 + alert_payload (dict): The alert payload containing the data
441 +
442 + Returns:
443 + Optional[str]: The resolved asset name value, or None if not found
444 + """
445 + # Split by comma and strip whitespace to support multiple fields
446 + possible_asset_fields = [field.strip() for field in asset_name_field.split(",")]
447 +
448 + logger.info(f"Checking for asset name in fields: {possible_asset_fields}")
449 +
450 + # Try each possible asset field in order
451 + for field in possible_asset_fields:
452 + if field in alert_payload and alert_payload[field]:
453 + asset_value = alert_payload[field]
454 + logger.info(f"Found asset name '{asset_value}' in field '{field}'")
455 + return asset_value
456 +
457 + logger.warning(f"No asset name found in any of these fields: {possible_asset_fields}")
458 + return None
459 +
460 +
461 async def get_process_name(source_dict: dict) -> List[str]:
462 """
463 Get the process name from the source dictionary.
@@ -536,8 +564,12 @@ async def build_alert_payload(
564 dict: The built alert payload.
565 """
566 field_names = await get_all_field_names(syslog_type, session)
539 - # Validate that the field_names exist in the alert_payload
540 - for field_name in [field_names.asset_name, field_names.timefield_name, field_names.alert_title_name]:
567 +
568 + # Resolve the actual asset name from the payload using multiple possible fields
569 + asset_name_value = await resolve_asset_name_from_payload(field_names.asset_name, alert_payload)
570 +
571 + # Validate that the required fields exist in the alert_payload
572 + for field_name in [field_names.timefield_name, field_names.alert_title_name]:
573 if field_name not in alert_payload:
574 raise HTTPException(
575 status_code=400,
@@ -550,7 +582,7 @@ async def build_alert_payload(
582
583 return CreatedAlertPayload(
584 alert_context_payload=await build_alert_context_payload(alert_payload, field_names),
553 - asset_payload=alert_payload[field_names.asset_name] if field_names.asset_name in alert_payload else None,
585 + asset_payload=asset_name_value,
586 timefield_payload=alert_payload[field_names.timefield_name] if field_names.timefield_name in alert_payload else None,
587 alert_title_payload=cleaned_alert_title,
588 ioc_payload=await build_ioc_payload(alert_payload, field_names),
frontend/src/components/incidentManagement/sources/SourceConfigurationForm.vue
+541 -432
@@ -1,214 +1,247 @@
1 <template>
2 - <n-spin :show="loading" class="creation-report-form">
3 - <n-form ref="formRef" :model="form" :rules="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 label="Asset name" path="asset_name">
107 - <n-select
108 - v-model:value="form.asset_name"
109 - :options="availableMappingsOptions"
110 - placeholder="Select..."
111 - clearable
112 - filterable
113 - :tag="arbitrarySourceField"
114 - to="body"
115 - :disabled="!isFieldEnabled"
116 - :loading="loadingAvailableMappings"
117 - >
118 - <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
119 - </n-select>
120 - </n-form-item>
121 - <n-form-item label="Timefield name" path="timefield_name">
122 - <n-select
123 - v-model:value="form.timefield_name"
124 - :options="availableMappingsOptions"
125 - placeholder="Select..."
126 - clearable
127 - filterable
128 - :tag="arbitrarySourceField"
129 - to="body"
130 - :disabled="!isFieldEnabled"
131 - :loading="loadingAvailableMappings"
132 - >
133 - <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
134 - </n-select>
135 - </n-form-item>
136 - <n-form-item label="Alert title name" path="alert_title_name">
137 - <n-select
138 - v-model:value="form.alert_title_name"
139 - :options="availableMappingsOptions"
140 - placeholder="Select..."
141 - clearable
142 - filterable
143 - :tag="arbitrarySourceField"
144 - to="body"
145 - :disabled="!isFieldEnabled"
146 - :loading="loadingAvailableMappings"
147 - >
148 - <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
149 - </n-select>
150 - </n-form-item>
151 - </div>
152 - <div class="flex justify-between gap-3">
153 - <div>
154 - <slot name="additionalActions"></slot>
155 - </div>
156 - <div class="flex items-center gap-3">
157 - <n-button :disabled="loading" @click="reset()">Reset</n-button>
158 - <n-button
159 - type="primary"
160 - :disabled="!isValid"
161 - :loading="submitting"
162 - @click="validate(() => submit())"
163 - >
164 - Submit
165 - </n-button>
166 - </div>
167 - </div>
168 - </div>
169 - </n-form>
170 - </n-spin>
2 + <n-spin :show="loading" class="creation-report-form">
3 + <n-form ref="formRef" :model="form" :rules="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 + <n-text depth="3" style="font-size: 11px; font-weight: 400">
114 + Add multiple fields. First field has priority (checked first).
115 + </n-text>
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(() => submit())"
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"
177 -import { NAlert, NButton, NForm, NFormItem, NInput, NSelect, NSpin, useMessage } from "naive-ui"
178 -import { computed, onBeforeMount, onMounted, ref, toRefs, watch } from "vue"
207 +import { NAlert, NButton, NForm, NFormItem, NInput, NSelect, NSpin, NDynamicTags, NAutoComplete, NTag, NText, useMessage } from "naive-ui"
208 +import { computed, h, onBeforeMount, onMounted, ref, toRefs, watch } from "vue"
209 import Api from "@/api"
210 +import Icon from "@/components/common/Icon.vue"
211 +
212 +const AddIcon = "carbon:add"
213
214 const props = defineProps<{
182 - sourceConfigurationModel?: SourceConfigurationModel
183 - showSourceField?: boolean
184 - arbitrarySourceField?: boolean
185 - disableSourceField?: boolean
186 - showIndexNameField?: boolean
187 - disableIndexNameField?: boolean
188 - applyFieldsSanitize?: boolean
189 - disabledSources?: SourceName[]
215 + sourceConfigurationModel?: SourceConfigurationModel
216 + showSourceField?: boolean
217 + arbitrarySourceField?: boolean
218 + disableSourceField?: boolean
219 + showIndexNameField?: boolean
220 + disableIndexNameField?: boolean
221 + applyFieldsSanitize?: boolean
222 + disabledSources?: SourceName[]
223 }>()
224
225 const emit = defineEmits<{
193 - (e: "submitted", value: SourceConfiguration): void
194 - (
195 - e: "mounted",
196 - value: {
197 - reset: () => void
198 - toggleSubmittingFlag: () => boolean
199 - }
200 - ): void
226 + (e: "submitted", value: SourceConfiguration): void
227 + (
228 + e: "mounted",
229 + value: {
230 + reset: () => void
231 + toggleSubmittingFlag: () => boolean
232 + }
233 + ): void
234 }>()
235
236 const {
204 - sourceConfigurationModel,
205 - showSourceField,
206 - arbitrarySourceField,
207 - disableSourceField,
208 - showIndexNameField,
209 - disableIndexNameField,
210 - applyFieldsSanitize,
211 - disabledSources
237 + sourceConfigurationModel,
238 + showSourceField,
239 + arbitrarySourceField,
240 + disableSourceField,
241 + showIndexNameField,
242 + disableIndexNameField,
243 + applyFieldsSanitize,
244 + disabledSources
245 } = toRefs(props)
246
247 const submitting = ref(false)
@@ -223,323 +256,399 @@ const form = ref<SourceConfigurationModel>(getSourceConfigurationForm())
256 const formRef = ref<FormInst | null>(null)
257 const availableMappingsOptions = ref<{ label: string; value: string }[]>([])
258 const indexNamesOptions = ref<{ label: string; value: string }[]>([])
259 +const assetNameInput = ref("")
260
261 const rules: FormRules = {
228 - source: {
229 - required: true,
230 - message: "Please input the Source",
231 - trigger: ["input", "blur"]
232 - },
233 - field_names: {
234 - required: true,
235 - validator: validateAtLeastOne,
236 - trigger: ["input", "blur"]
237 - },
238 - asset_name: {
239 - required: true,
240 - message: "Please input the asset name",
241 - trigger: ["input", "blur"]
242 - },
243 - timefield_name: {
244 - required: true,
245 - message: "Please input the timefield name",
246 - trigger: ["input", "blur"]
247 - },
248 - alert_title_name: {
249 - required: true,
250 - message: "Please input the alert title name",
251 - trigger: ["input", "blur"]
252 - }
262 + source: {
263 + required: true,
264 + message: "Please input the Source",
265 + trigger: ["input", "blur"]
266 + },
267 + field_names: {
268 + required: true,
269 + validator: validateAtLeastOne,
270 + trigger: ["input", "blur"]
271 + },
272 + asset_name_array: {
273 + required: true,
274 + validator: validateAtLeastOneAsset,
275 + trigger: ["input", "blur", "change"]
276 + },
277 + timefield_name: {
278 + required: true,
279 + message: "Please input the timefield name",
280 + trigger: ["input", "blur"]
281 + },
282 + alert_title_name: {
283 + required: true,
284 + message: "Please input the alert title name",
285 + trigger: ["input", "blur"]
286 + }
287 }
288
289 let validationMessage: MessageReactive | null = null
290
291 const isSourceNotAllowed = computed(
258 - () => form.value.source && disabledSources.value?.length && disabledSources.value.includes(form.value.source)
292 + () => form.value.source && disabledSources.value?.length && disabledSources.value.includes(form.value.source)
293 )
294 const isFieldEnabled = computed(
261 - () => (!!form.value.index_name && !isSourceNotAllowed.value) || arbitrarySourceField.value
295 + () => (!!form.value.index_name && !isSourceNotAllowed.value) || arbitrarySourceField.value
296 )
297
298 const isValid = computed(() => {
265 - if (
266 - !form.value.field_names.length ||
267 - !form.value.asset_name ||
268 - !form.value.timefield_name ||
269 - !form.value.alert_title_name ||
270 - !form.value.source ||
271 - isSourceNotAllowed.value
272 - ) {
273 - return false
274 - }
275 -
276 - return true
299 + if (
300 + !form.value.field_names.length ||
301 + !form.value.asset_name_array?.length ||
302 + !form.value.timefield_name ||
303 + !form.value.alert_title_name ||
304 + !form.value.source ||
305 + isSourceNotAllowed.value
306 + ) {
307 + return false
308 + }
309 +
310 + return true
311 })
312
313 const isSocfortressRecommendsAvailable = computed(() => form.value.source?.toLowerCase() === "wazuh")
314
315 +const filteredAssetNameOptions = computed(() => {
316 + return availableMappingsOptions.value.filter(
317 + option => !form.value.asset_name_array?.includes(option.value)
318 + )
319 +})
320 +
321 watch(sourceConfigurationModel, () => {
282 - reset()
283 - init()
322 + reset()
323 + init()
324 })
325
326 watch(
287 - () => form.value.index_name,
288 - val => {
289 - if (val) {
290 - getAvailableMappings(val)
291 - getSourceByIndex(val)
292 - } else {
293 - availableMappingsOptions.value = []
294 - }
295 - }
327 + () => form.value.index_name,
328 + val => {
329 + if (val) {
330 + getAvailableMappings(val)
331 + getSourceByIndex(val)
332 + } else {
333 + availableMappingsOptions.value = []
334 + }
335 + }
336 )
337
338 +watch(
339 + () => form.value.asset_name_array,
340 + (newVal) => {
341 + if (newVal && newVal.length > 0) {
342 + form.value.asset_name = newVal.join(", ")
343 + } else {
344 + form.value.asset_name = null
345 + }
346 + },
347 + { deep: true }
348 +)
349 +
350 +function renderAssetTag(tag: string, index: number) {
351 + return h(
352 + NTag,
353 + {
354 + type: index === 0 ? "primary" : "default",
355 + closable: true,
356 + onClose: () => {
357 + form.value.asset_name_array?.splice(index, 1)
358 + }
359 + },
360 + {
361 + default: () => tag,
362 + icon: () => (index === 0 ? h(Icon, { name: "carbon:star-filled", size: 14 }) : null)
363 + }
364 + )
365 +}
366 +
367 +function handleAssetSelect(value: string, submit: (value: string) => void) {
368 + if (value && !form.value.asset_name_array?.includes(value)) {
369 + submit(value)
370 + assetNameInput.value = ""
371 + }
372 +}
373 +
374 +function handleAssetEnter(submit: (value: string) => void) {
375 + const value = assetNameInput.value.trim()
376 + if (value && !form.value.asset_name_array?.includes(value)) {
377 + submit(value)
378 + assetNameInput.value = ""
379 + }
380 +}
381 +
382 function resetIndexAvailable() {
299 - form.value.index_name = null
300 - if (form.value.source) {
301 - getAvailableIndices(form.value.source)
302 - }
383 + form.value.index_name = null
384 + if (form.value.source) {
385 + getAvailableIndices(form.value.source)
386 + }
387 }
388
389 function validateAtLeastOne(_rule: FormItemRule, value: string[]) {
306 - if (!value || !value.length) {
307 - return new Error("Please select at least one option")
308 - }
390 + if (!value || !value.length) {
391 + return new Error("Please select at least one option")
392 + }
393 +
394 + return true
395 +}
396
310 - return true
397 +function validateAtLeastOneAsset(_rule: FormItemRule, value: string[]) {
398 + if (!value || !value.length) {
399 + return new Error("Please add at least one asset name field")
400 + }
401 + return true
402 }
403
404 function validate(cb?: () => void) {
314 - if (!formRef.value) return
315 -
316 - formRef.value.validate((errors?: Array<FormValidationError>) => {
317 - if (!errors) {
318 - validationMessage?.destroy()
319 - validationMessage = null
320 - if (cb) cb()
321 - } else {
322 - if (!validationMessage) {
323 - validationMessage = message.warning("You must fill in the required fields correctly.")
324 - }
325 - return false
326 - }
327 - })
405 + if (!formRef.value) return
406 +
407 + formRef.value.validate((errors?: Array<FormValidationError>) => {
408 + if (!errors) {
409 + validationMessage?.destroy()
410 + validationMessage = null
411 + if (cb) cb()
412 + } else {
413 + if (!validationMessage) {
414 + validationMessage = message.warning("You must fill in the required fields correctly.")
415 + }
416 + return false
417 + }
418 + })
419 }
420
421 function getSourceConfigurationForm(): SourceConfigurationModel {
331 - return {
332 - field_names: sourceConfigurationModel.value?.field_names || [],
333 - ioc_field_names: sourceConfigurationModel.value?.ioc_field_names || [],
334 - asset_name: sourceConfigurationModel.value?.asset_name || null,
335 - timefield_name: sourceConfigurationModel.value?.timefield_name || null,
336 - alert_title_name: sourceConfigurationModel.value?.alert_title_name || null,
337 - source: sourceConfigurationModel.value?.source || "",
338 - index_name: sourceConfigurationModel.value?.index_name || null
339 - }
422 + const assetName = sourceConfigurationModel.value?.asset_name
423 + const assetNameArray = assetName
424 + ? assetName.split(",").map(s => s.trim()).filter(Boolean)
425 + : []
426 +
427 + return {
428 + field_names: sourceConfigurationModel.value?.field_names || [],
429 + ioc_field_names: sourceConfigurationModel.value?.ioc_field_names || [],
430 + asset_name: sourceConfigurationModel.value?.asset_name || null,
431 + asset_name_array: assetNameArray,
432 + timefield_name: sourceConfigurationModel.value?.timefield_name || null,
433 + alert_title_name: sourceConfigurationModel.value?.alert_title_name || null,
434 + source: sourceConfigurationModel.value?.source || "",
435 + index_name: sourceConfigurationModel.value?.index_name || null
436 + }
437 }
438
439 function reset() {
343 - if (!loading.value) {
344 - resetForm()
345 - formRef.value?.restoreValidation()
346 - }
440 + if (!loading.value) {
441 + resetForm()
442 + formRef.value?.restoreValidation()
443 + }
444 }
445
446 function resetForm() {
350 - form.value = getSourceConfigurationForm()
447 + form.value = getSourceConfigurationForm()
448 }
449
450 function sanitizeFields() {
354 - const availableMappings = availableMappingsOptions.value.map(o => o.value)
355 -
356 - form.value.field_names = _intersection(availableMappings, form.value.field_names)
357 - form.value.ioc_field_names = _intersection(availableMappings, form.value.ioc_field_names)
358 -
359 - if (form.value.asset_name && !availableMappings.includes(form.value.asset_name)) {
360 - form.value.asset_name = null
361 - }
362 - if (form.value.timefield_name && !availableMappings.includes(form.value.timefield_name)) {
363 - form.value.timefield_name = null
364 - }
365 - if (form.value.alert_title_name && !availableMappings.includes(form.value.alert_title_name)) {
366 - form.value.alert_title_name = null
367 - }
451 + const availableMappings = availableMappingsOptions.value.map(o => o.value)
452 +
453 + form.value.field_names = _intersection(availableMappings, form.value.field_names)
454 + form.value.ioc_field_names = _intersection(availableMappings, form.value.ioc_field_names)
455 +
456 + if (form.value.asset_name_array?.length) {
457 + form.value.asset_name_array = form.value.asset_name_array.filter(name =>
458 + availableMappings.includes(name) || arbitrarySourceField.value
459 + )
460 + }
461 +
462 + if (form.value.timefield_name && !availableMappings.includes(form.value.timefield_name)) {
463 + form.value.timefield_name = null
464 + }
465 + if (form.value.alert_title_name && !availableMappings.includes(form.value.alert_title_name)) {
466 + form.value.alert_title_name = null
467 + }
468 }
469
470 function submit() {
371 - const payload: SourceConfiguration = {
372 - field_names: form.value?.field_names || [],
373 - ioc_field_names: form.value?.ioc_field_names || [],
374 - asset_name: form.value?.asset_name || "",
375 - timefield_name: form.value?.timefield_name || "",
376 - alert_title_name: form.value?.alert_title_name || "",
377 - source: form.value?.source || ""
378 - }
379 - emit("submitted", payload)
471 + const payload: SourceConfiguration = {
472 + field_names: form.value?.field_names || [],
473 + ioc_field_names: form.value?.ioc_field_names || [],
474 + asset_name: form.value?.asset_name || "",
475 + timefield_name: form.value?.timefield_name || "",
476 + alert_title_name: form.value?.alert_title_name || "",
477 + source: form.value?.source || ""
478 + }
479 + emit("submitted", payload)
480 }
481
482 function toggleSubmittingFlag(status?: boolean) {
383 - if (status !== undefined) {
384 - submitting.value = status
385 - } else {
386 - submitting.value = !submitting.value
387 - }
483 + if (status !== undefined) {
484 + submitting.value = status
485 + } else {
486 + submitting.value = !submitting.value
487 + }
488
389 - return submitting.value
489 + return submitting.value
490 }
491
492 function resetSource() {
393 - form.value.source = ""
493 + form.value.source = ""
494 }
495
496 function setSocfortressRecommendsWazuh() {
397 - form.value.field_names = socfortressRecommendsWazuh.value?.field_names || []
398 - form.value.ioc_field_names = socfortressRecommendsWazuh.value?.ioc_field_names || []
399 - form.value.asset_name = socfortressRecommendsWazuh.value?.asset_name || null
400 - form.value.timefield_name = socfortressRecommendsWazuh.value?.timefield_name || null
401 - form.value.alert_title_name = socfortressRecommendsWazuh.value?.alert_title_name || null
402 - form.value.source = socfortressRecommendsWazuh.value?.source || ""
497 + form.value.field_names = socfortressRecommendsWazuh.value?.field_names || []
498 + form.value.ioc_field_names = socfortressRecommendsWazuh.value?.ioc_field_names || []
499 +
500 + const assetName = socfortressRecommendsWazuh.value?.asset_name
501 + form.value.asset_name_array = assetName
502 + ? assetName.split(",").map(s => s.trim()).filter(Boolean)
503 + : []
504 +
505 + form.value.timefield_name = socfortressRecommendsWazuh.value?.timefield_name || null
506 + form.value.alert_title_name = socfortressRecommendsWazuh.value?.alert_title_name || null
507 + form.value.source = socfortressRecommendsWazuh.value?.source || ""
508 }
509
510 function getSocfortressRecommendsWazuh() {
406 - if (socfortressRecommendsWazuh.value) {
407 - setSocfortressRecommendsWazuh()
408 - return
409 - }
410 -
411 - loadingSocfortressRecommendsWazuh.value = true
412 -
413 - Api.incidentManagement.sources
414 - .getSocfortressRecommendsWazuh()
415 - .then(res => {
416 - if (res.data.success) {
417 - socfortressRecommendsWazuh.value = {
418 - field_names: res.data.field_names,
419 - ioc_field_names: res.data.ioc_field_names,
420 - asset_name: res.data.asset_name,
421 - timefield_name: res.data.timefield_name,
422 - alert_title_name: res.data.alert_title_name,
423 - source: res.data.source
424 - }
425 -
426 - setSocfortressRecommendsWazuh()
427 - } else {
428 - message.warning(res.data?.message || "An error occurred. Please try again later.")
429 - }
430 - })
431 - .catch(err => {
432 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
433 - })
434 - .finally(() => {
435 - loadingSocfortressRecommendsWazuh.value = false
436 - })
511 + if (socfortressRecommendsWazuh.value) {
512 + setSocfortressRecommendsWazuh()
513 + return
514 + }
515 +
516 + loadingSocfortressRecommendsWazuh.value = true
517 +
518 + Api.incidentManagement.sources
519 + .getSocfortressRecommendsWazuh()
520 + .then(res => {
521 + if (res.data.success) {
522 + socfortressRecommendsWazuh.value = {
523 + field_names: res.data.field_names,
524 + ioc_field_names: res.data.ioc_field_names,
525 + asset_name: res.data.asset_name,
526 + timefield_name: res.data.timefield_name,
527 + alert_title_name: res.data.alert_title_name,
528 + source: res.data.source
529 + }
530 +
531 + setSocfortressRecommendsWazuh()
532 + } else {
533 + message.warning(res.data?.message || "An error occurred. Please try again later.")
534 + }
535 + })
536 + .catch(err => {
537 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
538 + })
539 + .finally(() => {
540 + loadingSocfortressRecommendsWazuh.value = false
541 + })
542 }
543
544 function getAvailableMappings(indexName: string) {
440 - loadingAvailableMappings.value = true
441 -
442 - Api.incidentManagement.sources
443 - .getAvailableMappings(indexName)
444 - .then(res => {
445 - if (res.data.success) {
446 - availableMappingsOptions.value = (res.data?.available_mappings || []).map(o => ({
447 - label: o,
448 - value: o
449 - }))
450 - if (applyFieldsSanitize.value) {
451 - sanitizeFields()
452 - }
453 - } else {
454 - message.warning(res.data?.message || "An error occurred. Please try again later.")
455 - }
456 - })
457 - .catch(err => {
458 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
459 - })
460 - .finally(() => {
461 - loadingAvailableMappings.value = false
462 - })
545 + loadingAvailableMappings.value = true
546 +
547 + Api.incidentManagement.sources
548 + .getAvailableMappings(indexName)
549 + .then(res => {
550 + if (res.data.success) {
551 + availableMappingsOptions.value = (res.data?.available_mappings || []).map(o => ({
552 + label: o,
553 + value: o
554 + }))
555 + if (applyFieldsSanitize.value) {
556 + sanitizeFields()
557 + }
558 + } else {
559 + message.warning(res.data?.message || "An error occurred. Please try again later.")
560 + }
561 + })
562 + .catch(err => {
563 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
564 + })
565 + .finally(() => {
566 + loadingAvailableMappings.value = false
567 + })
568 }
569
570 function getAvailableIndices(source: SourceName) {
466 - loadingIndexNames.value = true
467 -
468 - Api.incidentManagement.sources
469 - .getAvailableIndices(source)
470 - .then(res => {
471 - if (res.data.success) {
472 - indexNamesOptions.value = (res.data?.indices || []).map(o => ({
473 - label: o,
474 - value: o
475 - }))
476 - } else {
477 - resetSource()
478 - message.warning(res.data?.message || "An error occurred. Please try again later.")
479 - }
480 - })
481 - .catch(err => {
482 - resetSource()
483 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
484 - })
485 - .finally(() => {
486 - loadingIndexNames.value = false
487 - })
571 + loadingIndexNames.value = true
572 +
573 + Api.incidentManagement.sources
574 + .getAvailableIndices(source)
575 + .then(res => {
576 + if (res.data.success) {
577 + indexNamesOptions.value = (res.data?.indices || []).map(o => ({
578 + label: o,
579 + value: o
580 + }))
581 + } else {
582 + resetSource()
583 + message.warning(res.data?.message || "An error occurred. Please try again later.")
584 + }
585 + })
586 + .catch(err => {
587 + resetSource()
588 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
589 + })
590 + .finally(() => {
591 + loadingIndexNames.value = false
592 + })
593 }
594
595 function getSourceByIndex(indexName: string) {
491 - loadingSource.value = true
492 -
493 - Api.incidentManagement.sources
494 - .getSourceByIndex(indexName)
495 - .then(res => {
496 - if (res.data.success) {
497 - form.value.source = res.data.source
498 - } else {
499 - resetSource()
500 - message.warning(res.data?.message || "An error occurred. Please try again later.")
501 - }
502 - })
503 - .catch(err => {
504 - resetSource()
505 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
506 - })
507 - .finally(() => {
508 - loadingSource.value = false
509 - })
596 + loadingSource.value = true
597 +
598 + Api.incidentManagement.sources
599 + .getSourceByIndex(indexName)
600 + .then(res => {
601 + if (res.data.success) {
602 + form.value.source = res.data.source
603 + } else {
604 + resetSource()
605 + message.warning(res.data?.message || "An error occurred. Please try again later.")
606 + }
607 + })
608 + .catch(err => {
609 + resetSource()
610 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
611 + })
612 + .finally(() => {
613 + loadingSource.value = false
614 + })
615 }
616
617 function init() {
513 - if (sourceConfigurationModel.value?.index_name) {
514 - getAvailableMappings(sourceConfigurationModel.value.index_name)
515 -
516 - if (!sourceConfigurationModel.value?.source) {
517 - getSourceByIndex(sourceConfigurationModel.value.index_name)
518 - }
519 - }
520 - if (sourceConfigurationModel.value?.source) {
521 - getAvailableIndices(sourceConfigurationModel.value.source)
522 - }
618 + if (sourceConfigurationModel.value?.index_name) {
619 + getAvailableMappings(sourceConfigurationModel.value.index_name)
620 +
621 + if (!sourceConfigurationModel.value?.source) {
622 + getSourceByIndex(sourceConfigurationModel.value.index_name)
623 + }
624 + }
625 + if (sourceConfigurationModel.value?.source) {
626 + getAvailableIndices(sourceConfigurationModel.value.source)
627 + }
628 }
629
630 onBeforeMount(() => {
526 - init()
631 + init()
632 })
633
634 onMounted(() => {
530 - emit("mounted", {
531 - reset,
532 - toggleSubmittingFlag
533 - })
635 + emit("mounted", {
636 + reset,
637 + toggleSubmittingFlag
638 + })
639 })
640 </script>
641
642 <style lang="scss" scoped>
643 .source-field {
539 - :deep() {
540 - .n-form-item-label__text {
541 - width: 100%;
542 - }
543 - }
644 + :deep() {
645 + .n-form-item-label__text {
646 + width: 100%;
647 + }
648 + }
649 +}
650 +
651 +:deep(.n-dynamic-tags) {
652 + width: 100%;
653 }
654 </style>
frontend/src/types/incidentManagement/sources.d.ts
+1
@@ -12,6 +12,7 @@ export interface SourceConfiguration {
12 export interface SourceConfigurationModel extends SourceConfiguration {
13 index_name?: string | null
14 asset_name: string | null
15 + asset_name_array?: string[]
16 timefield_name: string | null
17 alert_title_name: string | null
18 source: string | null