main
vue 309 lines 8.17 KB
Raw
1 <template>
2 <n-spin :show="loadingRule">
3 <div v-if="rule" class="flex flex-col gap-8">
4 <RuleHeader v-if="showHeader" :rule-detail="rule" />
5
6 <n-form
7 ref="formRef"
8 :model="formValue"
9 :rules
10 label-placement="left"
11 label-width="auto"
12 :disabled="executing"
13 >
14 <!-- Index Pattern -->
15 <n-form-item label="Index Pattern" path="index_pattern" required>
16 <n-input
17 v-model:value="formValue.index_pattern"
18 placeholder="e.g., wazuh-alerts-*"
19 clearable
20 :disabled="executing"
21 />
22 </n-form-item>
23
24 <!-- Result Size -->
25 <n-form-item label="Result Size" path="size">
26 <n-input-number
27 v-model:value="formValue.size"
28 :min="1"
29 :max="10000"
30 placeholder="Number of results"
31 class="w-full"
32 clearable
33 :disabled="executing"
34 />
35 </n-form-item>
36
37 <!-- Parameters Section -->
38 <div v-if="rule.parameters?.length">
39 <n-divider>Search Parameters</n-divider>
40
41 <n-form-item
42 v-for="param in rule.parameters"
43 :key="param.name"
44 :label="param.name"
45 :required="param.required"
46 >
47 <div class="flex w-full flex-col gap-2">
48 <n-input-number
49 v-if="['integer', 'int', 'long', 'number', 'numeric'].includes(param.type)"
50 v-model:value="formValue.parameters[param.name] as number"
51 class="w-full"
52 :placeholder="param.default?.toString()"
53 clearable
54 />
55 <n-switch
56 v-if="['boolean', 'bool'].includes(param.type)"
57 v-model:value="formValue.parameters[param.name] as boolean"
58 />
59 <n-input
60 v-else
61 v-model:value="formValue.parameters[param.name] as string"
62 :placeholder="param.example?.toString() || param.default?.toString()"
63 clearable
64 />
65 <div v-if="param.description" class="text-secondary text-xs">
66 {{ param.description }}
67 </div>
68 </div>
69 </n-form-item>
70 </div>
71
72 <div class="flex items-center justify-between">
73 <div class="flex flex-wrap gap-2">
74 <Badge v-for="mitre of ruleSummary?.mitre_attack_id?.slice(0, 3)" :key="mitre" size="small">
75 <template #value>{{ mitre }}</template>
76 </Badge>
77 </div>
78 <n-button type="primary" :loading="executing" :disabled="!isFormValid" @click="executeSearch">
79 <template #icon>
80 <Icon :name="PlayIcon" />
81 </template>
82 Execute Search
83 </n-button>
84 </div>
85 </n-form>
86
87 <!-- Search Results -->
88 <n-collapse-transition :show="!!searchResults">
89 <n-card v-if="searchResults" title="Search Results" size="small" content-class="flex flex-col gap-6">
90 <div class="flex flex-wrap items-center gap-3">
91 <Badge color="primary" type="splitted">
92 <template #label>Total Hits</template>
93 <template #value>{{ searchResults.total_hits }}</template>
94 </Badge>
95 <Badge type="splitted">
96 <template #label>Returned</template>
97 <template #value>{{ searchResults.returned_hits }}</template>
98 </Badge>
99 <Badge type="splitted">
100 <template #label>Time</template>
101 <template #value>{{ searchResults.took_ms }}ms</template>
102 </Badge>
103 </div>
104
105 <n-collapse v-if="searchResults.hits.length">
106 <n-collapse-item title="Results" name="results">
107 <n-scrollbar
108 class="max-h-64"
109 trigger="none"
110 :theme-overrides="{
111 railInsetVerticalRight: `0px 0px 0px auto`
112 }"
113 >
114 <div class="flex flex-col gap-2">
115 <div
116 v-for="hit in searchResults.hits"
117 :key="hit.id"
118 class="bg-secondary-color rounded-lg p-3"
119 >
120 <div class="mb-2 flex flex-wrap gap-2">
121 <Badge type="splitted" size="small">
122 <template #label>Index</template>
123 <template #value>{{ hit.index }}</template>
124 </Badge>
125 <Badge type="splitted" size="small">
126 <template #label>ID</template>
127 <template #value>{{ hit.id }}</template>
128 </Badge>
129 </div>
130 <CodeSource :code="hit.source" lang="json" />
131 </div>
132 </div>
133 </n-scrollbar>
134 </n-collapse-item>
135
136 <n-collapse-item title="Query Executed" name="query">
137 <CodeSource :code="searchResults.query_executed" lang="json" />
138 </n-collapse-item>
139 </n-collapse>
140 </n-card>
141 <n-empty v-else description="No results found" class="py-4" />
142 </n-collapse-transition>
143 </div>
144 </n-spin>
145 </template>
146
147 <script setup lang="ts">
148 import type { FormInst, FormRules } from "naive-ui"
149 import type { ExecuteSearchResponse, RuleDetail, RuleSummary } from "@/types/copilotSearches.d"
150 import type { AlertAsset } from "@/types/incidentManagement/alerts"
151 import {
152 NButton,
153 NCard,
154 NCollapse,
155 NCollapseItem,
156 NCollapseTransition,
157 NDivider,
158 NEmpty,
159 NForm,
160 NFormItem,
161 NInput,
162 NInputNumber,
163 NScrollbar,
164 NSpin,
165 NSwitch,
166 useMessage
167 } from "naive-ui"
168 import { computed, onBeforeMount, ref } from "vue"
169 import Api from "@/api"
170 import Badge from "@/components/common/Badge.vue"
171 import CodeSource from "@/components/common/CodeSource.vue"
172 import Icon from "@/components/common/Icon.vue"
173 import RuleHeader from "./RuleHeader.vue"
174
175 const props = defineProps<{
176 ruleId?: string
177 ruleSummary?: RuleSummary
178 ruleDetail?: RuleDetail
179 asset?: AlertAsset
180 showHeader?: boolean
181 }>()
182
183 const PlayIcon = "carbon:play"
184
185 const message = useMessage()
186 const formRef = ref<FormInst | null>(null)
187 const loadingRule = ref(false)
188 const executing = ref(false)
189 const rule = ref<RuleDetail | null>(null)
190 const searchResults = ref<ExecuteSearchResponse | null>(null)
191
192 const formValue = ref<{
193 index_pattern: string
194 size: number
195 parameters: Record<string, string | number | boolean>
196 }>({
197 index_pattern: props.asset?.index_name || "wazuh-alerts-*",
198 size: 20,
199 parameters: {}
200 })
201
202 const rules: FormRules = {
203 index_pattern: {
204 required: true,
205 message: "Index pattern is required",
206 trigger: ["blur", "change"]
207 }
208 }
209
210 const isFormValid = computed(() => {
211 if (!formValue.value.index_pattern) return false
212 if (!rule.value) return false
213
214 // Check required parameters
215 if (rule.value?.parameters?.length) {
216 for (const param of rule.value.parameters) {
217 if (param.required) {
218 const value = formValue.value.parameters[param.name]
219 if (value === undefined || value === null || value === "") {
220 return false
221 }
222 }
223 }
224 }
225
226 return true
227 })
228
229 function prefillParameters() {
230 // Initialize parameters with defaults
231 if (rule.value?.parameters?.length) {
232 rule.value.parameters.forEach(param => {
233 if (param.default !== undefined && param.default !== null) {
234 formValue.value.parameters[param.name] = param.default
235 }
236 })
237 }
238
239 // Set default size from rule if available
240 if (rule.value?.search?.size) {
241 formValue.value.size = rule.value.search.size as number
242 }
243 }
244
245 async function loadRule(ruleId: string) {
246 loadingRule.value = true
247
248 try {
249 const res = await Api.copilotSearches.getRuleById(ruleId)
250 if (res.data.success) {
251 rule.value = res.data.rule
252
253 prefillParameters()
254 } else {
255 message.error(res.data?.message || "Failed to load rule details")
256 }
257 } catch (err: any) {
258 message.error(err.response?.data?.message || "Failed to load rule details")
259 } finally {
260 loadingRule.value = false
261 }
262 }
263
264 async function executeSearch() {
265 if (!formRef.value || !rule.value) return
266
267 try {
268 await formRef.value.validate()
269 } catch {
270 return
271 }
272
273 executing.value = true
274 searchResults.value = null
275
276 try {
277 const res = await Api.copilotSearches.executeSearch({
278 rule_id: rule.value.id,
279 index_pattern: formValue.value.index_pattern,
280 parameters: formValue.value.parameters,
281 size: formValue.value.size
282 })
283
284 if (res.data.success) {
285 searchResults.value = res.data
286 message.success(`Search completed: ${res.data.total_hits} hits found`)
287 } else {
288 message.warning(res.data?.message || "Search failed")
289 }
290 } catch (err: any) {
291 message.error(err.response?.data?.message || "Search execution failed")
292 } finally {
293 executing.value = false
294 }
295 }
296
297 onBeforeMount(() => {
298 if (props.ruleDetail) {
299 rule.value = props.ruleDetail
300 prefillParameters()
301 } else if (props.ruleSummary) {
302 loadRule(props.ruleSummary?.id)
303 } else if (props.ruleId) {
304 loadRule(props.ruleId)
305 } else {
306 message.error("No rule data or rule ID provided")
307 }
308 })
309 </script>