| 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> |