@@ -31,7 +31,7 @@ export default {
31
31
PRODUCT_NAME ,
32
32
RESOURCE ,
33
33
PAGE ,
34
- cveDetail: cveDetail ,
34
+ cveDetail: null ,
35
35
rows: images,
36
36
images_table: VULNERABILITIES_DETAIL_IMAGE_LIST_TABLE ,
37
37
group_by_repository_table: VULNERABILITIES_DETAIL_GROUP_BY_REPOSITORY_TABLE ,
@@ -49,12 +49,31 @@ export default {
49
49
}
50
50
},
51
51
52
- async fetch () {
53
- this .preprocessedDataset = this .preprocessData (this .rows );
54
- console .log (" this.preprocessedDataset" , this .preprocessedDataset );
55
- },
56
52
57
53
methods: {
54
+ async loadData () {
55
+ this .preprocessedDataset = this .preprocessData (this .rows );
56
+ console .log (" this.preprocessedDataset" , this .preprocessedDataset );
57
+
58
+ const vulReport = await this .$store .dispatch (' cluster/findAll' , {type: " storage.sbombastic.rancher.io.vulnerabilityreport" })
59
+
60
+ const cveId = this .$route .params .id ;
61
+
62
+ this .affectedImages = [];
63
+
64
+ const {cveMetaData , affectedImages , totalScanned } = this .getCveMetaData (vulReport, cveId);
65
+
66
+ this .affectedImages = affectedImages;
67
+ this .totalScanned = totalScanned;
68
+
69
+ this .cveDetail = {
70
+ ... cveMetaData,
71
+ id: cveId,
72
+ affectedImages: affectedImages .length ,
73
+ totalImages: totalScanned,
74
+ };
75
+ },
76
+
58
77
updateData (event ) {
59
78
console .log (" isGrouped" , this .isGrouped );
60
79
},
@@ -89,7 +108,6 @@ export default {
89
108
},
90
109
showVendorPopup (vendor ) {
91
110
this .selectedVendor = vendor;
92
- this .showPopup = true ;
93
111
},
94
112
95
113
getSeverityColor (severity ) {
@@ -104,7 +122,125 @@ export default {
104
122
return colors[severity] || colors .none ;
105
123
},
106
124
125
+ getCveMetaData (vulReports , cveId ) {
126
+ let affectedImages = [];
127
+ let cveMetaData = null ;
128
+
129
+ vulReports .forEach (report => {
130
+ const { imageMetadata , report: { results = [] } } = report;
131
+
132
+ results .forEach ( result => {
133
+ (result .vulnerabilities || []).forEach ( vuln => {
134
+ if (vuln .cve === cveId) {
135
+ if (! cveMetaData) {
136
+ cveMetaData = {
137
+ score: this .getNvdV3Score (vuln .cvss ),
138
+ sources: this .convertCvssToSources (vuln .cvss ),
139
+ severity: vuln .severity ,
140
+ cvssScores: this .convertCvss (vuln .cvss ),
141
+ description: vuln .title ,
142
+ title: vuln .title ,
143
+ advisoryVendors: this .groupReferencesByDomain (vuln .references ),
144
+ }
145
+ }
146
+ affectedImages .push ({
147
+ image: ` ${ imageMetadata .registryURI } /${ imageMetadata .repository } :${ imageMetadata .tag } ` ,
148
+ repository: imageMetadata .repository ,
149
+ registry: imageMetadata .registry ,
150
+ packageName: vuln .packageName ,
151
+ packageVersion: vuln .installedVersion ,
152
+ fixedVersions: vuln .fixedVersions ,
153
+ severity: vuln .severity ,
154
+ path: result .target ,
155
+ cve: vuln .cve
156
+ });
157
+ }
158
+ })
159
+ })
160
+ });
161
+
162
+
163
+ const totalScanned = vulReports .length ;
164
+
165
+ return {
166
+ cve: cveId,
167
+ cveMetaData,
168
+ affectedImages,
169
+ totalScanned
170
+ };
171
+ },
172
+
173
+ groupReferencesByDomain (urls ){
174
+ const vendorMap = new Map ();
175
+ urls .forEach (url => {
176
+ const { hostname } = new URL (url);
177
+
178
+ // extract domain base (drop subdomains)
179
+ let name = hostname .replace (/ ^ www\. / , ' ' );
180
+ const parts = name .split (' .' );
181
+ if (parts .length > 2 ) {
182
+ name = parts[parts .length - 2 ];
183
+ }else {
184
+ name = parts[0 ];
185
+ }
186
+
187
+ // Capitalize first letter
188
+ name = name .charAt (0 ).toUpperCase () + name .slice (1 );
189
+ if ( ! vendorMap .has (name) ){
190
+ vendorMap .set ( name, []);
191
+ }
192
+ vendorMap .get (name).push (url);
193
+ });
194
+ return Array .from (vendorMap .entries ()).map (
195
+ ([name , references ]) => ({
196
+ name,
197
+ references,
198
+ })
199
+ );
200
+ },
201
+
202
+ convertCvss (cvssObj ) {
203
+ const cvssScores = [];
204
+ Object .entries (cvssObj).forEach (([source , values ]) => {
205
+ Object .entries (values).forEach (([key , val ]) => {
206
+ if (key .toLowerCase ().includes (" score" )) {
207
+ cvssScores .push ({
208
+ source: ` ${ source .charAt (0 ).toUpperCase () + source .slice (1 )} ${ key} ` ,
209
+ score: val
210
+ });
211
+ }
212
+ })
213
+ });
214
+
215
+ return cvssScores;
216
+ },
217
+
218
+ getNvdV3Score (cvss ) {
219
+ if (cvss .nvd && cvss .nvd .v3score ) {
220
+ return ` ${ cvss .nvd .v3score } (v3)` ;
221
+ }
222
+ return " N/A" ;
223
+ },
224
+
225
+ convertCvssToSources (cvss ){
226
+ return Object .keys (cvss).map (key => {
227
+ return {
228
+ name: key .toUpperCase (),
229
+ link: " "
230
+ };
231
+ });
232
+ }
233
+
107
234
},
235
+
236
+ watch: {
237
+ ' $route.params.id' : {
238
+ immediate: true ,
239
+ handler () {
240
+ this .loadData ();
241
+ }
242
+ }
243
+ }
108
244
}
109
245
</script >
110
246
<template >
@@ -117,8 +253,8 @@ export default {
117
253
{{ $route.params.id }}
118
254
</span >
119
255
<BadgeState
120
- :color =" getSeverityColor(cveDetail.severity)"
121
- :label =" t(`imageScanner.enum.cve.${cveDetail.severity}`)"
256
+ :color =" getSeverityColor(cveDetail.severity.toLowerCase() )"
257
+ :label =" t(`imageScanner.enum.cve.${cveDetail.severity.toLowerCase() }`)"
122
258
class =" severity-badge"
123
259
/>
124
260
</div >
@@ -205,8 +341,8 @@ export default {
205
341
:key =" rIndex"
206
342
class =" ref-item"
207
343
>
208
- <a :href =" ref.url " target =" _blank" class =" ref-url" >{{ ref.url }}</a >
209
- <div class =" ref-title" >{{ ref.title }}</div >
344
+ <a :href =" ref" target =" _blank" class =" ref-url" >{{ ref }}</a >
345
+ <!-- <div class="ref-title">{{ ref.title }}</div> -- >
210
346
</div >
211
347
</div >
212
348
</div >
@@ -224,59 +360,59 @@ export default {
224
360
</div >
225
361
</div >
226
362
</div >
227
- <div class =" table" >
228
- <SortableTable
229
- :has-advanced-filtering =" false"
230
- :namespaced =" false"
231
- :row-actions =" false"
232
- :table-actions =" true"
233
- :force-update-live-and-delayed =" 0"
234
- :use-query-params-for-simple-filtering =" true"
235
- :sub-expandable =" isGrouped"
236
- :sub-rows =" isGrouped"
237
- :sub-expand-column =" isGrouped"
238
- :rows =" isGrouped ? preprocessedDataset.rowsByRepo : rows"
239
- :headers =" isGrouped ? group_by_repository_table : images_table"
240
- :key-field =" 'id'"
241
- @selection =" onSelectionChange"
242
- >
243
- <template #header-left >
244
- <div class =" table-top-left" >
245
- <DownloadCustomReport
246
- class =" table-btn"
247
- :selectedRows =" selectedRows"
248
- :buttonName =" t('imageScanner.images.buttons.downloadCustomReport')"
249
- />
250
- </div >
251
- </template >
252
- <template #header-right >
253
- <div class =" table-top-right" >
254
- <Checkbox
255
- style =" margin-top : 8px ; width : 180px ;"
256
- label-key =" imageScanner.images.listTable.checkbox.groupByRepo"
257
- v-model:value =" isGrouped"
258
- @update:value =" updateData($event)"
259
- />
260
- </div >
261
- </template >
262
- <template v-if =" isGrouped " #sub-row =" { row , fullColspan } " >
263
- <tr
264
- class =" sub-row"
265
- >
266
- <td :colspan =" fullColspan" >
267
- <SortableTable
268
- class =" sub-table"
269
- :rows =" row.images"
270
- :headers =" sub_images_table"
271
- :search =" false"
272
- :row-actions =" false"
273
- :table-actions =" false"
274
- />
275
- </td >
276
- </tr >
277
- </template >
278
- </SortableTable >
279
- </div >
363
+ <!-- <div class="table"> -- >
364
+ <!-- <SortableTable-->
365
+ <!-- :has-advanced-filtering="false"-->
366
+ <!-- :namespaced="false"-->
367
+ <!-- :row-actions="false"-->
368
+ <!-- :table-actions="true"-->
369
+ <!-- :force-update-live-and-delayed="0"-->
370
+ <!-- :use-query-params-for-simple-filtering="true"-->
371
+ <!-- :sub-expandable="isGrouped"-->
372
+ <!-- :sub-rows="isGrouped"-->
373
+ <!-- :sub-expand-column="isGrouped"-->
374
+ <!-- :rows="isGrouped ? preprocessedDataset.rowsByRepo : rows"-->
375
+ <!-- :headers="isGrouped ? group_by_repository_table : images_table"-->
376
+ <!-- :key-field="'id'"-->
377
+ <!-- @selection="onSelectionChange"-->
378
+ <!-- > -- >
379
+ <!-- <template #header-left> -- >
380
+ <!-- <div class="table-top-left"> -- >
381
+ <!-- <DownloadCustomReport-->
382
+ <!-- class="table-btn"-->
383
+ <!-- :selectedRows="selectedRows"-->
384
+ <!-- :buttonName="t('imageScanner.images.buttons.downloadCustomReport')"-->
385
+ <!-- /> -- >
386
+ <!-- </div> -- >
387
+ <!-- </template> -- >
388
+ <!-- <template #header-right> -- >
389
+ <!-- <div class="table-top-right"> -- >
390
+ <!-- <Checkbox-->
391
+ <!-- style="margin-top: 8px; width: 180px;"-->
392
+ <!-- label-key="imageScanner.images.listTable.checkbox.groupByRepo"-->
393
+ <!-- v-model:value="isGrouped"-->
394
+ <!-- @update:value="updateData($event)"-->
395
+ <!-- /> -- >
396
+ <!-- </div> -- >
397
+ <!-- </template> -- >
398
+ <!-- <template v-if="isGrouped" #sub-row="{ row, fullColspan }"> -- >
399
+ <!-- <tr-->
400
+ <!-- class="sub-row"-->
401
+ <!-- > -- >
402
+ <!-- <td :colspan="fullColspan"> -- >
403
+ <!-- <SortableTable-->
404
+ <!-- class="sub-table"-->
405
+ <!-- :rows="row.images"-->
406
+ <!-- :headers="sub_images_table"-->
407
+ <!-- :search="false"-->
408
+ <!-- :row-actions="false"-->
409
+ <!-- :table-actions="false"-->
410
+ <!-- /> -- >
411
+ <!-- </td> -- >
412
+ <!-- </tr> -- >
413
+ <!-- </template> -- >
414
+ <!-- </SortableTable> -- >
415
+ <!-- </div> -- >
280
416
</div >
281
417
</template >
282
418
@@ -335,10 +471,10 @@ export default {
335
471
.description {
336
472
display : flex ;
337
473
max-width : 900px ;
338
- height : 21px ;
474
+ max- height : calc ( 21px * 3 ) ;
339
475
flex-direction : column ;
340
476
justify-content : center ;
341
- overflow : hidden ;
477
+ overflow-y : auto ;
342
478
color : #717179 ;
343
479
font-family : Lato;
344
480
font-size : 14px ;
0 commit comments