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