From b4595ecb4e1d99f13faf33d4ebb83bf14878cd8c Mon Sep 17 00:00:00 2001 From: Rush Kapoor Date: Fri, 26 Sep 2025 22:09:22 -0700 Subject: [PATCH 1/3] feat(images): Replace mock data implementation using `storage.sbombastic.rancher.io.vulnerabilityreport` objects --- package.json | 1 + .../components/common/AmountBarBySeverity.vue | 10 +- .../config/table-headers.ts | 2 +- .../l10n/en-us.yaml | 3 + .../ImageOverview.vue | 129 ++++++++++++------ .../sbombastic-image-vulnerability-scanner.ts | 12 +- yarn.lock | 5 + 7 files changed, 109 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index 664c77c..f660abb 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@kubernetes/client-node": "^1.3.0", "@rancher/components": "^0.3.0-alpha.1", "@rancher/shell": "3.0.5-rc.8", + "file-saver": "^2.0.5", "vue": "^3.5.17", "vue-router": "^4.5.0", "vuex": "^4.1.0" diff --git a/pkg/sbombastic-image-vulnerability-scanner/components/common/AmountBarBySeverity.vue b/pkg/sbombastic-image-vulnerability-scanner/components/common/AmountBarBySeverity.vue index 662ed25..501e4d7 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/components/common/AmountBarBySeverity.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/components/common/AmountBarBySeverity.vue @@ -12,7 +12,7 @@
{{ cveAmount.high }}
{{ cveAmount.medium }}
{{ cveAmount.low }}
-
{{ cveAmount.none }}
+
{{ cveAmount.unknown }}
@@ -44,13 +44,13 @@ }, computed: { pecentages() { - let total = this.cveAmount.critical + this.cveAmount.high + this.cveAmount.medium + this.cveAmount.low + this.cveAmount.none; + let total = this.cveAmount.critical + this.cveAmount.high + this.cveAmount.medium + this.cveAmount.low + this.cveAmount.unknown; return [ this.cveAmount.critical * 100 / total, this.cveAmount.high * 100 / total, - this.cveAmount.medium * 100 / total, + this.cveAmount.medium * 100 / total, this.cveAmount.low * 100 / total, - this.cveAmount.none * 100 / total, + this.cveAmount.unknown * 100 / total, ]; } } @@ -98,7 +98,7 @@ background-color: #FDD835; color: rgba(255, 255, 255, 0.90); } - .badge.none { + .badge.unknown { background-color: #E0E0E0; color: #717179; } diff --git a/pkg/sbombastic-image-vulnerability-scanner/config/table-headers.ts b/pkg/sbombastic-image-vulnerability-scanner/config/table-headers.ts index 0edc523..d290f6c 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/config/table-headers.ts +++ b/pkg/sbombastic-image-vulnerability-scanner/config/table-headers.ts @@ -164,7 +164,7 @@ export const IMAGE_LIST_TABLE = [ "spec.scanResult.high", "spec.scanResult.medium", "spec.scanResult.low", - "spec.scanResult.none", + "spec.scanResult.unknown", ], width: 300, }, diff --git a/pkg/sbombastic-image-vulnerability-scanner/l10n/en-us.yaml b/pkg/sbombastic-image-vulnerability-scanner/l10n/en-us.yaml index 8b25a18..6bff552 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/l10n/en-us.yaml +++ b/pkg/sbombastic-image-vulnerability-scanner/l10n/en-us.yaml @@ -130,6 +130,9 @@ imageScanner: registry: Registry buttons: downloadCustomReport: Download custom report + downloadSbom: Download SBOM + downloadCsv: Image detail report (CSV) + downloadJson: Vulnerability report (JSON) listTable: checkbox: groupByRepo: Group by repository diff --git a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ImageOverview.vue b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ImageOverview.vue index 0b9fe11..886b4e3 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ImageOverview.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ImageOverview.vue @@ -1,6 +1,6 @@ + @@ -99,11 +101,14 @@ import LabeledSelect from "@shell/components/form/LabeledSelect"; import DownloadCustomReport from "@pkg/components/common/DownloadCustomReport"; import TopRiskyImagesChart from "@pkg/components/TopRiskyImagesChart"; - import ImageRiskAssessment from "@pkg/components/ImageRiskAssessment" - import { images } from "@pkg/data/sbombastic.rancher.io.image"; + import ImageRiskAssessment from "@pkg/components/ImageRiskAssessment"; + import ActionMenu from '@shell/components/ActionMenuShell.vue'; import { IMAGE_LIST_TABLE, REPO_BASED_TABLE, REPO_BASED_IMAGE_LIST_TABLE } from "@pkg/config/table-headers"; import { Checkbox } from '@components/Form/Checkbox'; -import { filter } from "lodash"; + import { RESOURCE } from "@pkg/types"; + import { saveAs } from 'file-saver'; + + export default { name: 'imageOverview', components: { @@ -113,6 +118,7 @@ import { filter } from "lodash"; SortableTable, DownloadCustomReport, Checkbox, + ActionMenu }, data() { const filterCveOptions = [ @@ -140,7 +146,7 @@ import { filter } from "lodash"; } ]; return { - rows: images, + rows: [], REPO_BASED_TABLE: REPO_BASED_TABLE, IMAGE_LIST_TABLE: IMAGE_LIST_TABLE, REPO_BASED_IMAGE_LIST_TABLE: REPO_BASED_IMAGE_LIST_TABLE, @@ -158,14 +164,59 @@ import { filter } from "lodash"; selectedRegistry: "Any", } }, - + computed: { + customActions() { + const downloadSbom = { + action: 'downloadSbom', + label: this.t('imageScanner.images.buttons.downloadSbom'), + icon: 'icon-download', + enabled: true, + bulkable: false, + invoke: (_, res) => { + this.downloadSbom(res); + } + }; + const downloadCsv = { + action: 'downloadCsv', + label: this.t('imageScanner.images.buttons.downloadCsv'), + icon: 'icon-download', + enabled: true, + bulkable: false, + invoke: async ({}, res = []) => {} + }; + const downloadJson = { + action: 'downloadJson', + label: this.t('imageScanner.images.buttons.downloadJson'), + icon: 'icon-download', + enabled: true, + bulkable: false, + invoke: async ({}, res = []) => {} + }; + return [ + downloadSbom, + {divider: true}, + downloadCsv, + downloadJson + ]; + } + }, async fetch() { - this.preprocessedDataset = this.preprocessData(this.rows); + await this.$store.dispatch('cluster/findAll', { type: RESOURCE.VULNERABILITY_REPORT }); + const vulReportCRD = this.$store.getters['cluster/all']?.(RESOURCE.VULNERABILITY_REPORT); + this.preprocessData(vulReportCRD); + this.preprocessedDataset = this.preprocessData(vulReportCRD); this.preprocessedImagesBak = _.cloneDeep(this.preprocessedDataset.preprocessedImages); console.log("this.preprocessedDataset", this.preprocessedDataset) }, - methods: { + async downloadSbom(res) { + const target = (res && res.length ? res[0] : null); + console.log("target", target); + const sbom = await this.$store.dispatch('cluster/find', { type: RESOURCE.SBOM, id: target.id }); + const spdxString = JSON.stringify(sbom.spdx, null, 2); + const sbomBlob = new Blob([spdxString], { type: 'application/json;charset=utf-8' }); + saveAs(sbomBlob, `${sbom.metadata.name}-sbom.spdx.json`); + }, applyFilters() { let filtered = _.cloneDeep(this.preprocessedImagesBak); if (this.selectedImageFilter.value === "excludeBaseImages" || this.selectedImageFilter === "excludeBaseImages") { @@ -197,18 +248,28 @@ import { filter } from "lodash"; onSelectionChange(selected) { this.selectedRows = selected || []; }, - preprocessData(images) { - console.log("images", images); - const severityKeys = ['critical', 'high', 'medium', 'low', 'none']; - const chartData = {}; + preprocessData(vulReportCRD) { + this.rows = []; + vulReportCRD.forEach(report => { + this.rows.push({ + id: report.id, + metadata: { + name: `${report.imageMetadata.registry}:${report.imageMetadata.tag}` + }, + spec: { + isBaseImage: true, + hasAffectedPackages: false, + repository: report.imageMetadata.repository, + registry: report.imageMetadata.registryURI, + scanResult: report.report.summary, + } + }); + }); + const severityKeys = ['critical', 'high', 'medium', 'low', 'unknown']; const repoMap = new Map(); const preprocessedImages = []; - for (const key of severityKeys) { - chartData[key] = 0; - } - - const topRiskyImages = images.map(image => { + this.rows.forEach(image => { let repoRec = {}; let imageSeverity = ""; const mapKey = `${image.spec.repository},${image.spec.registry}`; @@ -246,9 +307,6 @@ import { filter } from "lodash"; } repoMap.set(mapKey, repoRec); } - for (const key of severityKeys) { - chartData[key] += imageSeverity === key ? 1 : 0; - } preprocessedImages.push({ ...image, severity: imageSeverity || "none", @@ -257,17 +315,9 @@ import { filter } from "lodash"; imageName: image.metadata.name, cveCnt: image.spec.scanResult, } - }).sort((a, b) => { - for (const key of severityKeys) { - const diff = (b.cveCnt[key] || 0) - (a.cveCnt[key] || 0); - if (diff !== 0) return diff; - } - return 0; - }).slice(0, 5); + }); return { preprocessedImages, - topRiskyImages, - chartData, rowsByRepo: Array.from(repoMap.values()) }; }, @@ -275,9 +325,6 @@ import { filter } from "lodash"; console.log("isGrouped", this.isGrouped); }, }, - - computed: { - }, } diff --git a/pkg/sbombastic-image-vulnerability-scanner/types/sbombastic-image-vulnerability-scanner.ts b/pkg/sbombastic-image-vulnerability-scanner/types/sbombastic-image-vulnerability-scanner.ts index 04500a7..d4a1e68 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/types/sbombastic-image-vulnerability-scanner.ts +++ b/pkg/sbombastic-image-vulnerability-scanner/types/sbombastic-image-vulnerability-scanner.ts @@ -3,12 +3,12 @@ export const SBOMBASTIC = { CONTROLLER: "sbombastic", SERVICE: "sbombastic-service", SCHEMA: "sbombastic.rancher.io.registry", -} +}; export const SBOMBASTIC_REPOS = { - CHARTS: 'https://charts.sbombastic.io', - CHARTS_REPO: 'https://github.com/sbombastic/helm-charts', - CHARTS_REPO_GIT: 'https://github.com/sbombastic/helm-charts.git', - CHARTS_REPO_NAME: 'sbombastic-charts', + CHARTS: "https://charts.sbombastic.io", + CHARTS_REPO: "https://github.com/sbombastic/helm-charts", + CHARTS_REPO_GIT: "https://github.com/sbombastic/helm-charts.git", + CHARTS_REPO_NAME: "sbombastic-charts", }; export const RESOURCE = { REGISTRY: "sbombastic.rancher.io.registry", @@ -31,4 +31,4 @@ export interface MetadataProperty { label?: string; value?: string; tags?: string[]; -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 2aca8e0..b4a557e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8319,6 +8319,11 @@ file-saver@2.0.2: resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.2.tgz#06d6e728a9ea2df2cce2f8d9e84dfcdc338ec17a" integrity sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw== +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + filelist@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" From 990a7b56c1ed26e4cec7020fc2be9de4f7d0af5d Mon Sep 17 00:00:00 2001 From: Rush Kapoor Date: Mon, 29 Sep 2025 18:44:05 -0700 Subject: [PATCH 2/3] feat: add column based filters UI components --- .../l10n/en-us.yaml | 13 +- .../ImageOverview.vue | 133 +++++++++++++++++- 2 files changed, 137 insertions(+), 9 deletions(-) diff --git a/pkg/sbombastic-image-vulnerability-scanner/l10n/en-us.yaml b/pkg/sbombastic-image-vulnerability-scanner/l10n/en-us.yaml index 6bff552..d46e226 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/l10n/en-us.yaml +++ b/pkg/sbombastic-image-vulnerability-scanner/l10n/en-us.yaml @@ -123,11 +123,6 @@ imageScanner: cve: allCves: All identified CVEs affectingCvesOnly: Affecting CVEs only - imageList: - image: Image - severity: severity - repository: repository - registry: Registry buttons: downloadCustomReport: Download custom report downloadSbom: Download SBOM @@ -142,6 +137,14 @@ imageScanner: repository: Repository registry: Registry vulnerabilities: vulnerabilities + filters: + placeholder: + image: Search by name + label: + image: Image + severity: Severity + repository: Repository + registry: Registry imageDetails: status: Status vulnerabilities: Vulnerabilities diff --git a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ImageOverview.vue b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ImageOverview.vue index 886b4e3..3432fcd 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ImageOverview.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ImageOverview.vue @@ -40,9 +40,54 @@ --> +
+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
\ No newline at end of file From 7430051874d013c6e4123a16ebb7d0bff0abe734 Mon Sep 17 00:00:00 2001 From: Rush Kapoor Date: Mon, 29 Sep 2025 19:12:22 -0700 Subject: [PATCH 3/3] fix(Registry Configuration details): Modify Scan History table to keep Steve API reference such that WebSocket updates render automatically --- .../components/RegistryDetailScanTable.vue | 1 + .../components/RegistryDetails.vue | 52 ++++--------------- .../config/table-headers.ts | 15 +++--- .../formatters/ScanErrorCell.vue | 15 +++--- .../formatters/ScanHistorySinceCell.vue | 25 +++++++++ .../formatters/ScanHistoryStatusCell.vue | 25 +++++++++ 6 files changed, 77 insertions(+), 56 deletions(-) create mode 100644 pkg/sbombastic-image-vulnerability-scanner/formatters/ScanHistorySinceCell.vue create mode 100644 pkg/sbombastic-image-vulnerability-scanner/formatters/ScanHistoryStatusCell.vue diff --git a/pkg/sbombastic-image-vulnerability-scanner/components/RegistryDetailScanTable.vue b/pkg/sbombastic-image-vulnerability-scanner/components/RegistryDetailScanTable.vue index 1ca1797..1a791b8 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/components/RegistryDetailScanTable.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/components/RegistryDetailScanTable.vue @@ -29,6 +29,7 @@ :namespaced="false" :row-actions="false" :table-actions="false" + :sub-rows-description="false" :search="false" :headers="headers" :rows="scanHistory" diff --git a/pkg/sbombastic-image-vulnerability-scanner/components/RegistryDetails.vue b/pkg/sbombastic-image-vulnerability-scanner/components/RegistryDetails.vue index 491bf10..9614f66 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/components/RegistryDetails.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/components/RegistryDetails.vue @@ -76,28 +76,22 @@ }, methods: { async loadData(isForceLoading = false) { - await this.$store.dispatch('cluster/find', { type: RESOURCE.REGISTRY, id: `${this.$route.params.ns}/${this.$route.params.id}`, opt: {force: isForceLoading} }); - if (this.$store.getters['cluster/canList'](RESOURCE.SCAN_JOB)) { - await this.$store.dispatch('cluster/findAll', { type: RESOURCE.SCAN_JOB, opt: {force: isForceLoading} }); - } - this.scanHistory = []; - - let registry = this.$store.getters['cluster/byId'](RESOURCE.REGISTRY, `${this.$route.params.ns}/${this.$route.params.id}`); - let scanJobs = this.$store.getters['cluster/all'](RESOURCE.SCAN_JOB).filter((rec) => { - return rec.spec.registry === registry.metadata.name; + this.registry = await this.$store.dispatch('cluster/find', { type: RESOURCE.REGISTRY, id: `${this.$route.params.ns}/${this.$route.params.id}`, opt: {force: isForceLoading} }); + this.scanHistory = (await this.$store.dispatch('cluster/findAll', { type: RESOURCE.SCAN_JOB, opt: {force: isForceLoading} })).filter((rec) => { + return rec.spec.registry === this.registry.metadata.name; }); - this.registryStatus = this.getRegistryStatus(registry); + this.registryStatus = this.scanHistory.sort((a, b) => new Date(b.status?.statusResult?.lastTransitionTime) - new Date(a.status?.statusResult?.lastTransitionTime))[0]?.status?.statusResult?.type.toLowerCase(); this.registryMetadata = [ { type: 'text', label: this.t('imageScanner.registries.configuration.meta.namespace'), - value: registry.metadata.namespace + value: this.registry.metadata.namespace }, { type: 'text', label: this.t('imageScanner.registries.configuration.meta.repositories'), - value: registry.spec.repositories?.length || 0 + value: this.registry.spec.repositories?.length || 0 }, { type: 'text', @@ -106,44 +100,18 @@ { type: 'text', label: this.t('imageScanner.registries.configuration.meta.uri'), - value: registry.spec.uri + value: this.registry.spec.uri }, { type: 'tags', - tags: registry.spec.repositories || [] + tags: this.registry.spec.repositories || [] }, { type: 'text', - value: registry.spec.scanInterval ? - this.t('imageScanner.general.schedule', { i: registry.spec.scanInterval }) : '', + value: this.registry.spec.scanInterval ? + this.t('imageScanner.general.schedule', { i: this.registry.spec.scanInterval }) : '', } ]; - - scanJobs.forEach((rec) => { - this.scanHistory.push({ - ...rec, - progress: rec.status.scannedImagesCount && rec.status.imagesCount ? Math.ceil(rec.status.scannedImagesCount / rec.status.imagesCount * 100) : 0, - status: { - ...rec.status, - statusResult: rec.status?.conditions?.filter(condition => { - return condition.status === "True"; - })[0] || { - type: "Pending", - lastTransitionTime: null, - } - } - }) - }); - this.registry = registry; - this.scanHistory = this.scanHistory.sort((a, b) => new Date(b.status?.statusResult?.lastTransitionTime) - new Date(a.status?.statusResult?.lastTransitionTime)); - this.registryStatus = this.scanHistory[0]?.status?.statusResult?.type.toLowerCase(); - }, - getRegistryStatus(registry) { - if (!registry || !registry.status || !registry.status.conditions || !registry.status.conditions.length) { - return null; - } - let status = registry.status.conditions[0].type.toLowerCase() === "discovering" ? "InProgress" : registry.status.conditions[0].type; - return status.toLowerCase(); }, }, } diff --git a/pkg/sbombastic-image-vulnerability-scanner/config/table-headers.ts b/pkg/sbombastic-image-vulnerability-scanner/config/table-headers.ts index d290f6c..10173da 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/config/table-headers.ts +++ b/pkg/sbombastic-image-vulnerability-scanner/config/table-headers.ts @@ -60,17 +60,17 @@ export const REGISTRY_SCAN_HISTORY_TABLE = [ { name: "status", labelKey: "imageScanner.registries.configuration.scanTable.header.status", - value: "status.statusResult.type", - formatter: "RegistryStatusCellBadge", - sort: "status.statusResult.type", + value: "status", + formatter: "ScanHistoryStatusCell", + sort: "status.conditions[?(@.status=='True')].type", width: 100, }, { name: "since", labelKey: "imageScanner.registries.configuration.scanTable.header.since", - value: "status.statusResult.lastTransitionTime", - formatter: "Date", - sort: "status.statusResult.lastTransitionTime:desc", + value: "status", + formatter: "ScanHistorySinceCell", + sort: "status.conditions[?(@.status=='True')].lastTransitionTime", width: 210, }, { @@ -100,9 +100,8 @@ export const REGISTRY_SCAN_HISTORY_TABLE = [ { name: "errors", labelKey: "imageScanner.registries.configuration.scanTable.header.error", - value: "status.statusResult.message", + value: "status", formatter: "ScanErrorCell", - sort: "errors", }, ]; diff --git a/pkg/sbombastic-image-vulnerability-scanner/formatters/ScanErrorCell.vue b/pkg/sbombastic-image-vulnerability-scanner/formatters/ScanErrorCell.vue index 444d1a4..6d68c79 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/formatters/ScanErrorCell.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/formatters/ScanErrorCell.vue @@ -12,18 +12,21 @@ export default { required: true } }, - data() { - return { - failedStatus: REGISTRY_STATUS.FAILED + computed: { + error() { + const statusResult = this.value.conditions.find(condition => { + return condition.status === "True"; + }); + return statusResult?.type.toLowerCase() === REGISTRY_STATUS.FAILED ? statusResult.message : null; } - } + }, };