main
vue 340 lines 8.56 KB
Raw
1 <template>
2 <div class="patch-tuesday-detail">
3 <!-- Header Section -->
4 <div class="detail-header">
5 <div class="header-row">
6 <PatchTuesdayPriorityBadge :priority="item.prioritization.priority" />
7 <n-tag v-if="item.kev.in_kev" type="error" size="small">
8 <template #icon>
9 <Icon :name="AlertIcon" />
10 </template>
11 Known Exploited
12 </n-tag>
13 <n-tag v-if="item.severity" :type="getSeverityType(item.severity)" size="small">
14 {{ item.severity }}
15 </n-tag>
16 </div>
17 <h2 class="detail-title">{{ item.title || "No title available" }}</h2>
18 </div>
19
20 <n-divider />
21
22 <!-- Scores Section -->
23 <div class="detail-section">
24 <h3 class="section-title">Risk Scores</h3>
25 <div class="scores-grid">
26 <div v-if="item.cvss.base !== null" class="score-card">
27 <span class="score-label">CVSS Base Score</span>
28 <span class="score-value" :class="getCvssClass(item.cvss.base)">
29 {{ item.cvss.base.toFixed(1) }}
30 </span>
31 <span v-if="item.cvss.vector" class="score-detail">{{ item.cvss.vector }}</span>
32 </div>
33 <div v-if="item.epss.score !== null" class="score-card">
34 <span class="score-label">EPSS Score</span>
35 <span class="score-value">{{ (item.epss.score * 100).toFixed(2) }}%</span>
36 <span class="score-detail">Probability of exploitation</span>
37 </div>
38 <div v-if="item.epss.percentile !== null" class="score-card">
39 <span class="score-label">EPSS Percentile</span>
40 <span class="score-value">{{ (item.epss.percentile * 100).toFixed(0) }}%</span>
41 <span class="score-detail">Higher than {{ (item.epss.percentile * 100).toFixed(0) }}% of CVEs</span>
42 </div>
43 </div>
44 </div>
45
46 <n-divider />
47
48 <!-- Prioritization Section -->
49 <div class="detail-section">
50 <h3 class="section-title">Prioritization</h3>
51 <n-alert :type="getPriorityAlertType(item.prioritization.priority)" class="mb-3">
52 <template #header>{{ item.prioritization.suggested_sla }}</template>
53 </n-alert>
54 <div class="reasons-list">
55 <strong>Priority Factors:</strong>
56 <ul>
57 <li v-for="reason in item.prioritization.reason" :key="reason">{{ reason }}</li>
58 </ul>
59 </div>
60 </div>
61
62 <n-divider />
63
64 <!-- Affected Product Section -->
65 <div class="detail-section">
66 <h3 class="section-title">Affected Product</h3>
67 <n-descriptions :column="1" label-placement="left" bordered size="small">
68 <n-descriptions-item label="Product">
69 {{ item.affected.product }}
70 </n-descriptions-item>
71 <n-descriptions-item label="Family">
72 <n-tag size="small">{{ item.affected.family }}</n-tag>
73 </n-descriptions-item>
74 </n-descriptions>
75 </div>
76
77 <!-- KEV Details (if applicable) -->
78 <template v-if="item.kev.in_kev">
79 <n-divider />
80 <div class="detail-section">
81 <h3 class="section-title">
82 <Icon :name="AlertIcon" class="text-error mr-2" />
83 CISA KEV Details
84 </h3>
85 <n-alert type="error" class="mb-3">
86 This vulnerability is in the CISA Known Exploited Vulnerabilities catalog and is actively being
87 exploited in the wild.
88 </n-alert>
89 <n-descriptions :column="1" label-placement="left" bordered size="small">
90 <n-descriptions-item v-if="item.kev.date_added" label="Date Added">
91 {{ formatDate(item.kev.date_added) }}
92 </n-descriptions-item>
93 <n-descriptions-item v-if="item.kev.due_date" label="Remediation Due">
94 <n-tag type="error" size="small">{{ formatDate(item.kev.due_date) }}</n-tag>
95 </n-descriptions-item>
96 <n-descriptions-item v-if="item.kev.required_action" label="Required Action">
97 {{ item.kev.required_action }}
98 </n-descriptions-item>
99 <n-descriptions-item v-if="item.kev.known_ransomware_campaign_use" label="Ransomware Use">
100 {{ item.kev.known_ransomware_campaign_use }}
101 </n-descriptions-item>
102 <n-descriptions-item v-if="item.kev.short_description" label="Description">
103 {{ item.kev.short_description }}
104 </n-descriptions-item>
105 </n-descriptions>
106 </div>
107 </template>
108
109 <n-divider />
110
111 <!-- Remediation Section -->
112 <div class="detail-section">
113 <h3 class="section-title">Remediation</h3>
114 <div v-if="item.remediation.kbs.length > 0" class="kb-list">
115 <strong>Related KB Articles:</strong>
116 <div class="kb-tags">
117 <n-tag v-for="kb in item.remediation.kbs" :key="kb" size="small" class="kb-tag" @click="openKB(kb)">
118 <template #icon>
119 <Icon :name="ExternalLinkIcon" />
120 </template>
121 {{ kb }}
122 </n-tag>
123 </div>
124 </div>
125 <n-empty v-else description="No KB articles available" size="small" />
126 </div>
127
128 <n-divider />
129
130 <!-- Source Section -->
131 <div class="detail-section">
132 <h3 class="section-title">Sources</h3>
133 <div class="source-links">
134 <n-button text tag="a" :href="item.source.msrc_cvrf_url" target="_blank" type="primary">
135 <template #icon>
136 <Icon :name="ExternalLinkIcon" />
137 </template>
138 MSRC Security Update
139 </n-button>
140 <n-button
141 text
142 tag="a"
143 :href="`https://nvd.nist.gov/vuln/detail/${item.cve}`"
144 target="_blank"
145 type="primary"
146 >
147 <template #icon>
148 <Icon :name="ExternalLinkIcon" />
149 </template>
150 NVD Entry
151 </n-button>
152 </div>
153 </div>
154
155 <!-- Metadata Footer -->
156 <div class="detail-footer">
157 <span>Cycle: {{ item.cycle }}</span>
158 <span></span>
159 <span>Updated: {{ formatDateTime(item.timestamp_utc) }}</span>
160 </div>
161 </div>
162 </template>
163
164 <script setup lang="ts">
165 // TODO-FE: refactor
166 import type { PatchTuesdayItem } from "@/types/patchTuesday.d"
167 import { NAlert, NButton, NDescriptions, NDescriptionsItem, NDivider, NEmpty, NTag } from "naive-ui"
168 import Icon from "@/components/common/Icon.vue"
169 import { PriorityLevel } from "@/types/patchTuesday.d"
170 import PatchTuesdayPriorityBadge from "./PatchTuesdayPriorityBadge.vue"
171
172 defineProps<{
173 item: PatchTuesdayItem
174 }>()
175 const AlertIcon = "carbon:warning"
176 const ExternalLinkIcon = "carbon:launch"
177
178 function getSeverityType(severity: string): "error" | "warning" | "info" | "default" {
179 const s = severity.toLowerCase()
180 if (s === "critical") return "error"
181 if (s === "important") return "warning"
182 if (s === "moderate") return "info"
183 return "default"
184 }
185
186 function getCvssClass(score: number): string {
187 if (score >= 9.0) return "critical"
188 if (score >= 7.0) return "high"
189 if (score >= 4.0) return "medium"
190 return "low"
191 }
192
193 function getPriorityAlertType(priority: PriorityLevel | string): "error" | "warning" | "info" | "success" {
194 switch (priority) {
195 case PriorityLevel.P0:
196 return "error"
197 case PriorityLevel.P1:
198 return "warning"
199 case PriorityLevel.P2:
200 return "info"
201 case PriorityLevel.P3:
202 return "success"
203 default:
204 return "info"
205 }
206 }
207
208 function formatDate(dateStr: string): string {
209 if (!dateStr) return "-"
210 return new Date(dateStr).toLocaleDateString("en-US", {
211 year: "numeric",
212 month: "long",
213 day: "numeric"
214 })
215 }
216
217 function formatDateTime(dateStr: string): string {
218 if (!dateStr) return "-"
219 return new Date(dateStr).toLocaleString()
220 }
221
222 function openKB(kb: string) {
223 window.open(`https://support.microsoft.com/help/${kb.replace("KB", "")}`, "_blank")
224 }
225 </script>
226
227 <style scoped lang="scss">
228 .patch-tuesday-detail {
229 .detail-header {
230 .header-row {
231 display: flex;
232 gap: 8px;
233 margin-bottom: 12px;
234 }
235
236 .detail-title {
237 font-size: 1.1rem;
238 font-weight: 600;
239 line-height: 1.4;
240 }
241 }
242
243 .detail-section {
244 .section-title {
245 font-size: 0.9rem;
246 font-weight: 600;
247 margin-bottom: 12px;
248 display: flex;
249 align-items: center;
250 }
251 }
252
253 .scores-grid {
254 display: grid;
255 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
256 gap: 12px;
257
258 .score-card {
259 background: var(--bg-secondary-color);
260 padding: 12px;
261 border-radius: 8px;
262 display: flex;
263 flex-direction: column;
264
265 .score-label {
266 font-size: 0.75rem;
267 opacity: 0.7;
268 margin-bottom: 4px;
269 }
270
271 .score-value {
272 font-size: 1.25rem;
273 font-weight: 700;
274
275 &.critical {
276 color: #ef4444;
277 }
278 &.high {
279 color: #f97316;
280 }
281 &.medium {
282 color: #eab308;
283 }
284 &.low {
285 color: #22c55e;
286 }
287 }
288
289 .score-detail {
290 font-size: 0.7rem;
291 opacity: 0.6;
292 margin-top: 4px;
293 }
294 }
295 }
296
297 .reasons-list {
298 ul {
299 margin: 8px 0 0 20px;
300 padding: 0;
301
302 li {
303 margin-bottom: 4px;
304 font-size: 0.875rem;
305 }
306 }
307 }
308
309 .kb-tags {
310 display: flex;
311 flex-wrap: wrap;
312 gap: 8px;
313 margin-top: 8px;
314
315 .kb-tag {
316 cursor: pointer;
317
318 &:hover {
319 opacity: 0.8;
320 }
321 }
322 }
323
324 .source-links {
325 display: flex;
326 flex-direction: column;
327 gap: 8px;
328 }
329
330 .detail-footer {
331 margin-top: 24px;
332 padding-top: 12px;
333 border-top: 1px solid var(--border-color);
334 font-size: 0.75rem;
335 opacity: 0.6;
336 display: flex;
337 gap: 8px;
338 }
339 }
340 </style>