main
vue 291 lines 9.07 KB
Raw
1 <template>
2 <div>
3 <div v-if="badge" class="alert-assets-badge" @click="showDetails = true">
4 <code>
5 <span>{{ asset.asset_name }}</span>
6 <Icon :name="ViewIcon" :size="14" />
7 </code>
8 </div>
9 <CardEntity v-else :embedded hoverable clickable @click="showDetails = true">
10 <template #default>{{ asset.asset_name }}</template>
11 <template #mainExtra>
12 <div class="flex flex-wrap items-center gap-3">
13 <Badge type="splitted">
14 <template #label>Index</template>
15 <template #value>
16 <div class="flex h-full items-center">
17 <code
18 class="text-primary cursor-pointer leading-none"
19 @click.stop="routeIndex(asset.index_name).navigate()"
20 >
21 {{ asset.index_name }}
22 <Icon :name="LinkIcon" :size="14" class="relative top-0.5" />
23 </code>
24 </div>
25 </template>
26 </Badge>
27
28 <Badge type="splitted">
29 <template #label>Agent</template>
30 <template #value>
31 <div class="flex h-full items-center">
32 <code
33 class="text-primary cursor-pointer leading-none"
34 @click.stop="routeAgent(asset.agent_id).navigate()"
35 >
36 {{ asset.agent_id }}
37 <Icon :name="LinkIcon" :size="14" class="relative top-0.5" />
38 </code>
39 </div>
40 </template>
41 </Badge>
42 </div>
43 </template>
44 </CardEntity>
45
46 <n-modal
47 v-model:show="showDetails"
48 preset="card"
49 content-class="p-0!"
50 :style="{ maxWidth: 'min(825px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
51 :bordered="false"
52 :title="assetNameTruncated"
53 segmented
54 >
55 <LicenseFeatureCheck
56 feature="SOCFORTRESS AI"
57 @response="
58 (() => {
59 licenseChecked = true
60 licenseResponse = $event
61 })()
62 "
63 />
64 <n-spin :show="!licenseChecked" content-class="flex flex-wrap gap-3 px-6 py-4" :size="18">
65 <AIVelociraptorArtifactRecommendationButton
66 :index-id="asset.index_id"
67 :index-name="asset.index_name"
68 :agent-id="asset.agent_id"
69 :alert-id="asset.alert_linked"
70 :force-license-response="licenseResponse"
71 />
72 <AIWazuhExclusionRuleButton
73 :index-id="asset.index_id"
74 :index-name="asset.index_name"
75 :alert-id="asset.alert_linked"
76 :force-license-response="licenseResponse"
77 />
78 <AIAnalystButton
79 :index-id="asset.index_id"
80 :index-name="asset.index_name"
81 :alert-id="asset.alert_linked"
82 :force-license-response="licenseResponse"
83 />
84 </n-spin>
85
86 <n-divider class="my-0!" />
87
88 <n-tabs type="line" animated :tabs-padding="24">
89 <n-tab-pane name="Info" tab="Info" display-directive="show">
90 <AlertAssetInfo :asset />
91 </n-tab-pane>
92 <n-tab-pane name="Context" tab="Context" display-directive="show">
93 <n-spin :show="loading" class="min-h-40">
94 <div v-if="alertContext" class="p-6 pt-3">
95 <div class="mb-4 flex flex-wrap gap-3">
96 <Badge type="splitted">
97 <template #label>id</template>
98 <template #value>#{{ alertContext.id }}</template>
99 </Badge>
100 <Badge type="splitted">
101 <template #label>source</template>
102 <template #value>
103 {{ alertContext.source }}
104 </template>
105 </Badge>
106 </div>
107
108 <CodeSource :code="alertContext.context" lang="json" />
109 </div>
110 </n-spin>
111 </n-tab-pane>
112 <n-tab-pane
113 v-if="isInvestigationAvailable"
114 name="Investigate"
115 tab="Investigate"
116 display-directive="show:lazy"
117 >
118 <div class="p-6 pt-3">
119 <div class="flex flex-wrap gap-2">
120 <ThreatIntelProcessEvaluationProvider
121 v-for="pn of processNameList"
122 :key="pn"
123 v-slot="{ openEvaluation }"
124 :process-name="pn"
125 >
126 <n-card
127 size="small"
128 embedded
129 class="hover:border-primary cursor-pointer overflow-hidden"
130 @click="openEvaluation()"
131 >
132 {{ pn }}
133 </n-card>
134 </ThreatIntelProcessEvaluationProvider>
135 </div>
136 </div>
137 </n-tab-pane>
138 <n-tab-pane
139 v-if="isWazuhSource"
140 name="CoPilot Searches"
141 tab="CoPilot Searches"
142 display-directive="show:lazy"
143 >
144 <div class="p-6 pt-3">
145 <AlertAssetSearches :asset />
146 </div>
147 </n-tab-pane>
148 <n-tab-pane
149 v-if="isWazuhSource"
150 name="Artifact Collection"
151 tab="Artifact Collection"
152 display-directive="show:lazy"
153 >
154 <div class="p-7 pt-2">
155 <ArtifactsCollect
156 :hostname="asset.asset_name"
157 :artifacts-filter="{ hostname: asset.asset_name }"
158 hide-hostname-field
159 velociraptor-id="string"
160 hide-velociraptor-id-field
161 />
162 </div>
163 </n-tab-pane>
164 <n-tab-pane
165 v-if="isWazuhSource"
166 name="Alert Timeline"
167 tab="Alert Timeline"
168 display-directive="show:lazy"
169 >
170 <div class="p-7 pt-2">
171 <AlertDetailTimeline :asset />
172 </div>
173 </n-tab-pane>
174 <n-tab-pane
175 v-if="isWazuhSource"
176 name="File Collection"
177 tab="File Collection"
178 display-directive="show:lazy"
179 >
180 <div class="p-7 pt-2">
181 <FileCollectionForm v-if="asset.agent_id" :agent-id="asset.agent_id" />
182
183 <n-empty v-else description="No agent associated with this asset" class="h-40" />
184 </div>
185 </n-tab-pane>
186 <n-tab-pane v-if="isWazuhSource" name="Data Store" tab="Data Store" display-directive="show:lazy">
187 <div class="p-7 pt-2">
188 <AgentDataStoreTabCompact v-if="asset.agent_id" :agent-id="asset.agent_id" />
189 <n-empty v-else description="No agent associated with this asset" class="h-40" />
190 </div>
191 </n-tab-pane>
192 </n-tabs>
193 </n-modal>
194 </div>
195 </template>
196
197 <script setup lang="ts">
198 import type { AlertAsset, AlertContext } from "@/types/incidentManagement/alerts.d"
199 import _truncate from "lodash/truncate"
200 import { NCard, NDivider, NEmpty, NModal, NSpin, NTabPane, NTabs, useMessage } from "naive-ui"
201 import { computed, defineAsyncComponent, ref, watch } from "vue"
202 import Api from "@/api"
203 import Badge from "@/components/common/Badge.vue"
204 import CardEntity from "@/components/common/cards/CardEntity.vue"
205 import Icon from "@/components/common/Icon.vue"
206 import { useNavigation } from "@/composables/useNavigation"
207
208 const { asset, embedded, badge } = defineProps<{ asset: AlertAsset; embedded?: boolean; badge?: boolean }>()
209
210 const AlertAssetInfo = defineAsyncComponent(() => import("./AlertAssetInfo.vue"))
211 const AlertDetailTimeline = defineAsyncComponent(() => import("./AlertDetailTimeline.vue"))
212 const AlertAssetSearches = defineAsyncComponent(() => import("@/components/copilotSearches/AlertAssetSearches.vue"))
213 const AIAnalystButton = defineAsyncComponent(() => import("@/components/threatIntel/AIAnalystButton.vue"))
214 const AIWazuhExclusionRuleButton = defineAsyncComponent(
215 () => import("@/components/threatIntel/AIWazuhExclusionRuleButton.vue")
216 )
217 const AIVelociraptorArtifactRecommendationButton = defineAsyncComponent(
218 () => import("@/components/threatIntel/AIVelociraptorArtifactRecommendationButton.vue")
219 )
220 const ThreatIntelProcessEvaluationProvider = defineAsyncComponent(
221 () => import("@/components/threatIntel/ThreatIntelProcessEvaluationProvider.vue")
222 )
223 const ArtifactsCollect = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCollect.vue"))
224 const CodeSource = defineAsyncComponent(() => import("@/components/common/CodeSource.vue"))
225 const LicenseFeatureCheck = defineAsyncComponent(() => import("@/components/license/LicenseFeatureCheck.vue"))
226 const AgentDataStoreTabCompact = defineAsyncComponent(
227 () => import("@/components/agents/dataStore/AgentDataStoreTabCompact.vue")
228 )
229 const FileCollectionForm = defineAsyncComponent(
230 () => import("@/components/agents/fileCollection/FileCollectionForm.vue")
231 )
232
233 const ViewIcon = "iconoir:eye-solid"
234 const LinkIcon = "carbon:launch"
235 const { routeAgent, routeIndex } = useNavigation()
236 const message = useMessage()
237 const loading = ref(false)
238 const showDetails = ref(false)
239 const assetNameTruncated = computed(() => _truncate(asset.asset_name, { length: 50 }))
240 const alertContext = ref<AlertContext | null>(null)
241 const processNameList = computed<string[]>(() => alertContext.value?.context?.process_name || [])
242 const isInvestigationAvailable = computed(() => processNameList.value.length)
243
244 const licenseChecked = ref(false)
245 const licenseResponse = ref(false)
246
247 const isWazuhSource = computed(() => {
248 return alertContext.value?.source?.toLowerCase() === "wazuh"
249 })
250
251 watch(showDetails, val => {
252 if (val && !alertContext.value) {
253 getAlertContext(asset.alert_context_id)
254 }
255 })
256
257 function getAlertContext(alertContextId: number) {
258 loading.value = true
259
260 Api.incidentManagement.alerts
261 .getAlertContext(alertContextId)
262 .then(res => {
263 if (res.data.success) {
264 alertContext.value = res.data?.alert_context || null
265 } else {
266 message.warning(res.data?.message || "An error occurred. Please try again later.")
267 }
268 })
269 .catch(err => {
270 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
271 })
272 .finally(() => {
273 loading.value = false
274 })
275 }
276 </script>
277
278 <style lang="scss" scoped>
279 .alert-assets-badge {
280 color: var(--primary-color);
281 line-height: 1;
282 cursor: pointer;
283
284 code {
285 display: flex;
286 align-items: center;
287 gap: 7px;
288 padding: 2px 5px;
289 }
290 }
291 </style>