From 640184a93fc68b33ab5b5c96357e4dcbadce15d4 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Tue, 2 Sep 2025 15:17:52 -0700 Subject: [PATCH 1/3] Fixed timing issue on refresh function passing --- .../models/sbombastic.rancher.io.registry.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/sbombastic-image-vulnerability-scanner/models/sbombastic.rancher.io.registry.js b/pkg/sbombastic-image-vulnerability-scanner/models/sbombastic.rancher.io.registry.js index be25608..5f27192 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/models/sbombastic.rancher.io.registry.js +++ b/pkg/sbombastic-image-vulnerability-scanner/models/sbombastic.rancher.io.registry.js @@ -46,9 +46,11 @@ export default class Registry extends SteveModel { message: e.message, }, { root: true }); } finally { + if (target.refreshFn instanceof Function) { setTimeout(() => { target.refreshFn(); }, 2000); + } } }, }; From a9eea68d86a74c2eec7b0240896571d6d468146b Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Mon, 8 Sep 2025 10:41:45 -0700 Subject: [PATCH 2/3] Added installation page, Crrected image assessment chart and added the severity filter --- .../assets/img/neuvector-logo.svg | 1 + .../components/ImageDetails.vue | 8 +- .../components/ImageRiskAssessment.vue | 8 +- .../components/StatusDistribution.vue | 8 +- .../components/common/BarChart.vue | 13 +- .../common/SevereVulnerabilitiesItem.vue | 9 +- .../sbombastic-image-vulnerability-scanner.ts | 34 ++- .../data/sbombastic.rancher.io.image.js | 62 ++++- .../l10n/en-us.yaml | 10 + .../ComponentDemo.vue | 173 ------------- .../Dashboard.vue | 236 +++++++++--------- .../ImageOverview.vue | 81 +++++- .../Installation.vue | 186 ++++++++++++++ .../RegistriesConfiguration.vue | 11 +- .../index.vue | 43 ++++ ...stic-image-vulnerability-scanner-routes.ts | 13 +- .../types.ts | 1 + .../types/chart.ts | 62 +++++ .../sbombastic-image-vulnerability-scanner.ts | 5 + .../utils/chart.ts | 101 ++++++++ .../utils/handle-growl.ts | 27 ++ 21 files changed, 745 insertions(+), 347 deletions(-) create mode 100644 pkg/sbombastic-image-vulnerability-scanner/assets/img/neuvector-logo.svg delete mode 100644 pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ComponentDemo.vue create mode 100644 pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/Installation.vue create mode 100644 pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/index.vue create mode 100644 pkg/sbombastic-image-vulnerability-scanner/types/chart.ts create mode 100644 pkg/sbombastic-image-vulnerability-scanner/utils/chart.ts create mode 100644 pkg/sbombastic-image-vulnerability-scanner/utils/handle-growl.ts diff --git a/pkg/sbombastic-image-vulnerability-scanner/assets/img/neuvector-logo.svg b/pkg/sbombastic-image-vulnerability-scanner/assets/img/neuvector-logo.svg new file mode 100644 index 0000000..ac44612 --- /dev/null +++ b/pkg/sbombastic-image-vulnerability-scanner/assets/img/neuvector-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pkg/sbombastic-image-vulnerability-scanner/components/ImageDetails.vue b/pkg/sbombastic-image-vulnerability-scanner/components/ImageDetails.vue index a0583a7..712367a 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/components/ImageDetails.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/components/ImageDetails.vue @@ -241,10 +241,10 @@ import SortableTable from "@shell/components/SortableTable"; import { BadgeState } from '@components/BadgeState'; import Checkbox from '@components/Form/Checkbox'; -import ScoreBadge from '@sbombastic-image-vulnerability-scanner/components/common/ScoreBadge'; -import BarChart from '@sbombastic-image-vulnerability-scanner/components/common/BarChart'; -import { VULNERABILITY_DETAILS_TABLE } from "@sbombastic-image-vulnerability-scanner/config/table-headers"; -import { images } from "@sbombastic-image-vulnerability-scanner/data/sbombastic.rancher.io.image"; +import ScoreBadge from '@pkg/components/common/ScoreBadge'; +import BarChart from '@pkg/components/common/BarChart'; +import { VULNERABILITY_DETAILS_TABLE } from "@pkg/config/table-headers"; +import { images } from "@pkg/data/sbombastic.rancher.io.image"; export default { name: 'ImageDetails', diff --git a/pkg/sbombastic-image-vulnerability-scanner/components/ImageRiskAssessment.vue b/pkg/sbombastic-image-vulnerability-scanner/components/ImageRiskAssessment.vue index 1dd59f8..d373b14 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/components/ImageRiskAssessment.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/components/ImageRiskAssessment.vue @@ -4,7 +4,7 @@ {{ t('imageScanner.images.imageRiskAssessment.title') }}
- +
@@ -21,7 +21,11 @@ chartData: { type: Object, required: true - } + }, + filterFn: { + type: Function, + required: false + }, }, data() {} } diff --git a/pkg/sbombastic-image-vulnerability-scanner/components/StatusDistribution.vue b/pkg/sbombastic-image-vulnerability-scanner/components/StatusDistribution.vue index d0ac8ed..ac87f58 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/components/StatusDistribution.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/components/StatusDistribution.vue @@ -4,7 +4,7 @@ {{ t('imageScanner.registries.StatusDistribution.title') }}
- +
@@ -21,7 +21,11 @@ chartData: { type: Object, required: true - } + }, + filterFn: { + type: Function, + required: false + }, }, data() {} } diff --git a/pkg/sbombastic-image-vulnerability-scanner/components/common/BarChart.vue b/pkg/sbombastic-image-vulnerability-scanner/components/common/BarChart.vue index d7ea353..9ee0d2e 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/components/common/BarChart.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/components/common/BarChart.vue @@ -6,7 +6,7 @@
-
{{ t(`imageScanner.enum.${ colorPrefix }.${ key.toLowerCase() }`) }}
+
{{ t(`imageScanner.enum.${ colorPrefix }.${ key.toLowerCase() }`) }}
{{ value }}
@@ -34,11 +34,18 @@ export default { colorPrefix: { type: String, required: true - } + }, + filterFn: { + type: Function, + required: false + }, }, methods: { percentage(value) { return this.total > 0 ? (value / this.total) * 100 : 0; + }, + filterByCategory(category) { + this.filterFn && this.filterFn(category); } }, computed: { @@ -102,6 +109,8 @@ export default { width: 80px; align-items: right; gap: 12px; + text-decoration: underline; + cursor: pointer; } .severity-item-bar { diff --git a/pkg/sbombastic-image-vulnerability-scanner/components/common/SevereVulnerabilitiesItem.vue b/pkg/sbombastic-image-vulnerability-scanner/components/common/SevereVulnerabilitiesItem.vue index cb94e94..6f53fac 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/components/common/SevereVulnerabilitiesItem.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/components/common/SevereVulnerabilitiesItem.vue @@ -10,7 +10,7 @@
@@ -32,15 +32,14 @@ type: Object, required: true }, - eventHandler: { - type: Function, - default: null - } }, data() { return { }; }, methods: { + resize(fn) { + window.addEventListener('resize', this.debounce(fn), 500); + }, } } diff --git a/pkg/sbombastic-image-vulnerability-scanner/config/sbombastic-image-vulnerability-scanner.ts b/pkg/sbombastic-image-vulnerability-scanner/config/sbombastic-image-vulnerability-scanner.ts index cefe2a9..f1e5263 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/config/sbombastic-image-vulnerability-scanner.ts +++ b/pkg/sbombastic-image-vulnerability-scanner/config/sbombastic-image-vulnerability-scanner.ts @@ -5,12 +5,29 @@ import { RESOURCE } from "@pkg/types"; -export function init($plugin: IPlugin, store: any) { - const { product, virtualType, basicType } = $plugin.DSL(store, PRODUCT_NAME); +export function init($plugin: any, store: any) { + const { product, virtualType, basicType, weightType } = $plugin.DSL(store, PRODUCT_NAME); product({ - icon: "pod_security", - inStore: "cluster", + icon: "pod_security", + inStore: "cluster", + inExplorer: true, + removeable: false, + showNamespaceFilter: true + }); + + virtualType({ + labelKey: 'imageScanner.dashboard.title', + name: PAGE.DASHBOARD, + namespaced: false, + route: { + name: `c-cluster-${PRODUCT_NAME}-${PAGE.DASHBOARD}`, + params: { + product: PRODUCT_NAME + }, + meta: { pkg: PRODUCT_NAME, product: PRODUCT_NAME } + }, + overview: true }); virtualType({ @@ -91,11 +108,14 @@ export function init($plugin: IPlugin, store: any) { }, }); + weightType(PAGE.DASHBOARD, 98, true); + weightType(PAGE.IMAGE_OVERVIEW, 97, true); + weightType(PAGE.VULNERABILITY_OVERVIEW, 96, true); + basicType([ PAGE.DASHBOARD, - PAGE.IMAGE_OVERVIEW, - PAGE.VULNERABILITY_OVERVIEW, - //"demo" + PAGE.IMAGE_OVERVIEW, + PAGE.VULNERABILITY_OVERVIEW, ]); // Prepend spaces on group name, as Rancher 2.12 render group name algin with sidemenu basicType([PAGE.REGISTRIES, PAGE.VEX_MANAGEMENT, RESOURCE.VEX_HUB], '    Advanced'); diff --git a/pkg/sbombastic-image-vulnerability-scanner/data/sbombastic.rancher.io.image.js b/pkg/sbombastic-image-vulnerability-scanner/data/sbombastic.rancher.io.image.js index ebd9380..546b3a6 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/data/sbombastic.rancher.io.image.js +++ b/pkg/sbombastic-image-vulnerability-scanner/data/sbombastic.rancher.io.image.js @@ -5,6 +5,8 @@ export const images = [ name: "struts-attacher:1.0", }, spec: { + isBaseImage: true, + hasAffectedPackages: true, repository: "coredns", registry: "Docker Hub", scanResult: { @@ -22,10 +24,12 @@ export const images = [ name: "imagemagick4.8.5613", }, spec: { + isBaseImage: false, + hasAffectedPackages: true, repository: "demo-cody-protected", registry: "demo.suse-security-ivs.io", scanResult: { - critical: 7, + critical: 0, high: 0, medium: 1028, low: 24, @@ -39,6 +43,8 @@ export const images = [ name: "centos7.7.1908", }, spec: { + isBaseImage: true, + hasAffectedPackages: false, repository: "kube-controller-manager", registry: "ecr.ap-southeast-emea.2", scanResult: { @@ -56,10 +62,12 @@ export const images = [ name: "nginx1.19.10", }, spec: { + isBaseImage: false, + hasAffectedPackages: true, repository: "kube-apiserver", registry: "Docker Hub", scanResult: { - critical: 150, + critical: 0, high: 5, medium: 850, low: 600, @@ -73,6 +81,8 @@ export const images = [ name: "docker-compose:1.29.2", }, spec: { + isBaseImage: true, + hasAffectedPackages: false, repository: "coredns", registry: "Docker Hub", scanResult: { @@ -90,12 +100,14 @@ export const images = [ name: "python3.9.7", }, spec: { + isBaseImage: false, + hasAffectedPackages: true, repository: "flask-app", registry: "pypi.org", scanResult: { - critical: 80, - high: 3, - medium: 1100, + critical: 0, + high: 0, + medium: 0, low: 50, none: 0 } @@ -107,6 +119,8 @@ export const images = [ name: "nodejs14.17.3", }, spec: { + isBaseImage: true, + hasAffectedPackages: true, repository: "web-server", registry: "npmjs.com", scanResult: { @@ -124,10 +138,12 @@ export const images = [ name: "redis5.0.7", }, spec: { + isBaseImage: false, + hasAffectedPackages: false, repository: "cache-service", registry: "Docker Hub", scanResult: { - critical: 58, + critical: 0, high: 0, medium: 700, low: 0, @@ -141,6 +157,8 @@ export const images = [ name: "mongodb4.4.1", }, spec: { + isBaseImage: true, + hasAffectedPackages: true, repository: "data-store", registry: "mongodb.com", scanResult: { @@ -158,13 +176,15 @@ export const images = [ name: "golang1.16.5", }, spec: { + isBaseImage: false, + hasAffectedPackages: false, repository: "api-gateway", registry: "demo.suse-security-ivs.io", scanResult: { - critical: 75, - high: 1, - medium: 1200, - low: 10, + critical: 0, + high: 0, + medium: 0, + low: 0, none: 18 } }, @@ -175,10 +195,12 @@ export const images = [ name: "ruby2.7.3", }, spec: { + isBaseImage: true, + hasAffectedPackages: false, repository: "web-application", registry: "rubygems.org", scanResult: { - critical: 60, + critical: 0, high: 1, medium: 500, low: 80, @@ -192,6 +214,8 @@ export const images = [ name: "elasticsearch7.10.0", }, spec: { + isBaseImage: false, + hasAffectedPackages: true, repository: "search-service", registry: "docker.elastic.co", scanResult: { @@ -209,10 +233,12 @@ export const images = [ name: "mysql8.0.25", }, spec: { + isBaseImage: true, + hasAffectedPackages: false, repository: "database-service", registry: "mysql.com", scanResult: { - critical: 70, + critical: 0, high: 0, medium: 850, low: 0, @@ -226,10 +252,12 @@ export const images = [ name: "php8.0.9", }, spec: { + isBaseImage: false, + hasAffectedPackages: true, repository: "web-backend", registry: "php.net", scanResult: { - critical: 55, + critical: 0, high: 0, medium: 400, low: 60, @@ -243,6 +271,8 @@ export const images = [ name: "postgresql13.3", }, spec: { + isBaseImage: true, + hasAffectedPackages: true, repository: "data-service", registry: "docker.io", scanResult: { @@ -260,6 +290,8 @@ export const images = [ name: "terraform1.0.0", }, spec: { + isBaseImage: false, + hasAffectedPackages: false, repository: "infrastructure-as-code", registry: "demo.suse-security-ivs.io", scanResult: { @@ -277,6 +309,8 @@ export const images = [ name: "ansible2.10.5", }, spec: { + isBaseImage: true, + hasAffectedPackages: false, repository: "automation-service", registry: "galaxy.ansible.com", scanResult: { @@ -294,6 +328,8 @@ export const images = [ name: "kafka2.8.0", }, spec: { + isBaseImage: false, + hasAffectedPackages: true, repository: "streaming-service", registry: "confluent.io", scanResult: { diff --git a/pkg/sbombastic-image-vulnerability-scanner/l10n/en-us.yaml b/pkg/sbombastic-image-vulnerability-scanner/l10n/en-us.yaml index 4844319..47e1f82 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/l10n/en-us.yaml +++ b/pkg/sbombastic-image-vulnerability-scanner/l10n/en-us.yaml @@ -30,6 +30,15 @@ imageScanner: title: Most affected images at risk viewAll: View all totalVulns: total vulnerabilities + appInstall: + title: Install SBOMBastic Image Vulnerability Scanner + description: Get a comprehensive view of your container image vulnerabilities and risk assessment across all your registries. + button: Install Image Vulnerability Scanner + reload: Unable to fetch SBOMBastic Helm chart - reload required. + checking: Checking... + versionError: + title: Chart Version not found. + message: Unable to determine the latest stable version of the SBOMBastic chart. Please make sure the Helm repository is configured correctly. registries: title: Registries configuration button: @@ -251,6 +260,7 @@ imageScanner: error: Error at: at schedule: Every {i} + reload: Reload typeLabel: sbombastic.rancher.io.registry: Registries configuration diff --git a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ComponentDemo.vue b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ComponentDemo.vue deleted file mode 100644 index 1e13144..0000000 --- a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ComponentDemo.vue +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/Dashboard.vue b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/Dashboard.vue index eaa0b1e..b41452f 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/Dashboard.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/Dashboard.vue @@ -26,7 +26,7 @@ - +
@@ -70,7 +70,7 @@
- +
@@ -252,12 +252,12 @@ export default { // Load registry data await this.$store.dispatch('cluster/findAll', { type: RESOURCE.REGISTRY }); const registries = this.$store.getters['cluster/all']?.(RESOURCE.REGISTRY) || []; - + // Build registry options this.registryOptions = [ { label: 'All registries', value: 'all' } ]; - + registries.forEach(registry => { this.registryOptions.push({ label: registry.metadata?.name || registry.spec?.uri || 'Unknown Registry', @@ -268,7 +268,7 @@ export default { console.error('Error loading registry options:', error); } }, - + async loadDashboardData() { try { // Load vulnerability data from the same source as Vulnerabilities page @@ -281,10 +281,10 @@ export default { totalImages: totalVulnerabilities } })); - + // Process vulnerability statistics from severity distribution this.processVulnerabilityStats(); - + // Set affecting vulnerabilities data for the chart this.affectingVulnerabilitiesData = { critical: 120, @@ -293,69 +293,69 @@ export default { low: 65, none: 200 }; - + // Process scanning statistics await this.processScanningStats(); - + // Load top vulnerabilities with exact data from the image this.topVulnerabilities = [ - { - metadata: { name: 'CVE-2017-5337' }, - spec: { scoreV3: 9.9, impactedImages: 2103, totalImages: 1000 } + { + metadata: { name: 'CVE-2017-5337' }, + spec: { scoreV3: 9.9, impactedImages: 2103, totalImages: 1000 } }, - { - metadata: { name: 'CVE-2018-1000007' }, - spec: { scoreV3: 9.6, impactedImages: 57, totalImages: 1000 } + { + metadata: { name: 'CVE-2018-1000007' }, + spec: { scoreV3: 9.6, impactedImages: 57, totalImages: 1000 } }, - { - metadata: { name: 'CVE-2019-10989' }, - spec: { scoreV2: 8.8, impactedImages: 86, totalImages: 1000 } + { + metadata: { name: 'CVE-2019-10989' }, + spec: { scoreV2: 8.8, impactedImages: 86, totalImages: 1000 } }, - { - metadata: { name: 'CVE-2020-0601' }, - spec: { scoreV3: 8.8, impactedImages: 29, totalImages: 1000 } + { + metadata: { name: 'CVE-2020-0601' }, + spec: { scoreV3: 8.8, impactedImages: 29, totalImages: 1000 } }, - { - metadata: { name: 'CVE-2021-22918' }, - spec: { scoreV3: 8.8, impactedImages: 7, totalImages: 1000 } + { + metadata: { name: 'CVE-2021-22918' }, + spec: { scoreV3: 8.8, impactedImages: 7, totalImages: 1000 } }, - { - metadata: { name: 'CVE-2022-22963' }, - spec: { scoreV3: 9.3, impactedImages: 14, totalImages: 1000 } + { + metadata: { name: 'CVE-2022-22963' }, + spec: { scoreV3: 9.3, impactedImages: 14, totalImages: 1000 } }, - { - metadata: { name: 'CVE-2022-23943' }, - spec: { scoreV3: 8.5, impactedImages: 22, totalImages: 1000 } + { + metadata: { name: 'CVE-2022-23943' }, + spec: { scoreV3: 8.5, impactedImages: 22, totalImages: 1000 } }, - { - metadata: { name: 'CVE-2022-26134' }, - spec: { scoreV3: 7.5, impactedImages: 33, totalImages: 1000 } + { + metadata: { name: 'CVE-2022-26134' }, + spec: { scoreV3: 7.5, impactedImages: 33, totalImages: 1000 } }, - { - metadata: { name: 'CVE-2023-24520' }, - spec: { scoreV3: 9.0, impactedImages: 10, totalImages: 1000 } + { + metadata: { name: 'CVE-2023-24520' }, + spec: { scoreV3: 9.0, impactedImages: 10, totalImages: 1000 } }, - { - metadata: { name: 'CVE-2023-29552' }, - spec: { scoreV3: 8.9, impactedImages: 5, totalImages: 1000 } + { + metadata: { name: 'CVE-2023-29552' }, + spec: { scoreV3: 8.9, impactedImages: 5, totalImages: 1000 } }, - { - metadata: { name: 'CVE-2024-00001' }, - spec: { scoreV3: 8.7, impactedImages: 12, totalImages: 1000 } + { + metadata: { name: 'CVE-2024-00001' }, + spec: { scoreV3: 8.7, impactedImages: 12, totalImages: 1000 } } ]; - + // Load most affected images this.loadMostAffectedImages(); - + // Update last updated time this.lastUpdatedTime = new Date().toLocaleString(); - + } catch (error) { console.error('Error loading dashboard data:', error); } }, - + processVulnerabilityStats() { // Use the same severity distribution data as Vulnerabilities page this.vulnerabilityStats = { @@ -366,17 +366,17 @@ export default { none: this.severityDistribution.none || 0, total: Object.values(this.severityDistribution).reduce((sum, value) => sum + value, 0) }; - + // Calculate affecting vulnerabilities (total minus none) this.vulnerabilityStats.affecting = this.vulnerabilityStats.total - this.vulnerabilityStats.none; }, - + async processScanningStats() { try { // Load scan job data await this.$store.dispatch('cluster/findAll', { type: RESOURCE.SCAN_JOB }); const scanJobs = this.$store.getters['cluster/all']?.(RESOURCE.SCAN_JOB) || []; - + this.scanningStats = { totalImages: scanJobs.length, failedScans: scanJobs.filter(job => job.status?.statusResult?.type === 'Failed').length, @@ -391,8 +391,8 @@ export default { }; } }, - - + + loadMostAffectedImages() { // Mock data for now - in real implementation, this would come from image data this.mostAffectedImages = [ @@ -446,8 +446,8 @@ export default { } ]; }, - - + + async refresh() { this.disabled = true; try { @@ -456,35 +456,35 @@ export default { this.disabled = false; } }, - + viewAllVulnerabilities() { this.$router.push({ name: `c-cluster-${PRODUCT_NAME}-${this.$store.getters['type-map/nameForId']('vulnerability_overview')}` }); }, - + viewAllImages() { this.$router.push({ name: `c-cluster-${PRODUCT_NAME}-${this.$store.getters['type-map/nameForId']('image_overview')}` }); }, - + viewVulnerabilityDetail(cveId) { // Navigate to vulnerability detail page console.log('View vulnerability detail:', cveId); }, - + viewImageDetail(imageName) { // Navigate to image detail page console.log('View image detail:', imageName); }, - + openAddEditRuleModal() { // Download full report functionality console.log('Download full report'); }, - + getSeverityPercentage(severity) { if (this.vulnerabilityStats.total === 0) return 0; return Math.round((this.vulnerabilityStats[severity] / this.vulnerabilityStats.total) * 100); }, - + getSeverityClass(score) { if (score >= 9.0) return 'critical'; if (score >= 7.0) return 'high'; @@ -492,7 +492,7 @@ export default { if (score >= 0.1) return 'low'; return 'none'; }, - + getVulnerabilityBarWidth(vulnerability) { if (!this.topVulnerabilities.length) return 0; const maxImages = Math.max(...this.topVulnerabilities.map(v => v.spec.totalImages)); @@ -517,7 +517,7 @@ export default { justify-content: space-between; align-self: stretch; margin-bottom: 24px; - + .title { display: flex; flex-direction: column; @@ -531,7 +531,7 @@ export default { font-style: normal; font-weight: 600; line-height: 32px; /* 133.333% */ - + .description { color: #717179; font-family: Lato; @@ -540,7 +540,7 @@ export default { line-height: 24px; } } - + .filter-dropdown { display: flex; width: 225px; @@ -565,17 +565,17 @@ export default { border: 1px solid #e1e5e9; border-radius: 0px; box-shadow: none; - + .panel-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 24px 24px 0 24px; - + .header-left { display: flex; align-items: center; - + h3 { margin: 0; color: #141419; @@ -584,7 +584,7 @@ export default { line-height: 24px; } } - + .header-right { .total-count { color: #717179; @@ -593,7 +593,7 @@ export default { line-height: 20px; } } - + .btn-link { background: none; border: none; @@ -602,26 +602,26 @@ export default { font-size: 14px; font-weight: 500; text-decoration: none; - + &:hover { color: #005a8b; text-decoration: underline; } } } - + .panel-content { flex: 1; padding: 24px; display: flex; flex-direction: column; } - + .view-all-inline { padding: 12px 0 0 0; margin-top: 8px; text-align: left; - + .btn-link { background: none; border: none; @@ -630,24 +630,24 @@ export default { font-size: 14px; font-weight: 500; text-decoration: underline; - + &:hover { color: #141419; text-decoration: underline; } } } - + .vulnerability-distribution { flex: 1; display: flex; flex-direction: column; justify-content: center; - + .main-stat { text-align: center; margin-bottom: 32px; - + .main-number { font-size: 48px; font-weight: 700; @@ -655,43 +655,43 @@ export default { line-height: 56px; margin-bottom: 8px; } - + .main-label { font-size: 16px; color: #717179; font-weight: 400; } } - + .severity-breakdown { display: flex; flex-direction: column; gap: 16px; - + .severity-item { display: flex; align-items: center; gap: 16px; - + .severity-label { min-width: 60px; font-size: 14px; font-weight: 500; color: #141419; } - + .severity-bar { flex: 1; height: 8px; background: #f0f0f0; border-radius: 0px; overflow: hidden; - + .bar-fill { height: 100%; border-radius: 0px; transition: width 0.3s ease; - + &.critical { background: #dc2626; } &.high { background: #ea580c; } &.medium { background: #f59e0b; } @@ -699,7 +699,7 @@ export default { &.none { background: #9ca3af; } } } - + .severity-count { min-width: 40px; text-align: right; @@ -710,7 +710,7 @@ export default { } } } - + .scanning-stats { flex: 1; display: flex; @@ -719,29 +719,29 @@ export default { align-items: flex-start; gap: 32px; padding: 24px 0; - + .scan-stat { display: flex; flex-direction: column; gap: 8px; flex: 1; text-align: center; - + .scan-value { font-size: 28px; font-weight: 700; color: #141419; line-height: 36px; } - + &.failed .scan-value { color: #f59e0b; } - + &.error .scan-value { color: #dc2626; } - + .scan-label { font-size: 14px; color: #717179; @@ -750,13 +750,13 @@ export default { } } } - + .vulnerability-list { flex: 1; display: flex; flex-direction: column; gap: 16px; - + .vulnerability-item { display: flex; align-items: center; @@ -764,25 +764,25 @@ export default { padding: 12px 0; cursor: pointer; border-bottom: 1px solid #f0f0f0; - + &:last-child { border-bottom: none; } - + &:hover { background: #f8f9fa; margin: 0 -12px; padding: 12px; border-radius: 0px; } - + .vuln-id { font-size: 14px; font-weight: 600; color: #141419; min-width: 120px; } - + .vuln-score { padding: 4px 8px; border-radius: 0px; @@ -790,21 +790,21 @@ export default { font-weight: 600; min-width: 60px; text-align: center; - + &.critical { background: #fee2e2; color: #dc2626; } &.high { background: #fed7aa; color: #ea580c; } &.medium { background: #fef3c7; color: #f59e0b; } &.low { background: #dbeafe; color: #3b82f6; } &.none { background: #f3f4f6; color: #6b7280; } } - + .vuln-bar { flex: 1; height: 6px; background: #f0f0f0; border-radius: 0px; overflow: hidden; - + .bar-fill { height: 100%; background: #9ca3af; @@ -812,7 +812,7 @@ export default { transition: width 0.3s ease; } } - + .vuln-count { min-width: 40px; text-align: right; @@ -822,13 +822,13 @@ export default { } } } - + .image-list { flex: 1; display: flex; flex-direction: column; gap: 16px; - + .image-item { display: flex; align-items: center; @@ -836,18 +836,18 @@ export default { padding: 12px 0; cursor: pointer; border-bottom: 1px solid #f0f0f0; - + &:last-child { border-bottom: none; } - + &:hover { background: #f8f9fa; margin: 0 -12px; padding: 12px; border-radius: 0px; } - + .image-name { font-size: 14px; font-weight: 500; @@ -855,11 +855,11 @@ export default { min-width: 200px; flex: 1; } - + .image-vulnerabilities { display: flex; gap: 4px; - + .vuln-count { padding: 4px 8px; border-radius: 0px; @@ -867,7 +867,7 @@ export default { font-weight: 600; min-width: 24px; text-align: center; - + &.critical { background: #fee2e2; color: #dc2626; } &.high { background: #fed7aa; color: #ea580c; } &.medium { background: #fef3c7; color: #f59e0b; } @@ -892,17 +892,17 @@ export default { background: transparent !important; border: none !important; } - + :deep(.vulnerability-record) { padding: 8px 16px !important; margin-bottom: 4px !important; } - + :deep(.cve) { color: #007cbb !important; text-decoration: none !important; cursor: pointer !important; - + &:hover { color: #005a8b !important; text-decoration: underline !important; @@ -923,12 +923,12 @@ export default { border: none !important; border-radius: 0px; box-shadow: none; - + .title { display: none !important; } } - + :deep(.chart-area) { border-right: none !important; flex: none !important; @@ -941,7 +941,7 @@ export default { border: none !important; border-radius: 0px; box-shadow: none; - + .title { display: none !important; } @@ -961,17 +961,17 @@ export default { .page { padding: 16px; } - + .header-section { flex-direction: column; gap: 16px; - + .header-right { width: 100%; justify-content: space-between; } } - + .summary-panel { padding: 16px; } 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 46fb68c..ce5e865 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 @@ -10,6 +10,7 @@ :options="filterCveOptions" :close-on-select="true" :multiple="false" + @selecting="changeCveFilter" />
@@ -18,6 +19,7 @@ :options="filterImageOptions" :close-on-select="true" :multiple="false" + @selecting="changeImageFilter" />
@@ -35,7 +37,7 @@
- +
!image.spec.isBaseImage); + } else if (this.selectedImageFilter.value === "includeBaseImages" || this.selectedImageFilter === "includeBaseImages") { + filtered = filtered.filter(image => image.spec.isBaseImage); + } + if (this.selectedCveFilter.value === "affectingCvesOnly" || this.selectedCveFilter === "affectingCvesOnly") { + filtered = filtered.filter(image => image.spec.hasAffectedPackages); + } + this.preprocessedDataset = this.preprocessData(filtered); + }, + + changeImageFilter(selectedImageFilter) { + this.selectedImageFilter = selectedImageFilter; + this.applyFilters(); + }, + + changeCveFilter(selectedCveFilter) { + this.selectedCveFilter = selectedCveFilter; + this.applyFilters(); + }, + filterBySeverity(severity) { + this.preprocessedDataset.preprocessedImages = _.cloneDeep(this.preprocessedImagesBak); + if (severity) { + this.preprocessedDataset.preprocessedImages = this.preprocessedDataset.preprocessedImages.filter(image => (image.severity.toLowerCase() === severity.toLowerCase())); + } + }, onSelectionChange(selected) { - console.log("selected", selected) this.selectedRows = selected || []; }, preprocessData(images) { + console.log("images", images); const severityKeys = ['critical', 'high', 'medium', 'low', 'none']; const chartData = {}; const repoMap = new Map(); + const preprocessedImages = []; for (const key of severityKeys) { chartData[key] = 0; @@ -165,10 +211,14 @@ const topRiskyImages = images.map(image => { let repoRec = {}; + let imageSeverity = ""; const mapKey = `${image.spec.repository},${image.spec.registry}`; const currImageScanResult = {}; for (const key of severityKeys) { currImageScanResult[key] = image.spec.scanResult[key]; + if (!imageSeverity) { + imageSeverity = (image.spec.scanResult[key] || 0) > 0 ? key : ""; + } } if (repoMap.has(mapKey)) { const currRepo = repoMap.get(mapKey); @@ -198,8 +248,12 @@ repoMap.set(mapKey, repoRec); } for (const key of severityKeys) { - chartData[key] += image.spec.scanResult[key]; + chartData[key] += imageSeverity === key ? 1 : 0; } + preprocessedImages.push({ + ...image, + severity: imageSeverity || "none", + }); return { imageName: image.metadata.name, cveCnt: image.spec.scanResult, @@ -212,6 +266,7 @@ return 0; }).slice(0, 5); return { + preprocessedImages, topRiskyImages, chartData, rowsByRepo: Array.from(repoMap.values()) diff --git a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/Installation.vue b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/Installation.vue new file mode 100644 index 0000000..5b128ed --- /dev/null +++ b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/Installation.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/RegistriesConfiguration.vue b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/RegistriesConfiguration.vue index 02804a4..413d8aa 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/RegistriesConfiguration.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/RegistriesConfiguration.vue @@ -34,7 +34,7 @@
- +
row.status === status); + this.registryStatusList = this.registryStatusList.filter(item => item.status === status); + this.statusSummary = this.statusSummary[status] || {}; + }, openAddEditRegistry() { this.$router.push({ name: `${ PRODUCT_NAME }-c-cluster-resource-create`, @@ -351,6 +357,7 @@ max-width: 900px; -webkit-box-orient: vertical; -webkit-line-clamp: 1; + line-clamp: 1; align-self: stretch; overflow: hidden; color: #717179; @@ -365,7 +372,7 @@ .state-date-time { overflow: hidden; -webkit-box-orient: vertical; - -webkit-line-clamp: 1; + line-clamp: 1; color: #717179; text-overflow: ellipsis; font-family: Lato; diff --git a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/index.vue b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/index.vue new file mode 100644 index 0000000..6a56d2b --- /dev/null +++ b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/index.vue @@ -0,0 +1,43 @@ + + + diff --git a/pkg/sbombastic-image-vulnerability-scanner/routes/sbombastic-image-vulnerability-scanner-routes.ts b/pkg/sbombastic-image-vulnerability-scanner/routes/sbombastic-image-vulnerability-scanner-routes.ts index 4c123b1..c63f079 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/routes/sbombastic-image-vulnerability-scanner-routes.ts +++ b/pkg/sbombastic-image-vulnerability-scanner/routes/sbombastic-image-vulnerability-scanner-routes.ts @@ -2,17 +2,23 @@ import RegistryDetails from "@pkg/components/RegistryDetails.vue"; import ComponentDemo from "@pkg/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ComponentDemo.vue"; import Dashboard from "@pkg/pages/c/_cluster/sbombastic-image-vulnerability-scanner/Dashboard.vue"; import ImageOverview from "@pkg/pages/c/_cluster/sbombastic-image-vulnerability-scanner/ImageOverview.vue"; -import ImageDetails from "@sbombastic-image-vulnerability-scanner/components/ImageDetails.vue"; +import ImageDetails from "@pkg/components/ImageDetails.vue"; import RegistriesConfiguration from "@pkg/pages/c/_cluster/sbombastic-image-vulnerability-scanner/RegistriesConfiguration.vue"; import Vulnerabilities from "@pkg/pages/c/_cluster/sbombastic-image-vulnerability-scanner/Vulnerabilities.vue"; import CreateResource from "@pkg/pages/c/_cluster/sbombastic-image-vulnerability-scanner/_resource/create.vue"; import ListResource from "@pkg/pages/c/_cluster/sbombastic-image-vulnerability-scanner/_resource/index.vue"; +import Entry from "@pkg/pages/c/_cluster/sbombastic-image-vulnerability-scanner/index.vue"; import { PRODUCT_NAME, PAGE, } from "@pkg/types"; const routes = [ + { + name: `c-cluster-${ PRODUCT_NAME }-${PAGE.DASHBOARD}`, + path: `/c/:cluster/${ PRODUCT_NAME }/${PAGE.DASHBOARD}`, + component: Entry, + }, { name: `c-cluster-${PRODUCT_NAME}-${PAGE.DASHBOARD}`, path: `/c/:cluster/${PRODUCT_NAME}/${PAGE.DASHBOARD}`, @@ -33,11 +39,6 @@ const routes = [ path: `/c/:cluster/${PRODUCT_NAME}/${PAGE.VULNERABILITY_OVERVIEW}`, component: Vulnerabilities, }, - { - name: `c-cluster-${PRODUCT_NAME}-demo`, - path: `/c/:cluster/${PRODUCT_NAME}/demo`, - component: ComponentDemo, - }, { name: `c-cluster-${PRODUCT_NAME}-${PAGE.REGISTRIES}`, path: `/c/:cluster/${PRODUCT_NAME}/${PAGE.REGISTRIES}`, diff --git a/pkg/sbombastic-image-vulnerability-scanner/types.ts b/pkg/sbombastic-image-vulnerability-scanner/types.ts index 0a5813f..c708f09 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/types.ts +++ b/pkg/sbombastic-image-vulnerability-scanner/types.ts @@ -2,3 +2,4 @@ export * from "./types/sbombastic-image-vulnerability-scanner"; export * from "./types/image"; export * from "./types/registry"; export * from "./types/vex"; +export * from "./types/chart"; diff --git a/pkg/sbombastic-image-vulnerability-scanner/types/chart.ts b/pkg/sbombastic-image-vulnerability-scanner/types/chart.ts new file mode 100644 index 0000000..4d00436 --- /dev/null +++ b/pkg/sbombastic-image-vulnerability-scanner/types/chart.ts @@ -0,0 +1,62 @@ +export interface Version { + name: string; + home?: string; + version: string; + description: string; + keywords?: string[]; + maintainers?: [ + { + name?: string; + email?: string; + url?: string; + } + ]; + icon?: string; + apiVersion: string; + appVersion: string; + annotations?: { [key: string]: string }; + kubeVersion: string; + dependencies?: [ + { + name?: string; + version?: string; + repository?: string; + condition?: string; + } + ]; + type: string; + urls?: string[]; + created?: string; + digest?: string; + key?: string; + repoType?: string; + repoName?: string; +} + +export interface Chart { + key: string; + type: string; + id: string; + certified?: string; + sideLabel?: null; + repoType: string; + repoName: string; + repoNameDisplay?: string; + certifiedSort?: number; + icon?: string; + color?: string; + chartType: string; + chartName: string; + chartNameDisplay?: string; + chartDescription?: string; + repoKey?: string; + versions?: Version[]; + categories?: string[]; + deprecated?: boolean; + hidden?: boolean; + targetNamespace?: string; + targetName?: string; + provides?: string[]; + windowsIncompatible?: boolean; + deploysOnWindows?: boolean; +} 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 b15b1fd..93e2c7a 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 @@ -1,4 +1,9 @@ export const PRODUCT_NAME = "imageScanner"; +export const SBOMBASTIC = { + CONTROLLER: "neuvector",//"sbombastic", + SERVICE: "sbombastic-service", + SCHEMA: "sbombastic.rancher.io.registry", +} export const RESOURCE = { REGISTRY: "sbombastic.rancher.io.registry", SCAN_JOB: "sbombastic.rancher.io.scanjob", diff --git a/pkg/sbombastic-image-vulnerability-scanner/utils/chart.ts b/pkg/sbombastic-image-vulnerability-scanner/utils/chart.ts new file mode 100644 index 0000000..02a7b5c --- /dev/null +++ b/pkg/sbombastic-image-vulnerability-scanner/utils/chart.ts @@ -0,0 +1,101 @@ +import { Chart } from "@pkg/types"; +import { handleGrowl } from "@pkg/utils/handle-growl"; +import isEmpty from 'lodash/isEmpty'; +import semver from 'semver'; + +export interface RefreshConfig { + store: any; + chartName: string; + retry?: number; + init?: boolean; +} + +export interface ReloadReady { + reloadReady: boolean; +} + +/** + * Asynchronously refreshes charts by dispatching actions to the store. It attempts to + * find a specific chart by its name and, if not found, dispatches actions to refresh + * the chart catalog. This method will retry the operation up to a maximum of three times + * based on the retry parameter and the presence of the chart. + * + * @param {RefreshConfig} config - The configuration object for the refresh operation. + * @param {any} config.store - The Vuex store instance used for state management. + * @param {string} config.chartName - The name of the chart to be refreshed. + * @param {number} [config.retry=0] - The current retry attempt count. Defaults to 0. + * @param {boolean} [config.init=false] - A flag indicating whether the initial load + * should prevent retries. Defaults to false. + * + * @returns {Promise} An object indicating whether the reload is ready. + * Currently, it always returns an object with `reloadReady` set to false. + * + * @example + * // Example usage: + * refreshCharts({ + * store: myStore, + * chartName: 'myChart', + * retry: 0, + * init: true + * }).then(result => { + * console.log(result.reloadReady); // false + * }); + */ +export async function refreshCharts( + config: RefreshConfig +): Promise { + const { store, chartName, init } = config; + let retry = config.retry ?? 0; + + while (retry < 3) { + const rawCharts = store.getters["catalog/rawCharts"]; + const chart = (Object.values(rawCharts) as Chart[])?.find( + (c) => c?.chartName === chartName + ); + + if (!chart) { + try { + // TODO: Add Custom VueX store for neuvector: { refreshingCharts: false } + // store.dispatch("neuvector/updateRefreshingCharts", true); + await store.dispatch("catalog/refresh"); + } catch (e) { + handleGrowl({ error: e as any, store }); + } finally { + // store.dispatch("neuvector/updateRefreshingCharts", false); + } + + if (retry < 2 && !init) { + retry++; + continue; + } + } + break; + } + + return { reloadReady: false }; +} + +export function getLatestStableVersion(versions: any[]): string | undefined { + const allVersions = versions.map(v => v.version); + const stableVersions = versions.filter(v => !v.version.includes('b')); + + if ( isEmpty(stableVersions) && !isEmpty(allVersions) ) { + return semver.rsort(allVersions)[0]; + } + + return stableVersions?.sort((a, b) => { + const versionA = a.version.split('.').map(Number); + const versionB = b.version.split('.').map(Number); + + for ( let i = 0; i < Math.max(versionA.length, versionB.length); i++ ) { + if ( versionA[i] === undefined || versionA[i] < versionB[i] ) { + return 1; + } + if ( versionB[i] === undefined || versionA[i] > versionB[i] ) { + return -1; + } + } + + return 0; + })[0]; +} diff --git a/pkg/sbombastic-image-vulnerability-scanner/utils/handle-growl.ts b/pkg/sbombastic-image-vulnerability-scanner/utils/handle-growl.ts new file mode 100644 index 0000000..bcd3957 --- /dev/null +++ b/pkg/sbombastic-image-vulnerability-scanner/utils/handle-growl.ts @@ -0,0 +1,27 @@ +export interface GrowlConfig { + error: { + data?: { + _statusText: String; + message: String; + }; + _statusText: String; + message: String; + }; + store?: any; + type?: String; +} + +export function handleGrowl(config: GrowlConfig): void { + const error = config.error?.data || config.error; + const type = config.type || "Error"; + + config.store.dispatch( + `growl/${type.toLowerCase()}`, + { + title: error._statusText || type, + message: error.message, + timeout: 5000, + }, + { root: true } + ); +} From f822a9ac06819725e65d227cd4af158edccd71fd Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Mon, 8 Sep 2025 16:43:42 -0700 Subject: [PATCH 3/3] Integrated installation route with dashbaord. Fixed bugs on some components --- .../components/TopRiskyImagesChart.vue | 15 +++++++++-- .../TopSevereVulnerabilitiesChart.vue | 5 ---- .../common/SevereVulnerabilitiesItem.vue | 3 ++- .../sbombastic-image-vulnerability-scanner.ts | 27 ------------------- .../Dashboard.vue | 11 -------- .../ImageOverview.vue | 2 +- ...stic-image-vulnerability-scanner-routes.ts | 12 --------- 7 files changed, 16 insertions(+), 59 deletions(-) diff --git a/pkg/sbombastic-image-vulnerability-scanner/components/TopRiskyImagesChart.vue b/pkg/sbombastic-image-vulnerability-scanner/components/TopRiskyImagesChart.vue index c7be328..538512e 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/components/TopRiskyImagesChart.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/components/TopRiskyImagesChart.vue @@ -4,14 +4,19 @@ Most affected images at risk
-
{{ image.imageName }}
+ + {{ image.imageName }} +
diff --git a/pkg/sbombastic-image-vulnerability-scanner/components/TopSevereVulnerabilitiesChart.vue b/pkg/sbombastic-image-vulnerability-scanner/components/TopSevereVulnerabilitiesChart.vue index 7813da2..7a8aa49 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/components/TopSevereVulnerabilitiesChart.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/components/TopSevereVulnerabilitiesChart.vue @@ -7,7 +7,6 @@ @@ -26,10 +25,6 @@ type: Array, required: true }, - eventHandler: { - type: Function, - default: null - } }, data() {} } diff --git a/pkg/sbombastic-image-vulnerability-scanner/components/common/SevereVulnerabilitiesItem.vue b/pkg/sbombastic-image-vulnerability-scanner/components/common/SevereVulnerabilitiesItem.vue index 6f53fac..aa75959 100644 --- a/pkg/sbombastic-image-vulnerability-scanner/components/common/SevereVulnerabilitiesItem.vue +++ b/pkg/sbombastic-image-vulnerability-scanner/components/common/SevereVulnerabilitiesItem.vue @@ -20,6 +20,7 @@