main
vue 171 lines 5.65 KB
Raw
1 <template>
2 <n-spin :show="loadingDetails" content-class="min-h-40">
3 <div v-if="softwareDetails" class="flex flex-col gap-4 md:flex-row">
4 <div class="flex grow flex-col gap-3">
5 <code class="self-start">
6 {{ softwareDetails.id ?? "" }}
7 </code>
8
9 <CardKV>
10 <template #key>name</template>
11 <template #value>
12 <span class="whitespace-pre-wrap">
13 {{ softwareDetails.name ?? "" }}
14 </span>
15 </template>
16 </CardKV>
17 <CardKV v-if="softwareDetails.description" class="[&_p]:text-white">
18 <template #key>description</template>
19 <template #value>
20 <Markdown :source="softwareDetails.description" />
21 </template>
22 </CardKV>
23
24 <CardKV v-if="softwareDetails.references?.length">
25 <template #key>references</template>
26 <template #value>
27 <References :references="softwareDetails.references" />
28 </template>
29 </CardKV>
30 </div>
31 <div ref="sidebarRef" class="shrink-0 basis-1/3 md:max-w-70">
32 <div ref="sidebarCardRef" class="flex flex-col gap-2 will-change-transform">
33 <n-card content-class="bg-secondary flex flex-col gap-3 rounded-lg" size="small">
34 <div class="flex flex-col gap-0.5 text-sm">
35 <div class="text-secondary font-mono text-xs">external_id</div>
36 <div>{{ softwareDetails.external_id }}</div>
37 </div>
38 <div class="flex flex-col gap-0.5 text-sm">
39 <div class="text-secondary font-mono text-xs">created_time</div>
40 <div>{{ formatDate(softwareDetails.created_time, dFormats.datetime) }}</div>
41 </div>
42 <div class="flex flex-col gap-0.5 text-sm">
43 <div class="text-secondary font-mono text-xs">modified_time</div>
44 <div>{{ formatDate(softwareDetails.modified_time, dFormats.datetime) }}</div>
45 </div>
46 <div class="flex flex-col gap-0.5 text-sm">
47 <div class="text-secondary font-mono text-xs">url</div>
48 <div>
49 <a :href="softwareDetails.url" target="_blank" rel="nofollow noopener noreferrer">
50 {{ softwareDetails.url }}
51 </a>
52 </div>
53 </div>
54 <div class="flex flex-col gap-0.5 text-sm">
55 <div class="text-secondary font-mono text-xs">source</div>
56 <div>{{ softwareDetails.source }}</div>
57 </div>
58 <div class="flex flex-col gap-0.5 text-sm">
59 <div class="text-secondary font-mono text-xs">type</div>
60 <div>{{ softwareDetails.type || "—" }}</div>
61 </div>
62 <div class="flex flex-col gap-0.5 text-sm">
63 <div class="text-secondary font-mono text-xs">mitre_version</div>
64 <div>{{ softwareDetails.mitre_version }}</div>
65 </div>
66 <div class="flex flex-col gap-0.5 text-sm">
67 <div class="text-secondary font-mono text-xs">platforms</div>
68 <div class="mt-0.5 flex flex-wrap gap-1">
69 <template v-if="!softwareDetails.platforms?.length"></template>
70 <template v-else>
71 <code v-for="item of softwareDetails.platforms" :key="item" class="text-xs">
72 {{ item }}
73 </code>
74 </template>
75 </div>
76 </div>
77 <div class="flex flex-col gap-0.5 text-sm">
78 <div class="text-secondary font-mono text-xs">aliases</div>
79 <div class="mt-0.5 flex flex-wrap gap-1">
80 <template v-if="!softwareDetails.aliases?.length"></template>
81 <template v-else>
82 <code v-for="item of softwareDetails.aliases" :key="item" class="text-xs">
83 {{ item }}
84 </code>
85 </template>
86 </div>
87 </div>
88 </n-card>
89
90 <div class="flex flex-wrap gap-1">
91 <Badge v-if="softwareDetails.deprecated" color="primary" class="font-mono text-xs!">
92 <template #value>deprecated</template>
93 </Badge>
94 </div>
95 </div>
96 </div>
97 </div>
98 </n-spin>
99 </template>
100
101 <script setup lang="ts">
102 import type { MitreSoftwareDetails } from "@/types/mitre.d"
103 import { useElementBounding, useRafFn } from "@vueuse/core"
104 import { useMotionProperties } from "@vueuse/motion"
105 import { NCard, NSpin, useMessage } from "naive-ui"
106 import { defineAsyncComponent, onBeforeMount, ref, watch } from "vue"
107 import Api from "@/api"
108 import Badge from "@/components/common/Badge.vue"
109 import CardKV from "@/components/common/cards/CardKV.vue"
110 import { useSettingsStore } from "@/stores/settings"
111 import { formatDate } from "@/utils/format"
112 import References from "../common/References.vue"
113
114 const { externalId, entity } = defineProps<{
115 externalId?: string
116 entity?: MitreSoftwareDetails
117 }>()
118
119 const Markdown = defineAsyncComponent(() => import("@/components/common/Markdown.vue"))
120
121 const dFormats = useSettingsStore().dateFormat
122 const message = useMessage()
123 const loadingDetails = ref(false)
124 const softwareDetails = ref<MitreSoftwareDetails | null>(null)
125
126 const sidebarRef = ref(null)
127 const sidebarCardRef = ref(null)
128 const { top: sidebarTop } = useElementBounding(sidebarRef)
129 const { transform: styleCardTransform } = useMotionProperties(sidebarCardRef)
130
131 const { resume } = useRafFn(
132 () => {
133 const targetY = sidebarTop.value <= 50 ? sidebarTop.value * -1 + 50 : 0
134 styleCardTransform.translateY = `${targetY}px`
135 },
136 { immediate: false }
137 )
138
139 watch(sidebarTop, () => {
140 resume()
141 })
142
143 function getDetails(id: string) {
144 loadingDetails.value = true
145
146 Api.wazuh.mitre
147 .getMitreSoftware({ id })
148 .then(res => {
149 if (res.data.success) {
150 softwareDetails.value = res.data.results?.[0] || null
151 } else {
152 message.warning(res.data?.message || "An error occurred. Please try again later.")
153 }
154 })
155 .catch(err => {
156 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
157 })
158 .finally(() => {
159 loadingDetails.value = false
160 })
161 }
162
163 onBeforeMount(() => {
164 if (externalId) {
165 getDetails(externalId)
166 }
167 if (entity) {
168 softwareDetails.value = entity
169 }
170 })
171 </script>