From 81060e61261dea061218a8ea72b1aa6554d5af1d Mon Sep 17 00:00:00 2001 From: xuelichao Date: Tue, 2 Apr 2024 15:27:02 +0800 Subject: [PATCH] [UI] support to stop sbom scanning #20200 Signed-off-by: xuelichao --- .../artifact/artifact-additions/models.ts | 1 + .../artifact-list-page.service.ts | 77 ++++- .../artifact-list-tab.component.html | 62 +++- .../artifact-list-tab.component.scss | 4 + .../artifact-list-tab.component.spec.ts | 133 ++++++- .../artifact-list-tab.component.ts | 145 +++++++- .../repository/artifact/artifact.module.ts | 4 + .../artifact/sbom-scanning/sbom-overview.ts | 39 +++ .../sbom-scanning/sbom-scan-component.html | 38 ++ .../sbom-scanning/sbom-scan.component.spec.ts | 146 ++++++++ .../sbom-scanning/sbom-scan.component.ts | 327 ++++++++++++++++++ .../sbom-tip-histogram.component.html | 78 +++++ .../sbom-tip-histogram.component.scss | 60 ++++ .../sbom-tip-histogram.component.spec.ts | 79 +++++ .../sbom-tip-histogram.component.ts | 108 ++++++ .../artifact/sbom-scanning/scanning.scss | 172 +++++++++ .../result-bar-chart.component.spec.ts | 55 +++ .../result-bar-chart.component.ts | 8 + .../vulnerability-scanning/scanning.scss | 2 +- .../services/event-service/event.service.ts | 3 + ...rtifact-detail-routing-resolver.service.ts | 3 +- .../src/app/shared/entities/shared.const.ts | 5 + .../src/app/shared/services/interface.ts | 10 + .../app/shared/services/permission-static.ts | 7 + src/portal/src/app/shared/units/utils.ts | 15 + src/portal/src/i18n/lang/de-de-lang.json | 37 ++ src/portal/src/i18n/lang/en-us-lang.json | 37 ++ src/portal/src/i18n/lang/es-es-lang.json | 37 ++ src/portal/src/i18n/lang/fr-fr-lang.json | 37 ++ src/portal/src/i18n/lang/ko-kr-lang.json | 37 ++ src/portal/src/i18n/lang/pt-br-lang.json | 37 ++ src/portal/src/i18n/lang/tr-tr-lang.json | 37 ++ src/portal/src/i18n/lang/zh-cn-lang.json | 37 ++ src/portal/src/i18n/lang/zh-tw-lang.json | 37 ++ 34 files changed, 1896 insertions(+), 18 deletions(-) create mode 100644 src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-overview.ts create mode 100644 src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-scan-component.html create mode 100644 src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-scan.component.spec.ts create mode 100644 src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-scan.component.ts create mode 100644 src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.html create mode 100644 src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.scss create mode 100644 src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.spec.ts create mode 100644 src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.ts create mode 100644 src/portal/src/app/base/project/repository/artifact/sbom-scanning/scanning.scss diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-additions/models.ts b/src/portal/src/app/base/project/repository/artifact/artifact-additions/models.ts index 84bf9bf09726..dc7ec0a4557f 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-additions/models.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact-additions/models.ts @@ -18,4 +18,5 @@ export enum ADDITIONS { SUMMARY = 'readme.md', VALUES = 'values.yaml', DEPENDENCIES = 'dependencies', + SBOMS = 'sboms', } diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.service.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.service.ts index 701d4c4532c1..a858938dd5af 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.service.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.service.ts @@ -10,11 +10,15 @@ import { ErrorHandler } from '../../../../../shared/units/error-handler'; @Injectable() export class ArtifactListPageService { private _scanBtnState: ClrLoadingState; + private _sbomBtnState: ClrLoadingState; private _hasEnabledScanner: boolean = false; + private _hasScannerSupportVulnerability: boolean = false; + private _hasScannerSupportSBOM: boolean = false; private _hasAddLabelImagePermission: boolean = false; private _hasRetagImagePermission: boolean = false; private _hasDeleteImagePermission: boolean = false; private _hasScanImagePermission: boolean = false; + private _hasSbomPermission: boolean = false; constructor( private scanningService: ScanningResultService, @@ -26,6 +30,10 @@ export class ArtifactListPageService { return this._scanBtnState; } + getSbomBtnState(): ClrLoadingState { + return this._sbomBtnState; + } + hasEnabledScanner(): boolean { return this._hasEnabledScanner; } @@ -46,14 +54,53 @@ export class ArtifactListPageService { return this._hasScanImagePermission; } + hasSbomPermission(): boolean { + return this._hasSbomPermission; + } + + hasScannerSupportVulnerability(): boolean { + return this._hasScannerSupportVulnerability; + } + + hasScannerSupportSBOM(): boolean { + return this._hasScannerSupportSBOM; + } + init(projectId: number) { this._getProjectScanner(projectId); this._getPermissionRule(projectId); } + updateStates( + enabledScanner: boolean, + scanState?: ClrLoadingState, + sbomState?: ClrLoadingState + ) { + if (scanState) { + this._scanBtnState = scanState; + } + if (sbomState) { + this._sbomBtnState = sbomState; + } + this._hasEnabledScanner = enabledScanner; + } + + updateCapabilities(capabilities?: any) { + if (capabilities) { + if (capabilities?.support_vulnerability !== undefined) { + this._hasScannerSupportVulnerability = + capabilities.support_vulnerability; + } + if (capabilities?.support_sbom !== undefined) { + this._hasScannerSupportSBOM = capabilities.support_sbom; + } + } + } + private _getProjectScanner(projectId: number): void { this._hasEnabledScanner = false; this._scanBtnState = ClrLoadingState.LOADING; + this._sbomBtnState = ClrLoadingState.LOADING; this.scanningService.getProjectScanner(projectId).subscribe( response => { if ( @@ -62,14 +109,28 @@ export class ArtifactListPageService { !response.disabled && response.health === 'healthy' ) { - this._scanBtnState = ClrLoadingState.SUCCESS; - this._hasEnabledScanner = true; + this.updateStates( + true, + ClrLoadingState.SUCCESS, + ClrLoadingState.SUCCESS + ); + if (response?.capabilities) { + this.updateCapabilities(response?.capabilities); + } } else { - this._scanBtnState = ClrLoadingState.ERROR; + this.updateStates( + false, + ClrLoadingState.ERROR, + ClrLoadingState.ERROR + ); } }, error => { - this._scanBtnState = ClrLoadingState.ERROR; + this.updateStates( + false, + ClrLoadingState.ERROR, + ClrLoadingState.ERROR + ); } ); } @@ -94,6 +155,11 @@ export class ArtifactListPageService { action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE .CREATE, }, + { + resource: USERSTATICPERMISSION.REPOSITORY_TAG_SBOM_JOB.KEY, + action: USERSTATICPERMISSION.REPOSITORY_TAG_SBOM_JOB.VALUE + .CREATE, + }, ]; this.userPermissionService .hasProjectPermissions(projectId, permissions) @@ -103,6 +169,9 @@ export class ArtifactListPageService { this._hasRetagImagePermission = results[1]; this._hasDeleteImagePermission = results[2]; this._hasScanImagePermission = results[3]; + this._hasSbomPermission = results?.[4] ?? false; + // TODO need to remove the static code + this._hasSbomPermission = true; }, error => this.errorHandlerService.error(error) ); diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.html b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.html index 6258f7a7d26a..1001da1444d2 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.html +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.html @@ -42,6 +42,34 @@   {{ 'VULNERABILITY.STOP_NOW' | translate }} + + - + + {{ 'REPOSITORY.SBOM' | translate }} + + + + {{ 'ARTIFACT.ANNOTATION' | translate }} - + {{ 'REPOSITORY.LABELS' | translate }} - + {{ 'REPOSITORY.PUSH_TIME' | translate }} - + {{ 'REPOSITORY.PULL_TIME' | translate }} @@ -389,6 +423,26 @@ + +
+ + {{ 'ARTIFACT.SBOM_UNSUPPORTED' | translate }} + + + +
+
diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.scss b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.scss index b90fad67719e..23c95498d4b3 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.scss +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.scss @@ -161,6 +161,10 @@ width: 11rem !important; } +.sbom-column { + width: 6rem !important; +} + .annotations-column { width: 5rem !important; } diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.spec.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.spec.ts index 9eef1097e613..27010f0656d3 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.spec.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.spec.ts @@ -13,7 +13,7 @@ import { ScanningResultDefaultService, ScanningResultService, } from '../../../../../../../shared/services'; -import { ArtifactFront as Artifact } from '../../../artifact'; +import { ArtifactFront as Artifact, ArtifactFront } from '../../../artifact'; import { ErrorHandler } from '../../../../../../../shared/units/error-handler'; import { OperationService } from '../../../../../../../shared/components/operation/operation.service'; import { ArtifactService as NewArtifactService } from '../../../../../../../../../ng-swagger-gen/services/artifact.service'; @@ -24,6 +24,10 @@ import { ArtifactListPageService } from '../../artifact-list-page.service'; import { ClrLoadingState } from '@clr/angular'; import { Accessory } from 'ng-swagger-gen/models/accessory'; import { ArtifactModule } from '../../../artifact.module'; +import { + SBOM_SCAN_STATUS, + VULNERABILITY_SCAN_STATUS, +} from 'src/app/shared/units/utils'; describe('ArtifactListTabComponent', () => { let comp: ArtifactListTabComponent; @@ -171,6 +175,16 @@ describe('ArtifactListTabComponent', () => { pull_time: '0001-01-01T00:00:00Z', }, ]; + const mockAccessory = { + id: 1, + artifact_id: 2, + subject_artifact_id: 3, + subject_artifact_digest: 'fakeDigest', + subject_artifact_repo: 'test', + size: 120, + digest: 'fakeDigest', + type: 'test', + }; const mockErrorHandler = { error: () => {}, }; @@ -236,9 +250,18 @@ describe('ArtifactListTabComponent', () => { getScanBtnState(): ClrLoadingState { return ClrLoadingState.DEFAULT; }, + getSbomBtnState(): ClrLoadingState { + return ClrLoadingState.DEFAULT; + }, hasEnabledScanner(): boolean { return true; }, + hasSbomPermission(): boolean { + return true; + }, + hasScannerSupportSBOM(): boolean { + return true; + }, hasAddLabelImagePermission(): boolean { return true; }, @@ -353,6 +376,27 @@ describe('ArtifactListTabComponent', () => { fixture.nativeElement.querySelector('.confirmation-title') ).toBeTruthy(); }); + it('Generate SBOM button should be disabled', async () => { + await fixture.whenStable(); + comp.selectedRow = [mockArtifacts[1]]; + await stepOpenAction(fixture, comp); + const generatedButton = + fixture.nativeElement.querySelector('#generate-sbom-btn'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(generatedButton.disabled).toBeTruthy(); + }); + it('Stop SBOM button should be disabled', async () => { + await fixture.whenStable(); + comp.selectedRow = [mockArtifacts[1]]; + await stepOpenAction(fixture, comp); + const stopButton = + fixture.nativeElement.querySelector('#stop-sbom-btn'); + fixture.detectChanges(); + await fixture.whenStable().then(() => { + expect(stopButton.disabled).toBeTruthy(); + }); + }); it('the length of hide array should equal to the number of column', async () => { comp.loading = false; fixture.detectChanges(); @@ -360,6 +404,93 @@ describe('ArtifactListTabComponent', () => { const cols = fixture.nativeElement.querySelectorAll('.datagrid-column'); expect(cols.length).toEqual(comp.hiddenArray.length); }); + + it('Test isEllipsisActive', async () => { + fixture = TestBed.createComponent(ArtifactListTabComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable().then(() => { + expect( + comp.isEllipsisActive(document.createElement('span')) + ).toBeFalsy(); + }); + }); + it('Test deleteAccessory', async () => { + fixture = TestBed.createComponent(ArtifactListTabComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + comp.deleteAccessory(mockAccessory); + fixture.detectChanges(); + await fixture.whenStable().then(() => { + expect( + fixture.nativeElement.querySelector('.confirmation-content') + ).toBeTruthy(); + }); + }); + it('Test scanNow', async () => { + fixture = TestBed.createComponent(ArtifactListTabComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + comp.selectedRow = mockArtifacts.slice(0, 1); + comp.scanNow(); + expect(comp.onScanArtifactsLength).toBe(1); + }); + it('Test stopNow', async () => { + fixture = TestBed.createComponent(ArtifactListTabComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + comp.selectedRow = mockArtifacts.slice(0, 1); + comp.stopNow(); + expect(comp.onStopScanArtifactsLength).toBe(1); + }); + it('Test stopSbom', async () => { + fixture = TestBed.createComponent(ArtifactListTabComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + comp.selectedRow = mockArtifacts.slice(0, 1); + comp.stopSbom(); + expect(comp.onStopSbomArtifactsLength).toBe(1); + }); + it('Test tagsString and isRunningState and canStopSbom and canStopScan', async () => { + fixture = TestBed.createComponent(ArtifactListTabComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + expect(comp.tagsString([])).toBeNull(); + expect( + comp.isRunningState(VULNERABILITY_SCAN_STATUS.RUNNING) + ).toBeTruthy(); + expect( + comp.isRunningState(VULNERABILITY_SCAN_STATUS.ERROR) + ).toBeFalsy(); + expect(comp.canStopSbom()).toBeFalsy(); + expect(comp.canStopScan()).toBeFalsy(); + }); + it('Test status and handleScanOverview', async () => { + fixture = TestBed.createComponent(ArtifactListTabComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + expect(comp.scanStatus(mockArtifacts[0])).toBe( + VULNERABILITY_SCAN_STATUS.ERROR + ); + expect(comp.sbomStatus(null)).toBe(SBOM_SCAN_STATUS.NOT_GENERATED_SBOM); + expect(comp.sbomStatus(mockArtifacts[0])).toBe( + SBOM_SCAN_STATUS.NOT_GENERATED_SBOM + ); + expect(comp.handleScanOverview(mockArtifacts[0])).not.toBeNull(); + }); + it('Test utils', async () => { + fixture = TestBed.createComponent(ArtifactListTabComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + expect(comp.selectedRowHasSbom()).toBeFalsy(); + expect(comp.selectedRowHasVul()).toBeFalsy(); + expect(comp.canScanNow()).toBeFalsy(); + expect(comp.hasEnabledSbom()).toBeTruthy(); + expect(comp.canAddLabel()).toBeFalsy(); + }); }); async function stepOpenAction(fixture, comp) { diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.ts index 5d4ffad46958..2c20f6660f9c 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.ts @@ -37,6 +37,7 @@ import { setHiddenArrayToLocalStorage, setPageSizeToLocalStorage, VULNERABILITY_SCAN_STATUS, + SBOM_SCAN_STATUS, } from '../../../../../../../shared/units/utils'; import { ErrorHandler } from '../../../../../../../shared/units/error-handler'; import { ArtifactService } from '../../../artifact.service'; @@ -141,28 +142,60 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { get hasScanImagePermission(): boolean { return this.artifactListPageService.hasScanImagePermission(); } + get hasSbomPermission(): boolean { + return this.artifactListPageService.hasSbomPermission(); + } get hasEnabledScanner(): boolean { return this.artifactListPageService.hasEnabledScanner(); } + get hasScannerSupportVulnerability(): boolean { + return this.artifactListPageService.hasScannerSupportVulnerability(); + } + get hasScannerSupportSBOM(): boolean { + return this.artifactListPageService.hasScannerSupportSBOM(); + } get scanBtnState(): ClrLoadingState { return this.artifactListPageService.getScanBtnState(); } + get generateSbomBtnState(): ClrLoadingState { + return this.artifactListPageService.getSbomBtnState(); + } onSendingScanCommand: boolean; onSendingStopScanCommand: boolean = false; onStopScanArtifactsLength: number = 0; scanStoppedArtifactLength: number = 0; + + onSendingSbomCommand: boolean; + onSendingStopSbomCommand: boolean = false; + onStopSbomArtifactsLength: number = 0; + sbomStoppedArtifactLength: number = 0; + artifactDigest: string; depth: string; // could Pagination filter filters: string[]; scanFinishedArtifactLength: number = 0; onScanArtifactsLength: number = 0; + sbomFinishedArtifactLength: number = 0; + onSbomArtifactsLength: number = 0; stopBtnState: ClrLoadingState = ClrLoadingState.DEFAULT; updateArtifactSub: Subscription; hiddenArray: boolean[] = getHiddenArrayFromLocalStorage( PageSizeMapKeys.ARTIFACT_LIST_TAB_COMPONENT, - [false, false, false, false, false, false, true, false, false, false] + [ + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + ] ); deleteAccessorySub: Subscription; copyDigestSub: Subscription; @@ -203,7 +236,8 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { } } ngOnInit() { - this.registryUrl = this.appConfigService.getConfig().registry_url; + const appConfig = this.appConfigService.getConfig(); + this.registryUrl = appConfig.registry_url; this.initRouterData(); if (!this.updateArtifactSub) { this.updateArtifactSub = this.eventService.subscribe( @@ -250,7 +284,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { this.copyDigestSub.unsubscribe(); this.copyDigestSub = null; } - this.datagrid['columnsService']?.columns?.forEach((item, index) => { + this.datagrid?.['columnsService']?.columns?.forEach((item, index) => { if (this.depth) { this.hiddenArray[index] = !!item?._value?.hidden; } else { @@ -326,6 +360,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { withImmutableStatus: true, withLabel: true, withScanOverview: true, + // withSbomOverview: true, withTag: false, XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES, withAccessory: false, @@ -350,6 +385,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { withImmutableStatus: true, withLabel: true, withScanOverview: true, + // withSbomOverview: true, withTag: false, XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES, @@ -381,7 +417,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { }); this.getArtifactTagsAsync(this.artifactList); this.getAccessoriesAsync(this.artifactList); - this.checkCosignAsync(this.artifactList); + this.checkCosignAndSbomAsync(this.artifactList); this.getIconsFromBackEnd(); }, error => { @@ -420,7 +456,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { this.artifactList = res.body; this.getArtifactTagsAsync(this.artifactList); this.getAccessoriesAsync(this.artifactList); - this.checkCosignAsync(this.artifactList); + this.checkCosignAndSbomAsync(this.artifactList); this.getIconsFromBackEnd(); }, error => { @@ -519,6 +555,14 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { return formatSize(tagSize); } + hasEnabledSbom(): boolean { + return ( + this.hasScannerSupportSBOM && + this.hasEnabledScanner && + this.hasSbomPermission + ); + } + retag() { if (this.selectedRow && this.selectedRow.length && !this.depth) { this.copyArtifactComponent.retag(this.selectedRow[0].digest); @@ -714,6 +758,17 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { } } + // Get sbom status + sbomStatus(artifact: Artifact): string { + if (artifact) { + let so = (artifact).sbom_overview; + if (so && so.scan_status) { + return so.scan_status; + } + } + return SBOM_SCAN_STATUS.NOT_GENERATED_SBOM; + } + // Get vulnerability scanning status scanStatus(artifact: Artifact): string { if (artifact) { @@ -771,6 +826,10 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { ); } + selectedRowHasSbom(): boolean { + return !!(this.selectedRow && this.selectedRow[0]); + } + hasVul(artifact: Artifact): boolean { return !!( artifact && @@ -779,6 +838,14 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { ); } + hasSbom(artifact: Artifact): boolean { + return !!( + artifact && + artifact.addition_links && + artifact.addition_links[ADDITIONS.SBOMS] + ); + } + submitFinish(e: boolean) { this.scanFinishedArtifactLength += 1; // all selected scan action has started @@ -794,9 +861,27 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { this.onSendingScanCommand = e; } } + + submitSbomFinish(e: boolean) { + this.sbomFinishedArtifactLength += 1; + // all selected scan action has started + if (this.sbomFinishedArtifactLength === this.onSbomArtifactsLength) { + this.onSendingSbomCommand = e; + } + } + + submitSbomStopFinish(e: boolean) { + this.sbomStoppedArtifactLength += 1; + // all selected scan action has stopped + if (this.sbomStoppedArtifactLength === this.onStopSbomArtifactsLength) { + this.onSendingSbomCommand = e; + } + } + handleScanOverview(scanOverview: any): any { if (scanOverview) { - return Object.values(scanOverview)[0]; + const keys = Object.keys(scanOverview) ?? []; + return keys.length > 0 ? scanOverview[keys[0]] : null; } return null; } @@ -857,6 +942,11 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { } } + // when finished, remove it from selectedRow + sbomFinished(artifact: Artifact) { + this.scanFinished(artifact); + } + getIconsFromBackEnd() { if (this.artifactList && this.artifactList.length) { this.artifactService.getIconsFromBackEnd(this.artifactList); @@ -929,11 +1019,18 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { }); } } - checkCosignAsync(artifacts: ArtifactFront[]) { + + checkCosignAndSbomAsync(artifacts: ArtifactFront[]) { if (artifacts) { if (artifacts.length) { artifacts.forEach(item => { item.signed = CHECKING; + // const sbomOverview = item?.sbom_overview; + // item.sbomDigest = sbomOverview?.sbom_digest; + // let queryTypes = `${AccessoryType.COSIGN} ${AccessoryType.NOTATION}`; + // if (!item.sbomDigest) { + // queryTypes = `${queryTypes} ${AccessoryType.SBOM}`; + // } this.newArtifactService .listAccessories({ projectName: this.projectName, @@ -979,6 +1076,24 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { return false; } + // return true if all selected rows are in "running" state + canStopSbom(): boolean { + if (this.onSendingStopSbomCommand) { + return false; + } + if (this.selectedRow && this.selectedRow.length) { + let flag: boolean = true; + this.selectedRow.forEach(item => { + const st: string = this.sbomStatus(item); + if (!this.isRunningState(st)) { + flag = false; + } + }); + return flag; + } + return false; + } + isRunningState(state: string): boolean { return ( state === VULNERABILITY_SCAN_STATUS.RUNNING || @@ -1001,6 +1116,22 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { }); } } + + stopSbom() { + if (this.selectedRow && this.selectedRow.length) { + this.sbomStoppedArtifactLength = 0; + this.onStopSbomArtifactsLength = this.selectedRow.length; + this.onSendingStopSbomCommand = true; + this.selectedRow.forEach((data: any) => { + let digest = data.digest; + this.eventService.publish( + HarborEvent.STOP_SBOM_ARTIFACT, + this.repoName + '/' + digest + ); + }); + } + } + tagsString(tags: Tag[]): string { if (tags?.length) { const arr: string[] = []; diff --git a/src/portal/src/app/base/project/repository/artifact/artifact.module.ts b/src/portal/src/app/base/project/repository/artifact/artifact.module.ts index bd4f49961b2c..4ff6da2d4836 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact.module.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact.module.ts @@ -15,6 +15,7 @@ import { ArtifactVulnerabilitiesComponent } from './artifact-additions/artifact- import { ArtifactDefaultService, ArtifactService } from './artifact.service'; import { ArtifactDetailRoutingResolverService } from '../../../../services/routing-resolvers/artifact-detail-routing-resolver.service'; import { ResultBarChartComponent } from './vulnerability-scanning/result-bar-chart.component'; +import { ResultSbomComponent } from './sbom-scanning/sbom-scan.component'; import { ResultTipHistogramComponent } from './vulnerability-scanning/result-tip-histogram/result-tip-histogram.component'; import { HistogramChartComponent } from './vulnerability-scanning/histogram-chart/histogram-chart.component'; import { ArtifactInfoComponent } from './artifact-list-page/artifact-list/artifact-info/artifact-info.component'; @@ -24,6 +25,7 @@ import { CopyArtifactComponent } from './artifact-list-page/artifact-list/artifa import { CopyDigestComponent } from './artifact-list-page/artifact-list/artifact-list-tab/copy-digest/copy-digest.component'; import { ArtifactFilterComponent } from './artifact-list-page/artifact-list/artifact-list-tab/artifact-filter/artifact-filter.component'; import { PullCommandComponent } from './artifact-list-page/artifact-list/artifact-list-tab/pull-command/pull-command.component'; +import { SbomTipHistogramComponent } from './sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component'; const routes: Routes = [ { @@ -80,6 +82,8 @@ const routes: Routes = [ BuildHistoryComponent, ArtifactVulnerabilitiesComponent, ResultBarChartComponent, + ResultSbomComponent, + SbomTipHistogramComponent, ResultTipHistogramComponent, HistogramChartComponent, ArtifactInfoComponent, diff --git a/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-overview.ts b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-overview.ts new file mode 100644 index 000000000000..753a32a206bc --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-overview.ts @@ -0,0 +1,39 @@ +/* tslint:disable */ + +import { Scanner } from 'ng-swagger-gen/models'; + +/** + * The generate SBOM overview information + */ +export interface SBOMOverview { + /** + * id of the native sbom report + */ + report_id?: string; + + /** + * The start time of the scan process that generating report + */ + start_time?: string; + + /** + * The end time of the scan process that generating report + */ + end_time?: string; + + /** + * The status of the generate SBOM process + */ + scan_status?: string; + + /** + * The digest of the generated SBOM accessory + */ + sbom_digest?: string; + + /** + * The seconds spent for generating the report + */ + duration?: number; + scanner?: Scanner; +} diff --git a/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-scan-component.html b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-scan-component.html new file mode 100644 index 000000000000..1b537edd5691 --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-scan-component.html @@ -0,0 +1,38 @@ +
+
+ {{ + 'SBOM.STATE.QUEUED' | translate + }} +
+ +
+
{{ 'SBOM.STATE.SCANNING' | translate }}
+
+
+
+ +
+
+ {{ + 'SBOM.STATE.STOPPED' | translate + }} +
+
+ {{ + 'SBOM.STATE.OTHER_STATUS' | translate + }} +
+
diff --git a/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-scan.component.spec.ts b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-scan.component.spec.ts new file mode 100644 index 000000000000..2d6d34add6c8 --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-scan.component.spec.ts @@ -0,0 +1,146 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ResultSbomComponent } from './sbom-scan.component'; +import { + ScanningResultDefaultService, + ScanningResultService, +} from '../../../../../shared/services'; +import { SBOM_SCAN_STATUS } from '../../../../../shared/units/utils'; +import { SharedTestingModule } from '../../../../../shared/shared.module'; +import { SbomTipHistogramComponent } from './sbom-tip-histogram/sbom-tip-histogram.component'; +import { SBOMOverview } from './sbom-overview'; +import { Subscription } from 'rxjs'; + +describe('ResultSbomComponent (inline template)', () => { + let component: ResultSbomComponent; + let fixture: ComponentFixture; + let mockData: SBOMOverview = { + scan_status: SBOM_SCAN_STATUS.SUCCESS, + end_time: new Date().toUTCString(), + }; + const mockedSbomDigest = + 'sha256:51a41cec9de9d62ee60e206f5a8a615a028a65653e45539990867417cb486285'; + const mockedSbomOverview = { + report_id: '12345', + scan_status: 'Error', + scanner: { + name: 'Trivy', + vendor: 'vm', + version: 'v1.2', + }, + }; + const mockedCloneSbomOverview = { + report_id: '12346', + scan_status: 'Pending', + scanner: { + name: 'Trivy', + vendor: 'vm', + version: 'v1.2', + }, + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SharedTestingModule], + declarations: [ResultSbomComponent, SbomTipHistogramComponent], + providers: [ + { + provide: ScanningResultService, + useValue: ScanningResultDefaultService, + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ResultSbomComponent); + component = fixture.componentInstance; + component.artifactDigest = 'mockTag'; + component.sbomDigest = mockedSbomDigest; + component.sbomOverview = mockData; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); + it('should show "scan stopped" if status is STOPPED', () => { + component.sbomOverview.scan_status = SBOM_SCAN_STATUS.STOPPED; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + let el: HTMLElement = fixture.nativeElement.querySelector('span'); + expect(el).toBeTruthy(); + expect(el.textContent).toEqual('SBOM.STATE.STOPPED'); + }); + }); + + it('should show progress if status is SCANNING', () => { + component.sbomOverview.scan_status = SBOM_SCAN_STATUS.RUNNING; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + + let el: HTMLElement = + fixture.nativeElement.querySelector('.progress'); + expect(el).toBeTruthy(); + }); + }); + + it('should show QUEUED if status is QUEUED', () => { + component.sbomOverview.scan_status = SBOM_SCAN_STATUS.PENDING; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + + let el: HTMLElement = + fixture.nativeElement.querySelector('.bar-state'); + expect(el).toBeTruthy(); + let el2: HTMLElement = el.querySelector('span'); + expect(el2).toBeTruthy(); + expect(el2.textContent).toEqual('SBOM.STATE.QUEUED'); + }); + }); + + it('should show summary bar chart if status is COMPLETED', () => { + component.sbomOverview.scan_status = SBOM_SCAN_STATUS.SUCCESS; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + let el: HTMLElement = fixture.nativeElement.querySelector('a'); + expect(el).not.toBeNull(); + }); + }); + it('Test ResultSbomComponent getScanner', () => { + fixture.detectChanges(); + expect(component.getScanner()).toBeUndefined(); + component.sbomOverview = mockedSbomOverview; + expect(component.getScanner()).toBe(mockedSbomOverview.scanner); + component.projectName = 'test'; + component.repoName = 'ui'; + component.artifactDigest = 'dg'; + expect(component.viewLog()).toBe( + '/api/v2.0/projects/test/repositories/ui/artifacts/dg/scan/12345/log' + ); + component.copyValue(mockedCloneSbomOverview); + expect(component.sbomOverview.report_id).toBe( + mockedCloneSbomOverview.report_id + ); + }); + it('Test ResultSbomComponent status', () => { + component.sbomOverview = mockedSbomOverview; + fixture.detectChanges(); + expect(component.status).toBe(SBOM_SCAN_STATUS.ERROR); + expect(component.completed).toBeFalsy(); + expect(component.queued).toBeFalsy(); + expect(component.generating).toBeFalsy(); + expect(component.stopped).toBeFalsy(); + expect(component.otherStatus).toBeFalsy(); + expect(component.error).toBeTruthy(); + }); + it('Test ResultSbomComponent ngOnDestroy', () => { + component.ngOnDestroy(); + expect(component.stateCheckTimer).toBeUndefined(); + }); +}); diff --git a/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-scan.component.ts b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-scan.component.ts new file mode 100644 index 000000000000..8a7d645987d1 --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-scan.component.ts @@ -0,0 +1,327 @@ +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import { Subscription, timer } from 'rxjs'; +import { finalize } from 'rxjs/operators'; +import { ScannerVo } from '../../../../../shared/services'; +import { ErrorHandler } from '../../../../../shared/units/error-handler'; +import { + clone, + CURRENT_BASE_HREF, + dbEncodeURIComponent, + DEFAULT_SUPPORTED_MIME_TYPES, + SBOM_SCAN_STATUS, +} from '../../../../../shared/units/utils'; +import { ArtifactService } from '../../../../../../../ng-swagger-gen/services/artifact.service'; +import { Artifact } from '../../../../../../../ng-swagger-gen/models/artifact'; +import { + EventService, + HarborEvent, +} from '../../../../../services/event-service/event.service'; +import { ScanService } from '../../../../../../../ng-swagger-gen/services/scan.service'; +import { ScanType } from 'ng-swagger-gen/models'; +import { ScanTypes } from 'src/app/shared/entities/shared.const'; +import { SBOMOverview } from './sbom-overview'; +const STATE_CHECK_INTERVAL: number = 3000; // 3s +const RETRY_TIMES: number = 3; + +@Component({ + selector: 'hbr-sbom-bar', + templateUrl: './sbom-scan-component.html', + styleUrls: ['./scanning.scss'], +}) +export class ResultSbomComponent implements OnInit, OnDestroy { + @Input() inputScanner: ScannerVo; + @Input() repoName: string = ''; + @Input() projectName: string = ''; + @Input() projectId: string = ''; + @Input() artifactDigest: string = ''; + @Input() sbomDigest: string = ''; + @Input() sbomOverview: SBOMOverview; + onSubmitting: boolean = false; + onStopping: boolean = false; + retryCounter: number = 0; + stateCheckTimer: Subscription; + generateSbomSubscription: Subscription; + stopSubscription: Subscription; + timerHandler: any; + @Output() + submitFinish: EventEmitter = new EventEmitter(); + // if sending stop scan request is finished, emit to farther component + @Output() + submitStopFinish: EventEmitter = new EventEmitter(); + @Output() + scanFinished: EventEmitter = new EventEmitter(); + + constructor( + private artifactService: ArtifactService, + private scanService: ScanService, + private errorHandler: ErrorHandler, + private eventService: EventService + ) {} + + ngOnInit(): void { + if ( + (this.status === SBOM_SCAN_STATUS.RUNNING || + this.status === SBOM_SCAN_STATUS.PENDING) && + !this.stateCheckTimer + ) { + // Avoid duplicated subscribing + this.stateCheckTimer = timer(0, STATE_CHECK_INTERVAL).subscribe( + () => { + this.getSbomOverview(); + } + ); + } + if (!this.generateSbomSubscription) { + this.generateSbomSubscription = this.eventService.subscribe( + HarborEvent.START_GENERATE_SBOM, + (artifactDigest: string) => { + let myFullTag: string = + this.repoName + '/' + this.artifactDigest; + if (myFullTag === artifactDigest) { + this.generateSbom(); + } + } + ); + } + if (!this.stopSubscription) { + this.stopSubscription = this.eventService.subscribe( + HarborEvent.STOP_SBOM_ARTIFACT, + (artifactDigest: string) => { + let myFullTag: string = + this.repoName + '/' + this.artifactDigest; + if (myFullTag === artifactDigest) { + this.stopSbom(); + } + } + ); + } + } + + ngOnDestroy(): void { + if (this.stateCheckTimer) { + this.stateCheckTimer.unsubscribe(); + this.stateCheckTimer = null; + } + if (this.generateSbomSubscription) { + this.generateSbomSubscription.unsubscribe(); + this.generateSbomSubscription = null; + } + if (!this.stopSubscription) { + this.stopSubscription.unsubscribe(); + this.stopSubscription = null; + } + } + + // Get vulnerability scanning status + public get status(): string { + if (this.sbomOverview && this.sbomOverview.scan_status) { + return this.sbomOverview.scan_status; + } + return SBOM_SCAN_STATUS.NOT_GENERATED_SBOM; + } + + public get completed(): boolean { + return this.status === SBOM_SCAN_STATUS.SUCCESS; + } + + public get error(): boolean { + return this.status === SBOM_SCAN_STATUS.ERROR; + } + + public get queued(): boolean { + return this.status === SBOM_SCAN_STATUS.PENDING; + } + + public get generating(): boolean { + return this.status === SBOM_SCAN_STATUS.RUNNING; + } + + public get stopped(): boolean { + return this.status === SBOM_SCAN_STATUS.STOPPED; + } + + public get otherStatus(): boolean { + return !( + this.completed || + this.error || + this.queued || + this.generating || + this.stopped + ); + } + + generateSbom(): void { + if (this.onSubmitting) { + // Avoid duplicated submitting + console.error('duplicated submit'); + return; + } + + if (!this.repoName || !this.artifactDigest) { + console.error('bad repository or tag'); + return; + } + + this.onSubmitting = true; + + this.scanService + .scanArtifact({ + projectName: this.projectName, + reference: this.artifactDigest, + repositoryName: dbEncodeURIComponent(this.repoName), + // scanType: { + // scan_type: ScanTypes.SBOM, + // }, + }) + .pipe(finalize(() => this.submitFinish.emit(false))) + .subscribe( + () => { + this.onSubmitting = false; + // Forcely change status to queued after successful submitting + this.sbomOverview = { + scan_status: SBOM_SCAN_STATUS.PENDING, + }; + // Start check status util the job is done + if (!this.stateCheckTimer) { + // Avoid duplicated subscribing + this.stateCheckTimer = timer( + STATE_CHECK_INTERVAL, + STATE_CHECK_INTERVAL + ).subscribe(() => { + this.getSbomOverview(); + }); + } + }, + error => { + this.onSubmitting = false; + if (error && error.error && error.error.code === 409) { + console.error(error.error.message); + } else { + this.errorHandler.error(error); + } + } + ); + } + + getSbomOverview(): void { + if (!this.repoName || !this.artifactDigest) { + return; + } + this.artifactService + .getArtifact({ + projectName: this.projectName, + repositoryName: dbEncodeURIComponent(this.repoName), + reference: this.artifactDigest, + // withSbomOverview: true, + XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES, + }) + .subscribe( + (artifact: Artifact) => { + // To keep the same summary reference, use value copy. + // if (artifact.sbom_overview) { + // this.copyValue(artifact.sbom_overview); + // } + if (!this.queued && !this.generating) { + // Scanning should be done + if (this.stateCheckTimer) { + this.stateCheckTimer.unsubscribe(); + this.stateCheckTimer = null; + } + this.scanFinished.emit(artifact); + } + this.eventService.publish( + HarborEvent.UPDATE_SBOM_INFO, + artifact + ); + }, + error => { + this.errorHandler.error(error); + this.retryCounter++; + if (this.retryCounter >= RETRY_TIMES) { + // Stop timer + if (this.stateCheckTimer) { + this.stateCheckTimer.unsubscribe(); + this.stateCheckTimer = null; + } + this.retryCounter = 0; + } + } + ); + } + + copyValue(newVal: SBOMOverview): void { + if (!this.sbomOverview || !newVal || !newVal.scan_status) { + return; + } + this.sbomOverview = clone(newVal); + } + + viewLog(): string { + return `${CURRENT_BASE_HREF}/projects/${ + this.projectName + }/repositories/${dbEncodeURIComponent(this.repoName)}/artifacts/${ + this.artifactDigest + }/scan/${this.sbomOverview.report_id}/log`; + } + + getScanner(): ScannerVo { + if (this.sbomOverview && this.sbomOverview.scanner) { + return this.sbomOverview.scanner; + } + return this.inputScanner; + } + + stopSbom() { + if (this.onStopping) { + // Avoid duplicated stopping command + console.error('duplicated stopping command for SBOM generation'); + return; + } + if (!this.repoName || !this.artifactDigest) { + console.error('bad repository or artifact'); + return; + } + this.onStopping = true; + + this.scanService + .stopScanArtifact({ + projectName: this.projectName, + reference: this.artifactDigest, + repositoryName: dbEncodeURIComponent(this.repoName), + scanType: { + scan_type: ScanTypes.SBOM, + }, + }) + .pipe( + finalize(() => { + this.submitStopFinish.emit(false); + this.onStopping = false; + }) + ) + .subscribe( + () => { + // Start check status util the job is done + if (!this.stateCheckTimer) { + // Avoid duplicated subscribing + this.stateCheckTimer = timer( + STATE_CHECK_INTERVAL, + STATE_CHECK_INTERVAL + ).subscribe(() => { + this.getSbomOverview(); + }); + } + this.errorHandler.info('SBOM.TRIGGER_STOP_SUCCESS'); + }, + error => { + this.errorHandler.error(error); + } + ); + } +} diff --git a/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.html b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.html new file mode 100644 index 000000000000..0bb28d622abe --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.html @@ -0,0 +1,78 @@ +
+ +
+ +
+ {{ 'SBOM.NO_SBOM' | translate }} +
+
+ +
+
+
+ {{ 'SBOM.PACKAGES' | translate }} +
+
+ + {{ 'SBOM.NO_SBOM' | translate }} + +
+
+
+ {{ + 'SCANNER.SCANNED_BY' | translate + }} + {{ getScannerInfo() }} +
+
+ {{ + 'SCANNER.DURATION' | translate + }} + {{ duration() }} +
+
+ {{ 'SBOM.CHART.SCANNING_TIME' | translate }} + + {{ completeTimestamp | harborDatetime : 'short' }} +
+
+
+ +
+ +
+ +
+ {{ 'SBOM.CHART.SCANNING_PERCENT' | translate }} + + {{ completePercent }} +
+
+ {{ + 'SBOM.CHART.SCANNING_PERCENT_EXPLAIN' | translate + }} +
+
+
+
diff --git a/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.scss b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.scss new file mode 100644 index 000000000000..973fa7e1a7f7 --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.scss @@ -0,0 +1,60 @@ +.tip-wrapper { + display: flex; + align-items: center; + color: #fff; + text-align: center; + font-size: 10px; + height: 15px; + line-height: 15px; +} + +.bar-scanning-time { + margin-top: 12px; +} + +.bar-tooltip-font-larger { + span { + font-size: 16px; + vertical-align: middle + } +} + +hr { + border-bottom: 0; + border-color: #aaa; + margin: 6px -10px; +} + +.font-weight-600 { + font-weight: 600; +} + +.margin-left-5 { + margin-left: 5px; +} + +.width-215 { + width: 215px; +} + +.width-150 { + width: 150px; +} + +.level-border>div{ + display: inline-flex; + align-items: center; + justify-items: center; + border-radius: 50%; + height: 26px; + width: 26px; + line-height: 26px; +} + +.tip-block { + position: relative; +} + +.margin-right-5 { + margin-right: 5px; +} diff --git a/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.spec.ts b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.spec.ts new file mode 100644 index 000000000000..70cee99f7efe --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.spec.ts @@ -0,0 +1,79 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ClarityModule } from '@clr/angular'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SbomTipHistogramComponent } from './sbom-tip-histogram.component'; +import { of } from 'rxjs'; +import { Project } from '../../../../../../../app/base/project/project'; +import { Artifact } from 'ng-swagger-gen/models'; + +describe('SbomTipHistogramComponent', () => { + let component: SbomTipHistogramComponent; + let fixture: ComponentFixture; + const mockRouter = { + navigate: () => {}, + }; + const mockedArtifact: Artifact = { + id: 123, + type: 'IMAGE', + }; + const mockActivatedRoute = { + RouterparamMap: of({ get: key => 'value' }), + snapshot: { + params: { + repo: 'test', + digest: 'ABC', + subscribe: () => { + return of(null); + }, + }, + parent: { + params: { + id: 1, + }, + }, + data: { + artifactResolver: [mockedArtifact, new Project()], + }, + }, + data: of({ + projectResolver: { + ismember: true, + role_name: 'maintainer', + }, + }), + }; + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + BrowserAnimationsModule, + ClarityModule, + TranslateModule.forRoot(), + ], + providers: [ + TranslateService, + { provide: Router, useValue: mockRouter }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + ], + declarations: [SbomTipHistogramComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SbomTipHistogramComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + fixture.whenStable().then(() => { + expect(component).toBeTruthy(); + expect(component.isLimitedSuccess()).toBeFalsy(); + expect(component.noSbom).toBeTruthy(); + expect(component.isThemeLight()).toBeFalsy(); + expect(component.duration()).toBe('0'); + expect(component.completePercent).toBe('0%'); + }); + }); +}); diff --git a/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.ts b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.ts new file mode 100644 index 000000000000..06cb536c0d6e --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/sbom-tip-histogram/sbom-tip-histogram.component.ts @@ -0,0 +1,108 @@ +import { Component, Input } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ScannerVo, SbomSummary } from '../../../../../../shared/services'; +import { SBOM_SCAN_STATUS } from '../../../../../../shared/units/utils'; +import { UN_LOGGED_PARAM, YES } from 'src/app/account/sign-in/sign-in.service'; +import { HAS_STYLE_MODE, StyleMode } from '../../../../../../services/theme'; +import { ScanTypes } from 'src/app/shared/entities/shared.const'; + +const MIN = 60; +const MIN_STR = 'min '; +const SEC_STR = 'sec'; +const SUCCESS_PCT: number = 100; + +@Component({ + selector: 'hbr-sbom-tip-histogram', + templateUrl: './sbom-tip-histogram.component.html', + styleUrls: ['./sbom-tip-histogram.component.scss'], +}) +export class SbomTipHistogramComponent { + @Input() scanner: ScannerVo; + @Input() sbomSummary: SbomSummary = { + scan_status: SBOM_SCAN_STATUS.NOT_GENERATED_SBOM, + }; + @Input() artifactDigest: string = ''; + @Input() sbomDigest: string = ''; + constructor( + private translate: TranslateService, + private activatedRoute: ActivatedRoute, + private router: Router + ) {} + + duration(): string { + if (this.sbomSummary && this.sbomSummary.duration) { + let str = ''; + const min = Math.floor(this.sbomSummary.duration / MIN); + if (min) { + str += min + ' ' + MIN_STR; + } + const sec = this.sbomSummary.duration % MIN; + if (sec) { + str += sec + ' ' + SEC_STR; + } + return str; + } + return '0'; + } + + public get completePercent(): string { + return this.sbomSummary.scan_status === SBOM_SCAN_STATUS.SUCCESS + ? `100%` + : '0%'; + } + isLimitedSuccess(): boolean { + return ( + this.sbomSummary && this.sbomSummary.complete_percent < SUCCESS_PCT + ); + } + get completeTimestamp(): Date { + return this.sbomSummary && this.sbomSummary.end_time + ? this.sbomSummary.end_time + : new Date(); + } + + get noSbom(): boolean { + return ( + this.sbomSummary.scan_status === SBOM_SCAN_STATUS.NOT_GENERATED_SBOM + ); + } + + isThemeLight() { + return localStorage.getItem(HAS_STYLE_MODE) === StyleMode.LIGHT; + } + + getScannerInfo(): string { + if (this.scanner) { + if (this.scanner.name && this.scanner.version) { + return `${this.scanner.name}@${this.scanner.version}`; + } + if (this.scanner.name && !this.scanner.version) { + return `${this.scanner.name}`; + } + } + return ''; + } + + goIntoArtifactSbomSummaryPage(): void { + const relativeRouterLink: string[] = ['artifacts', this.artifactDigest]; + if (this.activatedRoute.snapshot.queryParams[UN_LOGGED_PARAM] === YES) { + this.router.navigate(relativeRouterLink, { + relativeTo: this.activatedRoute, + queryParams: { + [UN_LOGGED_PARAM]: YES, + sbomDigest: this.sbomDigest ?? '', + tab: ScanTypes.SBOM, + }, + }); + } else { + this.router.navigate(relativeRouterLink, { + relativeTo: this.activatedRoute, + queryParams: { + sbomDigest: this.sbomDigest ?? '', + tab: ScanTypes.SBOM, + }, + }); + } + } +} diff --git a/src/portal/src/app/base/project/repository/artifact/sbom-scanning/scanning.scss b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/scanning.scss new file mode 100644 index 000000000000..248361e330fd --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/sbom-scanning/scanning.scss @@ -0,0 +1,172 @@ +.bar-wrapper { + width: 210px; +} + +.bar-state { + .unknow-text { + margin-left: -5px; + } + + .label { + width: 50%; + } +} + +.bar-state-chart { + .loop-height { + height: 2px; + } +} + +.bar-state-error { + position: relative; +} + +.error-text { + position: relative; + top: 1px; + margin-left: -5px; + cursor: pointer; +} + +.scanning-button { + height: 24px; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + top: -12px; + position: relative; +} + +.tip-wrapper { + display: inline-block; + height: 10px; + max-width: 120px; +} + + +.bar-tooltip-font-title { + font-weight: 600; +} + +.bar-summary { + margin-top: 12px; + text-align: left; +} + +.bar-scanning-time { + margin-top: 12px; +} + +.bar-summary-item { + margin-top: 3px; + margin-bottom: 3px; +} + +.bar-summary-item span:nth-child(1){ + width: 30px; + text-align: center; + display: inline-block; +} + +.bar-summary-item span:nth-child(2){ + width: 28px; + display: inline-block; +} + +.option-right { + padding-right: 16px; +} + +.refresh-btn { + cursor: pointer; +} + +.refresh-btn:hover { + color: #007CBB; +} + +.tip-icon-medium { + color: orange; +} + +.tip-icon-low { + color: yellow; +} + +.font-color-green{ + color:green; +} +/* stylelint-disable */ +.bar-tooltip-font-larger span{ + font-size:16px; + vertical-align:middle +} + +hr{ + border-bottom: 0; + border-color: #aaa; + margin: 6px -10px; +} + +.font-weight-600{ + font-weight:600; +} + +.rightPos{ + position: absolute; + z-index: 100; + right: 35px; + margin-top: 4px; +} + +.result-row { + position: relative; +} + +.help-icon { + margin-left: 3px; +} + +.mt-3px { + margin-top: 5px; +} + +.label-critical { + background:#ff4d2e; + color:#000; +} + +.label-danger { + background:#ff8f3d!important; + color:#000!important; +} + +.label-medium { + background-color: #ffce66; + color:#000; +} + +.label-low { + background: #fff1ad; + color:#000; +} + +.label-none { + background-color: #2ec0ff; + color:#000; +} + +.no-border { + border: none; +} + +hbr-vulnerability-bar { + .label,.not-scan { + width: 90%; + } +} + +.stopped { + border-color: #cccc15; +} diff --git a/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/result-bar-chart.component.spec.ts b/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/result-bar-chart.component.spec.ts index 1325491865b3..bcc7377197b5 100644 --- a/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/result-bar-chart.component.spec.ts +++ b/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/result-bar-chart.component.spec.ts @@ -11,14 +11,40 @@ import { import { VULNERABILITY_SCAN_STATUS } from '../../../../../shared/units/utils'; import { NativeReportSummary } from '../../../../../../../ng-swagger-gen/models/native-report-summary'; import { SharedTestingModule } from '../../../../../shared/shared.module'; +import { timer } from 'rxjs'; describe('ResultBarChartComponent (inline template)', () => { let component: ResultBarChartComponent; let fixture: ComponentFixture; let mockData: NativeReportSummary = { + report_id: '12345', scan_status: VULNERABILITY_SCAN_STATUS.SUCCESS, severity: 'High', end_time: new Date().toUTCString(), + scanner: { + name: 'Trivy', + vendor: 'vm', + version: 'v1.2', + }, + summary: { + total: 124, + fixable: 50, + summary: { + High: 5, + Low: 5, + }, + }, + }; + let mockCloneData: NativeReportSummary = { + report_id: '123456', + scan_status: VULNERABILITY_SCAN_STATUS.SUCCESS, + severity: 'High', + end_time: new Date().toUTCString(), + scanner: { + name: 'Trivy', + vendor: 'vm', + version: 'v1.3', + }, summary: { total: 124, fixable: 50, @@ -109,4 +135,33 @@ describe('ResultBarChartComponent (inline template)', () => { expect(el).not.toBeNull(); }); }); + it('Test ResultBarChartComponent getScanner', () => { + fixture.detectChanges(); + component.summary = mockData; + expect(component.getScanner()).toBe(mockData.scanner); + component.projectName = 'test'; + component.repoName = 'ui'; + component.artifactDigest = 'dg'; + expect(component.viewLog()).toBe( + '/api/v2.0/projects/test/repositories/ui/artifacts/dg/scan/12345/log' + ); + component.copyValue(mockCloneData); + expect(component.summary.report_id).toBe(mockCloneData.report_id); + }); + it('Test ResultBarChartComponent status', () => { + fixture.detectChanges(); + component.summary.scan_status = VULNERABILITY_SCAN_STATUS.SUCCESS; + expect(component.status).toBe(VULNERABILITY_SCAN_STATUS.SUCCESS); + expect(component.completed).toBeTruthy(); + expect(component.queued).toBeFalsy(); + expect(component.scanning).toBeFalsy(); + expect(component.stopped).toBeFalsy(); + expect(component.otherStatus).toBeFalsy(); + expect(component.error).toBeFalsy(); + }); + it('Test ResultBarChartComponent ngOnDestroy', () => { + component.stateCheckTimer = timer(0, 50000).subscribe(() => {}); + component.ngOnDestroy(); + expect(component.stateCheckTimer).toBeNull(); + }); }); diff --git a/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/result-bar-chart.component.ts b/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/result-bar-chart.component.ts index 69568d550edf..f83d20d396a5 100644 --- a/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/result-bar-chart.component.ts +++ b/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/result-bar-chart.component.ts @@ -25,6 +25,8 @@ import { HarborEvent, } from '../../../../../services/event-service/event.service'; import { ScanService } from '../../../../../../../ng-swagger-gen/services/scan.service'; +import { ScanType } from 'ng-swagger-gen/models'; +import { ScanTypes } from '../../../../../shared/entities/shared.const'; const STATE_CHECK_INTERVAL: number = 3000; // 3s const RETRY_TIMES: number = 3; @@ -171,6 +173,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy { projectName: this.projectName, reference: this.artifactDigest, repositoryName: dbEncodeURIComponent(this.repoName), + // scanType: { + // scan_type: ScanTypes.VULNERABILITY, + // }, }) .pipe(finalize(() => this.submitFinish.emit(false))) .subscribe( @@ -286,6 +291,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy { projectName: this.projectName, reference: this.artifactDigest, repositoryName: dbEncodeURIComponent(this.repoName), + scanType: { + scan_type: ScanTypes.VULNERABILITY, + }, }) .pipe( finalize(() => { diff --git a/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/scanning.scss b/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/scanning.scss index 5d1f983356e4..248361e330fd 100644 --- a/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/scanning.scss +++ b/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/scanning.scss @@ -8,7 +8,7 @@ } .label { - width: 90%; + width: 50%; } } diff --git a/src/portal/src/app/services/event-service/event.service.ts b/src/portal/src/app/services/event-service/event.service.ts index 88d5e28bd14b..31032f52d81c 100644 --- a/src/portal/src/app/services/event-service/event.service.ts +++ b/src/portal/src/app/services/event-service/event.service.ts @@ -76,8 +76,11 @@ export enum HarborEvent { SCROLL_TO_POSITION = 'scrollToPosition', REFRESH_PROJECT_INFO = 'refreshProjectInfo', START_SCAN_ARTIFACT = 'startScanArtifact', + START_GENERATE_SBOM = 'startGenerateSbom', STOP_SCAN_ARTIFACT = 'stopScanArtifact', + STOP_SBOM_ARTIFACT = 'stopSbomArtifact', UPDATE_VULNERABILITY_INFO = 'UpdateVulnerabilityInfo', + UPDATE_SBOM_INFO = 'UpdateSbomInfo', REFRESH_EXPORT_JOBS = 'refreshExportJobs', DELETE_ACCESSORY = 'deleteAccessory', COPY_DIGEST = 'copyDigest', diff --git a/src/portal/src/app/services/routing-resolvers/artifact-detail-routing-resolver.service.ts b/src/portal/src/app/services/routing-resolvers/artifact-detail-routing-resolver.service.ts index 77701f2a42b5..5aa9f8939cc1 100644 --- a/src/portal/src/app/services/routing-resolvers/artifact-detail-routing-resolver.service.ts +++ b/src/portal/src/app/services/routing-resolvers/artifact-detail-routing-resolver.service.ts @@ -18,7 +18,7 @@ import { ActivatedRouteSnapshot, } from '@angular/router'; import { forkJoin, Observable, of } from 'rxjs'; -import { map, catchError, mergeMap } from 'rxjs/operators'; +import { catchError, mergeMap } from 'rxjs/operators'; import { Artifact } from '../../../../ng-swagger-gen/models/artifact'; import { ArtifactService } from '../../../../ng-swagger-gen/services/artifact.service'; import { Project } from '../../base/project/project'; @@ -51,6 +51,7 @@ export class ArtifactDetailRoutingResolverService { projectName: project.name, withLabel: true, withScanOverview: true, + // withSbomOverview: true, withTag: false, withImmutableStatus: true, }), diff --git a/src/portal/src/app/shared/entities/shared.const.ts b/src/portal/src/app/shared/entities/shared.const.ts index be9bdab5f1e0..573cda40bbc8 100644 --- a/src/portal/src/app/shared/entities/shared.const.ts +++ b/src/portal/src/app/shared/entities/shared.const.ts @@ -382,3 +382,8 @@ export const stringsForClarity: Partial = { datepickerSelectYearText: 'CLARITY.DATE_PICKER_SELECT_YEAR_TEXT', datepickerSelectedLabel: 'CLARITY.DATE_PICKER_SELECTED_LABEL', }; + +export enum ScanTypes { + SBOM = 'sbom', + VULNERABILITY = 'vulnerability', +} diff --git a/src/portal/src/app/shared/services/interface.ts b/src/portal/src/app/shared/services/interface.ts index 5d641c99a873..1afbc36af379 100644 --- a/src/portal/src/app/shared/services/interface.ts +++ b/src/portal/src/app/shared/services/interface.ts @@ -211,6 +211,16 @@ export interface VulnerabilitySummary { scanner?: ScannerVo; complete_percent?: number; } +export interface SbomSummary { + report_id?: string; + sbom_digest?: string; + scan_status?: string; + duration?: number; + start_time?: Date; + end_time?: Date; + scanner?: ScannerVo; + complete_percent?: number; +} export interface ScannerVo { name?: string; vendor?: string; diff --git a/src/portal/src/app/shared/services/permission-static.ts b/src/portal/src/app/shared/services/permission-static.ts index 125b15f94541..fc15a030310c 100644 --- a/src/portal/src/app/shared/services/permission-static.ts +++ b/src/portal/src/app/shared/services/permission-static.ts @@ -105,6 +105,13 @@ export const USERSTATICPERMISSION = { READ: 'read', }, }, + REPOSITORY_TAG_SBOM_JOB: { + KEY: 'sbom', + VALUE: { + CREATE: 'create', + READ: 'read', + }, + }, REPOSITORY_ARTIFACT_LABEL: { KEY: 'artifact-label', VALUE: { diff --git a/src/portal/src/app/shared/units/utils.ts b/src/portal/src/app/shared/units/utils.ts index ad38954acf0e..4efa2eadb482 100644 --- a/src/portal/src/app/shared/units/utils.ts +++ b/src/portal/src/app/shared/units/utils.ts @@ -275,6 +275,21 @@ export const VULNERABILITY_SCAN_STATUS = { SUCCESS: 'Success', SCHEDULED: 'Scheduled', }; + +/** + * The state of sbom generation + */ +export const SBOM_SCAN_STATUS = { + // front-end status + NOT_GENERATED_SBOM: 'Not generated SBOM', + // back-end status + PENDING: 'Pending', + RUNNING: 'Running', + ERROR: 'Error', + STOPPED: 'Stopped', + SUCCESS: 'Success', + SCHEDULED: 'Scheduled', +}; /** * The severity of vulnerability scanning */ diff --git a/src/portal/src/i18n/lang/de-de-lang.json b/src/portal/src/i18n/lang/de-de-lang.json index acf0e8fdb7bc..2a42ef621af7 100644 --- a/src/portal/src/i18n/lang/de-de-lang.json +++ b/src/portal/src/i18n/lang/de-de-lang.json @@ -777,6 +777,7 @@ "ARTIFACTS": "Artefakte", "SIZE": "Größe", "VULNERABILITY": "Schwachstellen", + "SBOM": "SBOM", "BUILD_HISTORY": "Build History", "SIGNED": "Signiert", "AUTHOR": "Autor", @@ -1027,6 +1028,41 @@ "IN_PROGRESS": "Suche...", "BACK": "Zurück" }, + "SBOM": { + "CHART": { + "SCANNING_TIME": "Scan completed time:", + "SCANNING_PERCENT": "Scan completed percent:", + "SCANNING_PERCENT_EXPLAIN": "Scan completed percentage is calculated as # of successfully scanned images / total number of images referenced within the image index.", + "TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.", + "TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.", + "TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected" + }, + "GRID": { + "PLACEHOLDER": "We couldn't find any scanning results!", + "COLUMN_PACKAGE": "Package", + "COLUMN_PACKAGES": "Packages", + "COLUMN_VERSION": "Current version", + "COLUMN_LICENSE": "License", + "COLUMN_DESCRIPTION": "Description", + "FOOT_ITEMS": "Items", + "FOOT_OF": "of" + }, + "STATE": { + "OTHER_STATUS": "Not Generated", + "QUEUED": "Queued", + "ERROR": "View Log", + "SCANNING": "Generating", + "STOPPED": "Generating SBOM stopped" + }, + "NO_SBOM": "No SBOM", + "PACKAGES": "SBOM", + "REPORTED_BY": "Reported by {{scanner}}", + "GENERATE": "Start SBOM", + "DOWNLOAD": "Download SBOM", + "Details": "SBOM details", + "STOP": "Stop SBOM", + "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" + }, "VULNERABILITY": { "STATE": { "OTHER_STATUS": "Nicht gescannt", @@ -1107,6 +1143,7 @@ "ALL": "Alle", "PLACEHOLDER": "Keine Artefakte gefunden!", "SCAN_UNSUPPORTED": "Nicht unterstützt", + "SBOM_UNSUPPORTED": "Unsupported", "SUMMARY": "Zusammenfassung", "DEPENDENCIES": "Dependencies", "VALUES": "Values", diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 243ceee984a0..94a7ba63a8e1 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -778,6 +778,7 @@ "ARTIFACTS": "Artifacts", "SIZE": "Size", "VULNERABILITY": "Vulnerabilities", + "SBOM": "SBOM", "BUILD_HISTORY": "Build History", "SIGNED": "Signed", "AUTHOR": "Author", @@ -1028,6 +1029,41 @@ "IN_PROGRESS": "Search...", "BACK": "Back" }, + "SBOM": { + "CHART": { + "SCANNING_TIME": "Scan completed time:", + "SCANNING_PERCENT": "Scan completed percent:", + "SCANNING_PERCENT_EXPLAIN": "Scan completed percentage is calculated as # of successfully scanned images / total number of images referenced within the image index.", + "TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.", + "TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.", + "TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected" + }, + "GRID": { + "PLACEHOLDER": "We couldn't find any scanning results!", + "COLUMN_PACKAGE": "Package", + "COLUMN_PACKAGES": "Packages", + "COLUMN_VERSION": "Current version", + "COLUMN_LICENSE": "License", + "COLUMN_DESCRIPTION": "Description", + "FOOT_ITEMS": "Items", + "FOOT_OF": "of" + }, + "STATE": { + "OTHER_STATUS": "Not Generated", + "QUEUED": "Queued", + "ERROR": "View Log", + "SCANNING": "Generating", + "STOPPED": "Generating SBOM stopped" + }, + "NO_SBOM": "No SBOM", + "PACKAGES": "SBOM", + "REPORTED_BY": "Reported by {{scanner}}", + "GENERATE": "Start SBOM", + "DOWNLOAD": "Download SBOM", + "Details": "SBOM details", + "STOP": "Stop SBOM", + "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" + }, "VULNERABILITY": { "STATE": { "OTHER_STATUS": "Not Scanned", @@ -1108,6 +1144,7 @@ "ALL": "All", "PLACEHOLDER": "We couldn't find any artifacts!", "SCAN_UNSUPPORTED": "Unsupported", + "SBOM_UNSUPPORTED": "Unsupported", "SUMMARY": "Summary", "DEPENDENCIES": "Dependencies", "VALUES": "Values", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index cce410e54e82..240b48f9d87b 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -778,6 +778,7 @@ "ARTIFACTS": "Artifacts", "SIZE": "Size", "VULNERABILITY": "Vulnerabilities", + "SBOM": "SBOM", "BUILD_HISTORY": "Construir Historia", "SIGNED": "Firmada", "AUTHOR": "Autor", @@ -1026,6 +1027,41 @@ "IN_PROGRESS": "Buscar...", "BACK": "Volver" }, + "SBOM": { + "CHART": { + "SCANNING_TIME": "Scan completed time:", + "SCANNING_PERCENT": "Scan completed percent:", + "SCANNING_PERCENT_EXPLAIN": "Scan completed percentage is calculated as # of successfully scanned images / total number of images referenced within the image index.", + "TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.", + "TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.", + "TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected" + }, + "GRID": { + "PLACEHOLDER": "We couldn't find any scanning results!", + "COLUMN_PACKAGE": "Package", + "COLUMN_PACKAGES": "Packages", + "COLUMN_VERSION": "Current version", + "COLUMN_LICENSE": "License", + "COLUMN_DESCRIPTION": "Description", + "FOOT_ITEMS": "Items", + "FOOT_OF": "of" + }, + "STATE": { + "OTHER_STATUS": "Not Generated", + "QUEUED": "Queued", + "ERROR": "View Log", + "SCANNING": "Generating", + "STOPPED": "Generating SBOM stopped" + }, + "NO_SBOM": "No SBOM", + "PACKAGES": "SBOM", + "REPORTED_BY": "Reported by {{scanner}}", + "GENERATE": "Start SBOM", + "DOWNLOAD": "Download SBOM", + "Details": "SBOM details", + "STOP": "Stop SBOM", + "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" + }, "VULNERABILITY": { "STATE": { "OTHER_STATUS": "Not Scanned", @@ -1106,6 +1142,7 @@ "ALL": "All", "PLACEHOLDER": "We couldn't find any artifacts!", "SCAN_UNSUPPORTED": "Unsupported", + "SBOM_UNSUPPORTED": "Unsupported", "SUMMARY": "Summary", "DEPENDENCIES": "Dependencies", "VALUES": "Values", diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 035bb3a05c71..f1890bc42d06 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -777,6 +777,7 @@ "ARTIFACTS": "Artefacts", "SIZE": "Taille", "VULNERABILITY": "Vulnérabilité", + "SBOM": "SBOM", "BUILD_HISTORY": "Historique de construction", "SIGNED": "Signé", "AUTHOR": "Auteur", @@ -1026,6 +1027,41 @@ "IN_PROGRESS": "Recherche...", "BACK": "Retour" }, + "SBOM": { + "CHART": { + "SCANNING_TIME": "Scan completed time:", + "SCANNING_PERCENT": "Scan completed percent:", + "SCANNING_PERCENT_EXPLAIN": "Scan completed percentage is calculated as # of successfully scanned images / total number of images referenced within the image index.", + "TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.", + "TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.", + "TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected" + }, + "GRID": { + "PLACEHOLDER": "We couldn't find any scanning results!", + "COLUMN_PACKAGE": "Package", + "COLUMN_PACKAGES": "Packages", + "COLUMN_VERSION": "Current version", + "COLUMN_LICENSE": "License", + "COLUMN_DESCRIPTION": "Description", + "FOOT_ITEMS": "Items", + "FOOT_OF": "of" + }, + "STATE": { + "OTHER_STATUS": "Not Generated", + "QUEUED": "Queued", + "ERROR": "View Log", + "SCANNING": "Generating", + "STOPPED": "Generating SBOM stopped" + }, + "NO_SBOM": "No SBOM", + "PACKAGES": "SBOM", + "REPORTED_BY": "Reported by {{scanner}}", + "GENERATE": "Start SBOM", + "DOWNLOAD": "Download SBOM", + "Details": "SBOM details", + "STOP": "Stop SBOM", + "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" + }, "VULNERABILITY": { "STATE": { "OTHER_STATUS": "Non analysé", @@ -1106,6 +1142,7 @@ "ALL": "Tous", "PLACEHOLDER": "Nous n'avons trouvé aucun artefact !", "SCAN_UNSUPPORTED": "Non supporté", + "SBOM_UNSUPPORTED": "Unsupported", "SUMMARY": "Résumé", "DEPENDENCIES": "Dépendances", "VALUES": "Valeurs", diff --git a/src/portal/src/i18n/lang/ko-kr-lang.json b/src/portal/src/i18n/lang/ko-kr-lang.json index 20de6aa90da2..809c7707d5ad 100644 --- a/src/portal/src/i18n/lang/ko-kr-lang.json +++ b/src/portal/src/i18n/lang/ko-kr-lang.json @@ -775,6 +775,7 @@ "ARTIFACTS": "아티팩트들", "SIZE": "크기", "VULNERABILITY": "취약점", + "SBOM": "SBOM", "BUILD_HISTORY": "기록 생성", "SIGNED": "서명됨", "AUTHOR": "작성자", @@ -1025,6 +1026,41 @@ "IN_PROGRESS": "검색 중...", "BACK": "뒤로" }, + "SBOM": { + "CHART": { + "SCANNING_TIME": "Scan completed time:", + "SCANNING_PERCENT": "Scan completed percent:", + "SCANNING_PERCENT_EXPLAIN": "Scan completed percentage is calculated as # of successfully scanned images / total number of images referenced within the image index.", + "TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.", + "TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.", + "TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected" + }, + "GRID": { + "PLACEHOLDER": "We couldn't find any scanning results!", + "COLUMN_PACKAGE": "Package", + "COLUMN_PACKAGES": "Packages", + "COLUMN_VERSION": "Current version", + "COLUMN_LICENSE": "License", + "COLUMN_DESCRIPTION": "Description", + "FOOT_ITEMS": "Items", + "FOOT_OF": "of" + }, + "STATE": { + "OTHER_STATUS": "Not Generated", + "QUEUED": "Queued", + "ERROR": "View Log", + "SCANNING": "Generating", + "STOPPED": "Generating SBOM stopped" + }, + "NO_SBOM": "No SBOM", + "PACKAGES": "SBOM", + "REPORTED_BY": "Reported by {{scanner}}", + "GENERATE": "Start SBOM", + "DOWNLOAD": "Download SBOM", + "Details": "SBOM details", + "STOP": "Stop SBOM", + "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" + }, "VULNERABILITY": { "STATE": { "OTHER_STATUS": "스캔되지 않음", @@ -1105,6 +1141,7 @@ "ALL": "모두", "PLACEHOLDER": "아티팩트를 찾을 수 없음!", "SCAN_UNSUPPORTED": "지원되지 않음", + "SBOM_UNSUPPORTED": "Unsupported", "SUMMARY": "요약", "DEPENDENCIES": "종속성", "VALUES": "값", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 31243b5c33c2..d834f92b5673 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -776,6 +776,7 @@ "ARTIFACTS": "Artefatos", "SIZE": "Tamanho", "VULNERABILITY": "Vulnerabilidade", + "SBOM": "SBOM", "SIGNED": "Assinada", "AUTHOR": "Autor", "CREATED": "Data de criação", @@ -1024,6 +1025,41 @@ "IN_PROGRESS": "Buscando...", "BACK": "Voltar" }, + "SBOM": { + "CHART": { + "SCANNING_TIME": "Scan completed time:", + "SCANNING_PERCENT": "Scan completed percent:", + "SCANNING_PERCENT_EXPLAIN": "Scan completed percentage is calculated as # of successfully scanned images / total number of images referenced within the image index.", + "TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.", + "TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.", + "TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected" + }, + "GRID": { + "PLACEHOLDER": "We couldn't find any scanning results!", + "COLUMN_PACKAGE": "Package", + "COLUMN_PACKAGES": "Packages", + "COLUMN_VERSION": "Current version", + "COLUMN_LICENSE": "License", + "COLUMN_DESCRIPTION": "Description", + "FOOT_ITEMS": "Items", + "FOOT_OF": "of" + }, + "STATE": { + "OTHER_STATUS": "Not Generated", + "QUEUED": "Queued", + "ERROR": "View Log", + "SCANNING": "Generating", + "STOPPED": "Generating SBOM stopped" + }, + "NO_SBOM": "No SBOM", + "PACKAGES": "SBOM", + "REPORTED_BY": "Reported by {{scanner}}", + "GENERATE": "Start SBOM", + "DOWNLOAD": "Download SBOM", + "Details": "SBOM details", + "STOP": "Stop SBOM", + "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" + }, "VULNERABILITY": { "STATE": { "OTHER_STATUS": "Não analisado", @@ -1104,6 +1140,7 @@ "ALL": "Todos", "PLACEHOLDER": "Nenhum artefato encontrado!", "SCAN_UNSUPPORTED": "Não pode ser examinada", + "SBOM_UNSUPPORTED": "Unsupported", "SUMMARY": "Resumo", "DEPENDENCIES": "Dependências", "VALUES": "Valores", diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 82be0f11fd0b..883e0432b328 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -777,6 +777,7 @@ "ARTIFACTS": "Artifacts", "SIZE": "Boyut", "VULNERABILITY": "Güvenlik Açığı", + "SBOM": "SBOM", "BUILD_HISTORY": "Geçmişi Oluştur", "SIGNED": "İmzalanmış", "AUTHOR": "Yazar", @@ -1027,6 +1028,41 @@ "IN_PROGRESS": "Ara...", "BACK": "Geri" }, + "SBOM": { + "CHART": { + "SCANNING_TIME": "Scan completed time:", + "SCANNING_PERCENT": "Scan completed percent:", + "SCANNING_PERCENT_EXPLAIN": "Scan completed percentage is calculated as # of successfully scanned images / total number of images referenced within the image index.", + "TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.", + "TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.", + "TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected" + }, + "GRID": { + "PLACEHOLDER": "We couldn't find any scanning results!", + "COLUMN_PACKAGE": "Package", + "COLUMN_PACKAGES": "Packages", + "COLUMN_VERSION": "Current version", + "COLUMN_LICENSE": "License", + "COLUMN_DESCRIPTION": "Description", + "FOOT_ITEMS": "Items", + "FOOT_OF": "of" + }, + "STATE": { + "OTHER_STATUS": "Not Generated", + "QUEUED": "Queued", + "ERROR": "View Log", + "SCANNING": "Generating", + "STOPPED": "Generating SBOM stopped" + }, + "NO_SBOM": "No SBOM", + "PACKAGES": "SBOM", + "REPORTED_BY": "Reported by {{scanner}}", + "GENERATE": "Start SBOM", + "DOWNLOAD": "Download SBOM", + "Details": "SBOM details", + "STOP": "Stop SBOM", + "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" + }, "VULNERABILITY": { "STATE": { "OTHER_STATUS": "Taranmadı", @@ -1107,6 +1143,7 @@ "ALL": "All", "PLACEHOLDER": "We couldn't find any artifacts!", "SCAN_UNSUPPORTED": "Unsupported", + "SBOM_UNSUPPORTED": "Unsupported", "SUMMARY": "Özet", "DEPENDENCIES": "Bağımlılıklar", "VALUES": "Değerler", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index b738a3fdf572..3b0bd885fef3 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -776,6 +776,7 @@ "ARTIFACTS": "Artifacts", "SIZE": "大小", "VULNERABILITY": "漏洞", + "SBOM": "SBOM", "BUILD_HISTORY": "构建历史", "SIGNED": "已签名", "AUTHOR": "作者", @@ -1025,6 +1026,41 @@ "IN_PROGRESS": "搜索中...", "BACK": "返回" }, + "SBOM": { + "CHART": { + "SCANNING_TIME": "Scan completed time:", + "SCANNING_PERCENT": "Scan completed percent:", + "SCANNING_PERCENT_EXPLAIN": "Scan completed percentage is calculated as # of successfully scanned images / total number of images referenced within the image index.", + "TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.", + "TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.", + "TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected" + }, + "GRID": { + "PLACEHOLDER": "We couldn't find any scanning results!", + "COLUMN_PACKAGE": "Package", + "COLUMN_PACKAGES": "Packages", + "COLUMN_VERSION": "Current version", + "COLUMN_LICENSE": "License", + "COLUMN_DESCRIPTION": "Description", + "FOOT_ITEMS": "Items", + "FOOT_OF": "of" + }, + "STATE": { + "OTHER_STATUS": "Not Generated", + "QUEUED": "Queued", + "ERROR": "View Log", + "SCANNING": "Generating", + "STOPPED": "Generating SBOM stopped" + }, + "NO_SBOM": "No SBOM", + "PACKAGES": "SBOM", + "REPORTED_BY": "Reported by {{scanner}}", + "GENERATE": "Start SBOM", + "DOWNLOAD": "Download SBOM", + "Details": "SBOM details", + "STOP": "Stop SBOM", + "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" + }, "VULNERABILITY": { "STATE": { "OTHER_STATUS": "未扫描", @@ -1105,6 +1141,7 @@ "ALL": "所有", "PLACEHOLDER": "未发现任何 artifacts!", "SCAN_UNSUPPORTED": "不支持扫描", + "SBOM_UNSUPPORTED": "Unsupported", "SUMMARY": "概要", "DEPENDENCIES": "依赖", "VALUES": "取值", diff --git a/src/portal/src/i18n/lang/zh-tw-lang.json b/src/portal/src/i18n/lang/zh-tw-lang.json index 4553e344526b..c16c5b575f2c 100644 --- a/src/portal/src/i18n/lang/zh-tw-lang.json +++ b/src/portal/src/i18n/lang/zh-tw-lang.json @@ -776,6 +776,7 @@ "ARTIFACTS": "Artifacts", "SIZE": "大小", "VULNERABILITY": "弱點", + "SBOM": "SBOM", "BUILD_HISTORY": "建置歷史", "SIGNED": "已簽署", "AUTHOR": "作者", @@ -1024,6 +1025,41 @@ "IN_PROGRESS": "搜尋中...", "BACK": "返回" }, + "SBOM": { + "CHART": { + "SCANNING_TIME": "Scan completed time:", + "SCANNING_PERCENT": "Scan completed percent:", + "SCANNING_PERCENT_EXPLAIN": "Scan completed percentage is calculated as # of successfully scanned images / total number of images referenced within the image index.", + "TOOLTIPS_TITLE": "{{totalSbom}} of {{totalPackages}} {{package}} have known {{sbom}}.", + "TOOLTIPS_TITLE_SINGULAR": "{{totalSbom}} of {{totalPackages}} {{package}} has known {{sbom}}.", + "TOOLTIPS_TITLE_ZERO": "No recognizable SBOM detected" + }, + "GRID": { + "PLACEHOLDER": "We couldn't find any scanning results!", + "COLUMN_PACKAGE": "Package", + "COLUMN_PACKAGES": "Packages", + "COLUMN_VERSION": "Current version", + "COLUMN_LICENSE": "License", + "COLUMN_DESCRIPTION": "Description", + "FOOT_ITEMS": "Items", + "FOOT_OF": "of" + }, + "STATE": { + "OTHER_STATUS": "Not Generated", + "QUEUED": "Queued", + "ERROR": "View Log", + "SCANNING": "Generating", + "STOPPED": "Generating SBOM stopped" + }, + "NO_SBOM": "No SBOM", + "PACKAGES": "SBOM", + "REPORTED_BY": "Reported by {{scanner}}", + "GENERATE": "Start SBOM", + "DOWNLOAD": "Download SBOM", + "Details": "SBOM details", + "STOP": "Stop SBOM", + "TRIGGER_STOP_SUCCESS": "Trigger stopping SBOM generation successfully" +}, "VULNERABILITY": { "STATE": { "OTHER_STATUS": "未掃描", @@ -1104,6 +1140,7 @@ "ALL": "全部", "PLACEHOLDER": "未發現任何 artifacts!", "SCAN_UNSUPPORTED": "不支援掃描", + "SBOM_UNSUPPORTED": "Unsupported", "SUMMARY": "摘要", "DEPENDENCIES": "相依性", "VALUES": "值",