main
vue 271 lines 7.05 KB
Raw
1 <template>
2 <div class="vulnerability-card" :class="`severity-${vulnerability.severity}`" @click="showDialog = true">
3 <div class="severity" :severity="vulnerability.severity">
4 {{ vulnerability.severity }}
5 </div>
6 <div class="wrapper">
7 <div class="title">
8 <span>{{ title }}</span>
9 </div>
10 <div class="property">
11 <div v-if="hideTooltip">
12 <Icon :name="ClockIcon" />
13 <span>{{ detectionTime }}</span>
14 </div>
15 <n-tooltip v-else placement="top-start">
16 <span>Detection time</span>
17 <template #trigger>
18 <div>
19 <Icon :name="ClockIcon" />
20 <span>{{ detectionTime }}</span>
21 </div>
22 </template>
23 </n-tooltip>
24 </div>
25 <div class="property">
26 <div v-if="hideTooltip">
27 <Icon :name="CounterIcon" />
28 <span>{{ vulnerability.cve }}</span>
29 </div>
30 <n-tooltip v-else placement="top-start">
31 <span>{{ `CVSS2: ${vulnerability.cvss2_score} - CVSS3: ${vulnerability.cvss3_score}` }}</span>
32 <template #trigger>
33 <div>
34 <Icon :name="CounterIcon" />
35 <span>{{ vulnerability.cve }}</span>
36 </div>
37 </template>
38 </n-tooltip>
39 </div>
40 <div class="property">
41 <div v-if="hideTooltip">
42 <Icon :name="VulnerabilityIcon" />
43 <span>{{ vulnerability.name }}</span>
44 </div>
45 <n-tooltip v-else placement="top-start">
46 <span>{{ `Version: ${vulnerability.version}` }}</span>
47 <template #trigger>
48 <div>
49 <Icon :name="VulnerabilityIcon" />
50 <span>{{ vulnerability.name }}</span>
51 </div>
52 </template>
53 </n-tooltip>
54 </div>
55 </div>
56
57 <n-modal
58 v-model:show="showDialog"
59 preset="card"
60 class="vulnerability-dialog"
61 :title="extract"
62 content-class="p-0!"
63 style="width: 90vw; max-width: 1000px; min-height: min(500px, 90vh)"
64 >
65 <n-tabs type="line" animated :tabs-padding="24">
66 <n-tab-pane name="Details" tab="Details" display-directive="show">
67 <div class="p-6 pt-3 pb-0">
68 <CardKV>
69 <template #key>title</template>
70 <template #value>
71 {{ vulnerability.title ?? "-" }}
72 </template>
73 </CardKV>
74 </div>
75 <div v-if="vulnerabilitySanitized" class="grid-auto-fit-200 grid gap-2 p-7 pt-2">
76 <CardKV v-for="item of vulnerabilitySanitized" :key="item.label">
77 <template #key>
78 {{ item.label }}
79 </template>
80 <template #value>
81 {{ item.value ?? "-" }}
82 </template>
83 </CardKV>
84 </div>
85 </n-tab-pane>
86 <n-tab-pane name="External references" tab="External references" display-directive="show">
87 <div class="p-7 pt-2">
88 <ul>
89 <li v-for="refItem of externalReferences" :key="refItem">
90 <a :href="refItem" target="_blank">{{ refItem }}</a>
91 </li>
92 </ul>
93 </div>
94 </n-tab-pane>
95 <n-tab-pane name="EPSS Score" tab="EPSS Score" display-directive="show:lazy">
96 <div class="p-7 pt-2">
97 <ThreatIntelEpssScore :cve="vulnerability.cve" />
98 </div>
99 </n-tab-pane>
100 </n-tabs>
101 </n-modal>
102 </div>
103 </template>
104
105 <script setup lang="ts">
106 import type { AgentVulnerabilities } from "@/types/agents.d"
107 import { cloneDeep } from "lodash"
108 import _split from "lodash/split"
109 import _truncate from "lodash/truncate"
110 import { NModal, NTabPane, NTabs, NTooltip } from "naive-ui"
111 import { computed, defineAsyncComponent, ref, toRefs } from "vue"
112 import CardKV from "@/components/common/cards/CardKV.vue"
113 import Icon from "@/components/common/Icon.vue"
114 import { useSettingsStore } from "@/stores/settings"
115 import dayjs from "@/utils/dayjs"
116
117 const props = defineProps<{
118 vulnerability: AgentVulnerabilities
119 hideTooltip?: boolean
120 }>()
121
122 const ThreatIntelEpssScore = defineAsyncComponent(() => import("@/components/threatIntel/ThreatIntelEpssScore.vue"))
123
124 const { vulnerability, hideTooltip } = toRefs(props)
125
126 const ClockIcon = "carbon:time"
127 const CounterIcon = "mdi:counter"
128 const VulnerabilityIcon = "bi:radioactive"
129 const dFormats = useSettingsStore().dateFormat
130
131 const title = computed(() => _truncate(vulnerability.value.title, { length: 20 }))
132 const extract = computed(() => _truncate(vulnerability.value.title, { length: 50 }))
133 const externalReferences = computed(() =>
134 (vulnerability.value.external_references || []).map(ref => _split(ref, ",")).flat()
135 )
136
137 const vulnerabilitySanitized = computed(() => {
138 const newObj = []
139 const obj = cloneDeep(vulnerability.value)
140 for (const k in obj) {
141 const prop = obj[k as keyof typeof obj]
142 if (typeof prop === "string") {
143 const maybeTime = dayjs(prop as string | number | Date | null | undefined)
144 if (maybeTime.isValid()) {
145 // @ts-expect-error k is ok
146 obj[k] = maybeTime.format(dFormats.datetime)
147 }
148 }
149
150 if (!["external_references", "id", "title"].includes(k)) {
151 newObj.push({
152 label: k,
153 // @ts-expect-error k is ok
154 value: obj[k]
155 })
156 }
157 }
158 return newObj
159 })
160
161 const detectionTime = computed(() => {
162 const detection_time = dayjs(vulnerability.value.detection_time)
163 if (!detection_time.isValid()) return vulnerability.value.detection_time
164
165 return detection_time.format(dFormats.datetime)
166 })
167
168 const showDialog = ref(false)
169 </script>
170
171 <style lang="scss" scoped>
172 .vulnerability-card {
173 overflow: visible;
174 border: 2px solid var(--bg-body-color);
175 background-color: var(--bg-default-color);
176 max-width: 100%;
177 box-sizing: border-box;
178 transition: all 0.3s;
179 cursor: pointer;
180 position: relative;
181 border-radius: var(--border-radius);
182 padding-block: calc(var(--spacing) * 2);
183
184 .severity {
185 position: absolute;
186 top: -10px;
187 right: -2px;
188 border: 2px solid var(--bg-body-color);
189 border-radius: var(--border-radius);
190 line-height: 1;
191 text-transform: uppercase;
192 font-family: var(--font-family-mono);
193 padding-inline: calc(var(--spacing) * 2);
194 padding-block: calc(var(--spacing) * 0.5);
195 font-size: var(--text-xs);
196 white-space: nowrap;
197 text-overflow: ellipsis;
198 background-color: var(--bg-default-color);
199 transition: all 0.3s;
200
201 &::before {
202 content: "";
203 position: absolute;
204 right: 0;
205 bottom: -2px;
206 z-index: 1;
207 width: calc(100% + 2px);
208 height: 13px;
209 background-color: var(--bg-default-color);
210 transition: all 0.3s;
211 }
212 &::after {
213 content: attr(severity);
214 position: absolute;
215 top: 0;
216 right: 0px;
217 text-align: center;
218 padding-inline: calc(var(--spacing) * 2);
219 padding-block: calc(var(--spacing) * 0.5);
220 font-size: var(--text-xs);
221 z-index: 2;
222 width: 100%;
223 height: 100%;
224 }
225 }
226
227 .wrapper {
228 padding-inline: calc(var(--spacing) * 4);
229 padding-block: calc(var(--spacing) * 3);
230
231 .title {
232 font-weight: bold;
233 line-height: 1.2;
234 margin-bottom: 10px;
235 }
236
237 .property {
238 font-size: 13px;
239 margin-bottom: 2px;
240
241 & > div {
242 display: flex;
243 align-items: center;
244 gap: 6px;
245 }
246 }
247 }
248
249 &.severity-Critical {
250 border-color: var(--error-color);
251 .severity {
252 color: var(--error-color);
253 border-color: var(--error-color);
254 }
255 }
256 &.severity-High {
257 border-color: var(--error-color);
258 .severity {
259 color: var(--error-color);
260 border-color: var(--error-color);
261 }
262 }
263 &.severity-Medium {
264 border-color: var(--warning-color);
265 .severity {
266 color: var(--warning-color);
267 border-color: var(--warning-color);
268 }
269 }
270 }
271 </style>