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