main
vue 268 lines 6.76 KB
Raw
1 <template>
2 <n-tabs type="line" animated :tabs-padding="24">
3 <n-tab-pane name="Overview" tab="Overview" display-directive="show">
4 <div class="flex flex-col gap-6 p-7 pt-2">
5 <div class="grid-auto-fit-200 grid gap-2">
6 <CardKV>
7 <template #key>CVE ID</template>
8 <template #value>
9 {{ vulnerability.cve_id }}
10 </template>
11 </CardKV>
12 <CardKV>
13 <template #key>Severity</template>
14 <template #value>
15 <VulnerabilitySeverityBadge :severity="vulnerability.severity" />
16 </template>
17 </CardKV>
18 <CardKV>
19 <template #key>Title</template>
20 <template #value>
21 {{ vulnerability.title }}
22 </template>
23 </CardKV>
24 <CardKV>
25 <template #key>Agent</template>
26 <template #value>
27 {{ vulnerability.agent_name }}
28 </template>
29 </CardKV>
30 <CardKV v-if="vulnerability.customer_code">
31 <template #key>Customer</template>
32 <template #value>
33 <code
34 class="text-primary cursor-pointer"
35 @click.stop="routeCustomer({ code: vulnerability.customer_code }).navigate()"
36 >
37 #{{ vulnerability.customer_code }}
38 <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
39 </code>
40 </template>
41 </CardKV>
42 </div>
43
44 <n-card
45 v-if="vulnerability.package_name"
46 class="bg-secondary! overflow-hidden"
47 title="Package Information"
48 >
49 <div class="flex flex-wrap justify-between gap-8">
50 <n-statistic label="Package Name" :value="vulnerability.package_name" tabular-nums />
51 <n-statistic label="Version" :value="vulnerability.package_version || '-'" tabular-nums />
52 <n-statistic
53 label="Architecture"
54 :value="vulnerability.package_architecture || '-'"
55 tabular-nums
56 />
57 </div>
58 </n-card>
59
60 <n-card class="bg-secondary! overflow-hidden" title="Scoring">
61 <div class="flex flex-wrap justify-between gap-8">
62 <n-statistic label="CVSS Base Score" :value="`${vulnerability.base_score}`" tabular-nums />
63 <n-statistic
64 label="EPSS Score"
65 :value="parseFloat(vulnerability.epss_score || '0').toFixed(3)"
66 tabular-nums
67 />
68 <n-statistic
69 label="EPSS Percentile"
70 :value="`${parseFloat(vulnerability.epss_percentile || '0').toFixed(1)}%`"
71 tabular-nums
72 />
73 </div>
74 </n-card>
75 </div>
76 </n-tab-pane>
77 <n-tab-pane name="Timeline" tab="Timeline" display-directive="show:lazy">
78 <div class="p-6 pt-3">
79 <n-timeline>
80 <n-timeline-item
81 v-if="vulnerability.published_at"
82 type="success"
83 title="Published at"
84 :time="formatDateTime(vulnerability.published_at)"
85 />
86 <n-timeline-item
87 v-if="vulnerability.detected_at"
88 title="Detected at"
89 :time="formatDateTime(vulnerability.detected_at)"
90 line-type="dashed"
91 />
92 </n-timeline>
93 </div>
94 </n-tab-pane>
95 <n-tab-pane name="References" tab="References" display-directive="show">
96 <div class="p-7 pt-2">
97 <ul v-if="vulnerability.references && parseReferences(vulnerability.references).length">
98 <li v-for="refItem in parseReferences(vulnerability.references)" :key="refItem">
99 <a :href="refItem" target="_blank">{{ refItem }}</a>
100 </li>
101 </ul>
102 <n-empty v-else description="No references available" class="h-48 justify-center" />
103 </div>
104 </n-tab-pane>
105 </n-tabs>
106 </template>
107
108 <script setup lang="ts">
109 // TODO-FE: review VulnerabilityCardContent (this component)
110 import type { VulnerabilitySearchItem } from "@/types/vulnerabilities.d"
111 import { NCard, NEmpty, NStatistic, NTabPane, NTabs, NTimeline, NTimelineItem } from "naive-ui"
112 import CardKV from "@/components/common/cards/CardKV.vue"
113 import Icon from "@/components/common/Icon.vue"
114 import { useNavigation } from "@/composables/useNavigation"
115 import { useSettingsStore } from "@/stores/settings"
116 import { formatDate } from "@/utils/format"
117 import VulnerabilitySeverityBadge from "./VulnerabilitySeverityBadge.vue"
118
119 const { vulnerability } = defineProps<{ vulnerability: VulnerabilitySearchItem }>()
120
121 const dFormats = useSettingsStore().dateFormat
122
123 const { routeCustomer } = useNavigation()
124
125 const LinkIcon = "carbon:launch"
126 const REFS_SPLIT_REGEX = /[,;\n|]/
127 const SPACE_SPLIT_REGEX = /\s+/
128
129 function parseReferences(references: string): string[] {
130 if (!references || references.trim() === "") return []
131
132 // Try to handle different formats:
133 // 1. JSON array string
134 try {
135 const parsed = JSON.parse(references)
136 if (Array.isArray(parsed)) {
137 return parsed.filter(ref => ref && typeof ref === "string" && ref.trim().length > 0)
138 }
139 } catch {
140 // Not JSON, continue with other parsing methods
141 }
142
143 // 2. Comma, semicolon, or newline separated
144 let refs = references
145 .split(REFS_SPLIT_REGEX)
146 .map(ref => ref.trim())
147 .filter(ref => ref.length > 0)
148
149 // 3. Space separated URLs (if they start with http)
150 if (refs.length === 1 && refs?.[0]?.includes("http")) {
151 const spaceRefs = refs[0].split(SPACE_SPLIT_REGEX).filter(ref => ref.startsWith("http"))
152 if (spaceRefs.length > 1) {
153 refs = spaceRefs
154 }
155 }
156
157 return refs
158 }
159
160 function formatDateTime(timestamp: number | Date | string): string {
161 return formatDate(timestamp, dFormats.datetimesec).toString()
162 }
163 </script>
164
165 <style scoped>
166 .section {
167 border-bottom: 1px solid rgb(229 231 235);
168 padding-bottom: 1rem;
169 margin-bottom: 1rem;
170 }
171
172 .section:last-child {
173 border-bottom: none;
174 margin-bottom: 0;
175 }
176
177 .section-title {
178 font-size: 1.125rem;
179 font-weight: 600;
180 margin-bottom: 0.75rem;
181 color: rgb(17 24 39);
182 }
183
184 .detail-item {
185 display: flex;
186 flex-direction: column;
187 gap: 0.25rem;
188 }
189
190 .detail-item label {
191 font-size: 0.875rem;
192 font-weight: 500;
193 color: rgb(75 85 99);
194 }
195
196 .detail-item .value {
197 font-size: 0.875rem;
198 color: rgb(17 24 39);
199 }
200
201 .reference-link {
202 color: rgb(37 99 235);
203 text-decoration: underline;
204 word-break: break-all;
205 }
206
207 .reference-link:hover {
208 color: rgb(29 78 216);
209 }
210
211 /* Dark mode styles */
212 :deep(.dark) .section,
213 .dark .section {
214 border-bottom-color: rgb(55 65 81);
215 }
216
217 :deep(.dark) .section-title,
218 .dark .section-title {
219 color: rgb(243 244 246);
220 }
221
222 :deep(.dark) .detail-item label,
223 .dark .detail-item label {
224 color: rgb(156 163 175);
225 }
226
227 :deep(.dark) .detail-item .value,
228 .dark .detail-item .value {
229 color: rgb(243 244 246);
230 }
231
232 :deep(.dark) .reference-link,
233 .dark .reference-link {
234 color: rgb(96 165 250);
235 }
236
237 :deep(.dark) .reference-link:hover,
238 .dark .reference-link:hover {
239 color: rgb(147 197 253);
240 }
241
242 /* For better compatibility with different dark mode implementations */
243 @media (prefers-color-scheme: dark) {
244 .section {
245 border-bottom-color: rgb(55 65 81);
246 }
247
248 .section-title {
249 color: rgb(243 244 246);
250 }
251
252 .detail-item label {
253 color: rgb(156 163 175);
254 }
255
256 .detail-item .value {
257 color: rgb(243 244 246);
258 }
259
260 .reference-link {
261 color: rgb(96 165 250);
262 }
263
264 .reference-link:hover {
265 color: rgb(147 197 253);
266 }
267 }
268 </style>