From e8907a47ab6301298a2b77042b1408edf24a17c5 Mon Sep 17 00:00:00 2001 From: Lichao Xue <68891670+xuelichao@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:22:11 +0800 Subject: [PATCH] SBOM UI feature implementation (#19946) * draft: sbom UI feature implementation Signed-off-by: xuelichao * refactor based on swagger yaml changes Signed-off-by: xuelichao * update scan type for scan and stop sbom request Signed-off-by: xuelichao --------- Signed-off-by: xuelichao --- .../create-edit-rule.component.spec.ts | 9 + .../artifact-additions.component.html | 117 ++++++--- .../artifact-additions.component.ts | 45 +++- .../artifact-sbom.component.html | 92 +++++++ .../artifact-sbom.component.scss | 74 ++++++ .../artifact-sbom.component.spec.ts | 194 ++++++++++++++ .../artifact-sbom/artifact-sbom.component.ts | 248 ++++++++++++++++++ .../artifact-list-page.service.ts | 43 +-- .../artifact-list-tab.component.html | 84 ++++-- .../artifact-list-tab.component.spec.ts | 13 +- .../artifact-list-tab.component.ts | 92 +++++-- .../artifact/artifact-summary.component.html | 2 + .../artifact-summary.component.spec.ts | 6 + .../artifact/artifact-summary.component.ts | 10 +- .../repository/artifact/artifact.module.ts | 2 + .../project/repository/artifact/artifact.ts | 195 ++++++++++++++ .../sbom-scanning/sbom-scan.component.spec.ts | 25 +- .../sbom-scanning/sbom-scan.component.ts | 23 +- .../sbom-tip-histogram.component.ts | 6 +- .../result-bar-chart.component.spec.ts | 2 +- .../result-bar-chart.component.ts | 6 +- ...rtifact-detail-routing-resolver.service.ts | 2 +- src/portal/src/app/shared/shared.module.ts | 17 ++ src/portal/src/app/shared/units/utils.ts | 33 +++ src/portal/src/i18n/lang/de-de-lang.json | 5 +- src/portal/src/i18n/lang/en-us-lang.json | 5 +- src/portal/src/i18n/lang/es-es-lang.json | 5 +- src/portal/src/i18n/lang/fr-fr-lang.json | 7 +- src/portal/src/i18n/lang/ko-kr-lang.json | 7 +- src/portal/src/i18n/lang/pt-br-lang.json | 7 +- src/portal/src/i18n/lang/tr-tr-lang.json | 5 +- src/portal/src/i18n/lang/zh-cn-lang.json | 5 +- src/portal/src/i18n/lang/zh-tw-lang.json | 3 +- 33 files changed, 1222 insertions(+), 167 deletions(-) create mode 100644 src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.html create mode 100644 src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.scss create mode 100644 src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.spec.ts create mode 100644 src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.ts diff --git a/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.spec.ts b/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.spec.ts index 3bdf95fa5..19e47f9bc 100644 --- a/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.spec.ts +++ b/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.spec.ts @@ -281,4 +281,13 @@ describe('CreateEditRuleComponent (inline template)', () => { expect(ruleNameInput).toBeTruthy(); expect(ruleNameInput.value.trim()).toEqual('sync_01'); })); + + it('List all Registries Response', fakeAsync(() => { + fixture.detectChanges(); + comp.ngOnInit(); + comp.getAllRegistries(); + fixture.whenStable(); + tick(5000); + expect(comp.targetList.length).toBe(4); + })); }); diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-additions.component.html b/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-additions.component.html index 20d2950b8..99208d3f4 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-additions.component.html +++ b/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-additions.component.html @@ -1,62 +1,105 @@

{{ 'ARTIFACT.ADDITIONS' | translate }}

- + - - - - + + + + + + + + + + + + + - - - - + + + + + - - - - + + + + + - - - - + + + + + - - - - + + + + +
diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-additions.component.ts b/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-additions.component.ts index b31ef5df4..45994ac8e 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-additions.component.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-additions.component.ts @@ -1,15 +1,23 @@ -import { Component, Input } from '@angular/core'; +import { + AfterViewChecked, + ChangeDetectorRef, + Component, + Input, + OnInit, + ViewChild, +} from '@angular/core'; import { ADDITIONS } from './models'; import { AdditionLinks } from '../../../../../../../ng-swagger-gen/models/addition-links'; import { AdditionLink } from '../../../../../../../ng-swagger-gen/models/addition-link'; import { Artifact } from '../../../../../../../ng-swagger-gen/models/artifact'; +import { ClrTabs } from '@clr/angular'; @Component({ selector: 'artifact-additions', templateUrl: './artifact-additions.component.html', styleUrls: ['./artifact-additions.component.scss'], }) -export class ArtifactAdditionsComponent { +export class ArtifactAdditionsComponent implements AfterViewChecked, OnInit { @Input() artifact: Artifact; @Input() additionLinks: AdditionLinks; @Input() projectName: string; @@ -19,7 +27,28 @@ export class ArtifactAdditionsComponent { repoName: string; @Input() digest: string; - constructor() {} + @Input() + sbomDigest: string; + @Input() + tab: string; + + @Input() currentTabLinkId: string = 'vulnerability'; + activeTab: string = null; + + @ViewChild('additionsTab') tabs: ClrTabs; + constructor(private ref: ChangeDetectorRef) {} + + ngOnInit(): void { + this.activeTab = this.tab; + } + + ngAfterViewChecked() { + if (this.activeTab) { + this.currentTabLinkId = this.activeTab; + this.activeTab = null; + } + this.ref.detectChanges(); + } getVulnerability(): AdditionLink { if ( @@ -30,6 +59,12 @@ export class ArtifactAdditionsComponent { } return null; } + getSbom(): AdditionLink { + if (this.additionLinks && this.additionLinks[ADDITIONS.SBOMS]) { + return this.additionLinks[ADDITIONS.SBOMS]; + } + return {}; + } getBuildHistory(): AdditionLink { if (this.additionLinks && this.additionLinks[ADDITIONS.BUILD_HISTORY]) { return this.additionLinks[ADDITIONS.BUILD_HISTORY]; @@ -54,4 +89,8 @@ export class ArtifactAdditionsComponent { } return null; } + + actionTab(tab: string): void { + this.currentTabLinkId = tab; + } } diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.html b/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.html new file mode 100644 index 000000000..c7b9cf8a6 --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.html @@ -0,0 +1,92 @@ +
+
+
+
+ +
+
+
+
+ + +
+
+ +
+
+
+ {{ + 'SBOM.GRID.COLUMN_PACKAGE' | translate + }} + {{ + 'SBOM.GRID.COLUMN_VERSION' | translate + }} + {{ + 'SBOM.GRID.COLUMN_LICENSE' | translate + }} + + {{ + 'SBOM.STATE.OTHER_STATUS' | translate + }} + + {{ 'SBOM.CHART.TOOLTIPS_TITLE_ZERO' | translate }} + + + + {{ + res.name ?? '' + }} + {{ + res.versionInfo ?? '' + }} + {{ res.licenseConcluded ?? '' }} + + + +
+ {{ + 'SBOM.REPORTED_BY' + | translate + : { + scanner: getScannerInfo( + artifact?.sbom_overview?.scanner + ) + } + }} +
+ + {{ + 'PAGINATION.PAGE_SIZE' | translate + }} + {{ pagination.firstItem + 1 }} - + {{ pagination.lastItem + 1 }} + {{ 'SBOM.GRID.FOOT_OF' | translate }} + {{ artifactSbomPackages().length }} + {{ 'SBOM.GRID.FOOT_ITEMS' | translate }} + +
+
+
+
diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.scss b/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.scss new file mode 100644 index 000000000..9ec2fa919 --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.scss @@ -0,0 +1,74 @@ +.result-row { + position: relative; +} +/* stylelint-disable */ +.rightPos{ + position: absolute; + z-index: 100; + right: 0; + margin-top: 1.25rem; +} + +.option-right { + padding-right: 16px; + margin-top: 5px; +} + +.center { + align-items: center; +} + +.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; +} + +.ml-05 { + margin-left: 0.5rem; +} + +.report { + text-align: left; +} + +.mt-5px { + margin-top: 5px; +} + +.label { + min-width: 3rem; +} + +.package-medium { + max-width: 25rem; + text-overflow: ellipsis; +} + +.version-medium { + min-width: 22rem; + text-overflow: ellipsis; +} diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.spec.ts b/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.spec.ts new file mode 100644 index 000000000..e3978ad39 --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.spec.ts @@ -0,0 +1,194 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtifactSbomComponent } from './artifact-sbom.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ClarityModule } from '@clr/angular'; +import { of } from 'rxjs'; +import { + TranslateFakeLoader, + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { UserPermissionService } from '../../../../../../shared/services'; +import { AdditionLink } from '../../../../../../../../ng-swagger-gen/models/addition-link'; +import { ErrorHandler } from '../../../../../../shared/units/error-handler'; +import { SessionService } from '../../../../../../shared/services/session.service'; +import { SessionUser } from '../../../../../../shared/entities/session-user'; +import { AppConfigService } from 'src/app/services/app-config.service'; +import { ArtifactSbomPackageItem } from '../../artifact'; +import { ArtifactService } from 'ng-swagger-gen/services'; +import { ArtifactListPageService } from '../../artifact-list-page/artifact-list-page.service'; + +describe('ArtifactSbomComponent', () => { + let component: ArtifactSbomComponent; + let fixture: ComponentFixture; + const artifactSbomPackages: ArtifactSbomPackageItem[] = [ + { + name: 'alpine-baselayout', + SPDXID: 'SPDXRef-Package-5b53573c19a59415', + versionInfo: '3.2.0-r18', + supplier: 'NOASSERTION', + downloadLocation: 'NONE', + checksums: [ + { + algorithm: 'SHA1', + checksumValue: '132992eab020986b3b5d886a77212889680467a0', + }, + ], + sourceInfo: 'built package from: alpine-baselayout 3.2.0-r18', + licenseConcluded: 'GPL-2.0-only', + licenseDeclared: 'GPL-2.0-only', + copyrightText: '', + externalRefs: [ + { + referenceCategory: 'PACKAGE-MANAGER', + referenceType: 'purl', + referenceLocator: + 'pkg:apk/alpine/alpine-baselayout@3.2.0-r18?arch=x86_64\u0026distro=3.15.5', + }, + ], + attributionTexts: [ + 'PkgID: alpine-baselayout@3.2.0-r18', + 'LayerDiffID: sha256:ad543cd673bd9de2bac48599da992506dcc37a183179302ea934853aaa92cb84', + ], + primaryPackagePurpose: 'LIBRARY', + }, + { + name: 'alpine-keys', + SPDXID: 'SPDXRef-Package-7e5952f7a76e9643', + versionInfo: '2.4-r1', + supplier: 'NOASSERTION', + downloadLocation: 'NONE', + checksums: [ + { + algorithm: 'SHA1', + checksumValue: '903176b2d2a8ddefd1ba6940f19ad17c2c1d4aff', + }, + ], + sourceInfo: 'built package from: alpine-keys 2.4-r1', + licenseConcluded: 'MIT', + licenseDeclared: 'MIT', + copyrightText: '', + externalRefs: [ + { + referenceCategory: 'PACKAGE-MANAGER', + referenceType: 'purl', + referenceLocator: + 'pkg:apk/alpine/alpine-keys@2.4-r1?arch=x86_64\u0026distro=3.15.5', + }, + ], + attributionTexts: [ + 'PkgID: alpine-keys@2.4-r1', + 'LayerDiffID: sha256:ad543cd673bd9de2bac48599da992506dcc37a183179302ea934853aaa92cb84', + ], + primaryPackagePurpose: 'LIBRARY', + }, + ]; + const artifactSbomJson = { + spdxVersion: 'SPDX-2.3', + dataLicense: 'CC0-1.0', + SPDXID: 'SPDXRef-DOCUMENT', + name: 'alpine:3.15.5', + documentNamespace: + 'http://aquasecurity.github.io/trivy/container_image/alpine:3.15.5-7ead854c-7340-44c9-bbbf-5403c21cc9b6', + creationInfo: { + licenseListVersion: '', + creators: ['Organization: aquasecurity', 'Tool: trivy-0.47.0'], + created: '2023-11-29T07:06:22Z', + }, + packages: artifactSbomPackages, + }; + const fakedArtifactService = { + getAddition() { + return of(JSON.stringify(artifactSbomJson)); + }, + }; + const fakedUserPermissionService = { + hasProjectPermissions() { + return of(true); + }, + }; + const fakedAppConfigService = { + getConfig() { + return of({ sbom_enabled: true }); + }, + }; + const mockedUser: SessionUser = { + user_id: 1, + username: 'admin', + email: 'harbor@vmware.com', + realname: 'admin', + has_admin_role: true, + comment: 'no comment', + }; + const fakedSessionService = { + getCurrentUser() { + return mockedUser; + }, + }; + const mockedSbomDigest = + 'sha256:51a41cec9de9d62ee60e206f5a8a615a028a65653e45539990867417cb486285'; + const mockedArtifactListPageService = { + hasScannerSupportSBOM(): boolean { + return true; + }, + init() {}, + }; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + BrowserAnimationsModule, + ClarityModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateFakeLoader, + }, + }), + ], + declarations: [ArtifactSbomComponent], + providers: [ + ErrorHandler, + { provide: AppConfigService, useValue: fakedAppConfigService }, + { provide: ArtifactService, useValue: fakedArtifactService }, + { + provide: UserPermissionService, + useValue: fakedUserPermissionService, + }, + { provide: SessionService, useValue: fakedSessionService }, + { + provide: ArtifactListPageService, + useValue: mockedArtifactListPageService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ArtifactSbomComponent); + component = fixture.componentInstance; + component.hasSbomPermission = true; + component.hasScannerSupportSBOM = true; + component.sbomDigest = mockedSbomDigest; + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + it('should get sbom list and render', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + const rows = fixture.nativeElement.getElementsByTagName('clr-dg-row'); + expect(rows.length).toEqual(2); + }); + + it('download button should show the right text', async () => { + fixture.autoDetectChanges(true); + const scanBtn: HTMLButtonElement = + fixture.nativeElement.querySelector('#sbom-btn'); + expect(scanBtn.innerText).toContain('SBOM.DOWNLOAD'); + }); +}); diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.ts b/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.ts new file mode 100644 index 000000000..ac352ff0f --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/artifact-additions/artifact-sbom/artifact-sbom.component.ts @@ -0,0 +1,248 @@ +import { + AfterViewInit, + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; +import { ClrDatagridStateInterface, ClrLoadingState } from '@clr/angular'; +import { finalize } from 'rxjs/operators'; +import { AdditionLink } from '../../../../../../../../ng-swagger-gen/models/addition-link'; +import { + ScannerVo, + UserPermissionService, + USERSTATICPERMISSION, +} from '../../../../../../shared/services'; +import { ErrorHandler } from '../../../../../../shared/units/error-handler'; +import { + dbEncodeURIComponent, + downloadJson, + getPageSizeFromLocalStorage, + PageSizeMapKeys, + SBOM_SCAN_STATUS, + setPageSizeToLocalStorage, +} from '../../../../../../shared/units/utils'; +import { Subscription } from 'rxjs'; +import { Artifact } from '../../../../../../../../ng-swagger-gen/models/artifact'; +import { SessionService } from '../../../../../../shared/services/session.service'; +import { + EventService, + HarborEvent, +} from '../../../../../../services/event-service/event.service'; +import { severityText } from '../../../../../left-side-nav/interrogation-services/vulnerability-database/security-hub.interface'; +import { AppConfigService } from 'src/app/services/app-config.service'; + +import { + ArtifactSbom, + ArtifactSbomPackageItem, + getArtifactSbom, +} from '../../artifact'; +import { ArtifactService } from 'ng-swagger-gen/services'; +import { ScanTypes } from 'src/app/shared/entities/shared.const'; +import { ArtifactListPageService } from '../../artifact-list-page/artifact-list-page.service'; + +@Component({ + selector: 'hbr-artifact-sbom', + templateUrl: './artifact-sbom.component.html', + styleUrls: ['./artifact-sbom.component.scss'], +}) +export class ArtifactSbomComponent implements OnInit, OnDestroy { + @Input() + projectName: string; + @Input() + projectId: number; + @Input() + repoName: string; + @Input() + sbomDigest: string; + @Input() artifact: Artifact; + + artifactSbom: ArtifactSbom; + loading: boolean = false; + hasScannerSupportSBOM: boolean = false; + downloadSbomBtnState: ClrLoadingState = ClrLoadingState.DEFAULT; + hasSbomPermission: boolean = false; + + hasShowLoading: boolean = false; + sub: Subscription; + hasViewInitWithDelay: boolean = false; + pageSize: number = getPageSizeFromLocalStorage( + PageSizeMapKeys.ARTIFACT_SBOM_COMPONENT, + 25 + ); + readonly severityText = severityText; + constructor( + private errorHandler: ErrorHandler, + private appConfigService: AppConfigService, + private artifactService: ArtifactService, + private artifactListPageService: ArtifactListPageService, + private userPermissionService: UserPermissionService, + private eventService: EventService, + private session: SessionService + ) {} + + ngOnInit() { + this.artifactListPageService.init(this.projectId); + this.getSbom(); + this.getSbomPermission(); + if (!this.sub) { + this.sub = this.eventService.subscribe( + HarborEvent.UPDATE_SBOM_INFO, + (artifact: Artifact) => { + if (artifact?.digest === this.artifact?.digest) { + if (artifact.sbom_overview) { + const sbomDigest = Object.values( + artifact.sbom_overview + )?.[0]?.sbom_digest; + if (sbomDigest) { + this.sbomDigest = sbomDigest; + } + } + this.getSbom(); + } + } + ); + } + setTimeout(() => { + this.hasViewInitWithDelay = true; + }, 0); + } + + ngOnDestroy() { + if (this.sub) { + this.sub.unsubscribe(); + this.sub = null; + } + } + + getSbom() { + if (this.sbomDigest) { + if (!this.hasShowLoading) { + this.loading = true; + this.hasShowLoading = true; + } + const sbomAdditionParams = { + repositoryName: dbEncodeURIComponent(this.repoName), + reference: this.sbomDigest, + projectName: this.projectName, + addition: ScanTypes.SBOM, + }; + this.artifactService + .getAddition(sbomAdditionParams) + .pipe( + finalize(() => { + this.loading = false; + this.hasShowLoading = false; + }) + ) + .subscribe( + res => { + if (res) { + this.artifactSbom = getArtifactSbom( + JSON.parse(res) + ); + } else { + this.loading = false; + this.hasShowLoading = false; + } + }, + error => { + this.errorHandler.error(error); + } + ); + } + } + + getSbomPermission(): void { + const permissions = [ + { + resource: USERSTATICPERMISSION.REPOSITORY_TAG_SBOM_JOB.KEY, + action: USERSTATICPERMISSION.REPOSITORY_TAG_SBOM_JOB.VALUE.READ, + }, + ]; + this.userPermissionService + .hasProjectPermissions(this.projectId, permissions) + .subscribe( + (results: Array) => { + this.hasSbomPermission = results[0]; + // only has label permission + }, + error => this.errorHandler.error(error) + ); + } + + refresh(): void { + this.getSbom(); + } + + hasGeneratedSbom(): boolean { + return this.hasViewInitWithDelay; + } + + isSystemAdmin(): boolean { + const account = this.session.getCurrentUser(); + return account && account.has_admin_role; + } + + getScannerInfo(scanner: ScannerVo): string { + if (scanner) { + if (scanner.name && scanner.version) { + return `${scanner.name}@${scanner.version}`; + } + if (scanner.name && !scanner.version) { + return `${scanner.name}`; + } + } + return ''; + } + + isRunningState(): boolean { + return ( + this.hasViewInitWithDelay && + this.artifact.sbom_overview && + (this.artifact.sbom_overview.scan_status === + SBOM_SCAN_STATUS.PENDING || + this.artifact.sbom_overview.scan_status === + SBOM_SCAN_STATUS.RUNNING) + ); + } + + downloadSbom() { + this.downloadSbomBtnState = ClrLoadingState.LOADING; + if ( + this.artifact?.sbom_overview?.scan_status === + SBOM_SCAN_STATUS.SUCCESS + ) { + downloadJson( + this.artifactSbom.sbomJsonRaw, + `${this.artifactSbom.sbomName}.json` + ); + } + this.downloadSbomBtnState = ClrLoadingState.DEFAULT; + } + + canDownloadSbom(): boolean { + this.hasScannerSupportSBOM = + this.artifactListPageService.hasScannerSupportSBOM(); + return ( + this.hasScannerSupportSBOM && + //this.hasSbomPermission && + this.sbomDigest && + this.downloadSbomBtnState !== ClrLoadingState.LOADING && + this.artifactSbom !== undefined + ); + } + + artifactSbomPackages(): ArtifactSbomPackageItem[] { + return this.artifactSbom?.sbomPackage?.packages ?? []; + } + + load(state: ClrDatagridStateInterface) { + if (state?.page?.size) { + setPageSizeToLocalStorage( + PageSizeMapKeys.ARTIFACT_SBOM_COMPONENT, + state.page.size + ); + } + } +} 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 a858938dd..daf0c0ae5 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 @@ -6,6 +6,7 @@ import { USERSTATICPERMISSION, } from '../../../../../shared/services'; import { ErrorHandler } from '../../../../../shared/units/error-handler'; +import { Scanner } from '../../../../left-side-nav/interrogation-services/scanner/scanner'; @Injectable() export class ArtifactListPageService { @@ -19,6 +20,7 @@ export class ArtifactListPageService { private _hasDeleteImagePermission: boolean = false; private _hasScanImagePermission: boolean = false; private _hasSbomPermission: boolean = false; + private _scanner: Scanner = undefined; constructor( private scanningService: ScanningResultService, @@ -26,6 +28,10 @@ export class ArtifactListPageService { private errorHandlerService: ErrorHandler ) {} + getProjectScanner(): Scanner { + return this._scanner; + } + getScanBtnState(): ClrLoadingState { return this._scanBtnState; } @@ -103,29 +109,28 @@ export class ArtifactListPageService { this._sbomBtnState = ClrLoadingState.LOADING; this.scanningService.getProjectScanner(projectId).subscribe( response => { - if ( - response && - '{}' !== JSON.stringify(response) && - !response.disabled && - response.health === 'healthy' - ) { - this.updateStates( - true, - ClrLoadingState.SUCCESS, - ClrLoadingState.SUCCESS - ); - if (response?.capabilities) { - this.updateCapabilities(response?.capabilities); + if (response && '{}' !== JSON.stringify(response)) { + this._scanner = response; + if (!response.disabled && response.health === 'healthy') { + this.updateStates( + true, + ClrLoadingState.SUCCESS, + ClrLoadingState.SUCCESS + ); + if (response?.capabilities) { + this.updateCapabilities(response?.capabilities); + } + } else { + this.updateStates( + false, + ClrLoadingState.ERROR, + ClrLoadingState.ERROR + ); } - } else { - this.updateStates( - false, - ClrLoadingState.ERROR, - ClrLoadingState.ERROR - ); } }, error => { + this._scanner = null; this.updateStates( false, ClrLoadingState.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 1001da144..d523b67c1 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 @@ -24,6 +24,7 @@ canScanNow() && selectedRowHasVul() && hasEnabledScanner && + hasScannerSupportVulnerability && hasScanImagePermission ) " @@ -32,44 +33,26 @@ >  {{ 'VULNERABILITY.SCAN_NOW' | translate }} - - + + + + +
{{ 'REPOSITORY.COPY_DIGEST_ID' | translate }}
+ +
@@ -432,7 +462,7 @@ (submitStopFinish)="submitSbomStopFinish($event)" (scanFinished)="sbomFinished($event)" *ngIf="hasScannerSupportSBOM" - [inputScanner]="artifact?.sbom_overview?.scanner" + [inputScanner]="projectScanner" (submitFinish)="submitSbomFinish($event)" [projectName]="projectName" [projectId]="projectId" 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 27010f065..bb45710a1 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 @@ -27,11 +27,17 @@ import { ArtifactModule } from '../../../artifact.module'; import { SBOM_SCAN_STATUS, VULNERABILITY_SCAN_STATUS, -} from 'src/app/shared/units/utils'; +} from '../../../../../../../shared/units/utils'; +import { Scanner } from '../../../../../../left-side-nav/interrogation-services/scanner/scanner'; describe('ArtifactListTabComponent', () => { let comp: ArtifactListTabComponent; let fixture: ComponentFixture; + const mockScanner = { + name: 'Trivy', + vendor: 'vm', + version: 'v1.2', + }; const mockActivatedRoute = { snapshot: { params: { @@ -274,6 +280,9 @@ describe('ArtifactListTabComponent', () => { hasScanImagePermission(): boolean { return true; }, + getProjectScanner(): Scanner { + return mockScanner; + }, init() {}, }; beforeEach(async () => { @@ -384,7 +393,7 @@ describe('ArtifactListTabComponent', () => { fixture.nativeElement.querySelector('#generate-sbom-btn'); fixture.detectChanges(); await fixture.whenStable(); - expect(generatedButton.disabled).toBeTruthy(); + expect(generatedButton.disabled).toBeFalsy(); }); it('Stop SBOM button should be disabled', async () => { await fixture.whenStable(); 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 4675031dc..f73b448bb 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 @@ -84,6 +84,7 @@ import { Accessory } from 'ng-swagger-gen/models/accessory'; import { Tag } from '../../../../../../../../../ng-swagger-gen/models/tag'; import { CopyArtifactComponent } from './copy-artifact/copy-artifact.component'; import { CopyDigestComponent } from './copy-digest/copy-digest.component'; +import { Scanner } from '../../../../../../left-side-nav/interrogation-services/scanner/scanner'; export const AVAILABLE_TIME = '0001-01-01T00:00:00.000Z'; @@ -160,6 +161,10 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { get generateSbomBtnState(): ClrLoadingState { return this.artifactListPageService.getSbomBtnState(); } + get projectScanner(): Scanner { + return this.artifactListPageService.getProjectScanner(); + } + onSendingScanCommand: boolean; onSendingStopScanCommand: boolean = false; onStopScanArtifactsLength: number = 0; @@ -190,7 +195,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { false, false, false, - true, + false, true, false, false, @@ -269,6 +274,9 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { } ); } + if (this.projectId) { + this.artifactListPageService.init(this.projectId); + } } ngOnDestroy() { @@ -360,7 +368,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { withImmutableStatus: true, withLabel: true, withScanOverview: true, - // withSbomOverview: true, + withSbomOverview: true, withTag: false, XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES, withAccessory: false, @@ -385,7 +393,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { withImmutableStatus: true, withLabel: true, withScanOverview: true, - // withSbomOverview: true, + withSbomOverview: true, withTag: false, XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES, @@ -435,6 +443,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { repositoryName: dbEncodeURIComponent(this.repoName), withLabel: true, withScanOverview: true, + withSbomOverview: true, withTag: false, sort: getSortingString(state), XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES, @@ -749,11 +758,15 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { if (this.activatedRoute.snapshot.queryParams[UN_LOGGED_PARAM] === YES) { this.router.navigate(relativeRouterLink, { relativeTo: this.activatedRoute, - queryParams: { [UN_LOGGED_PARAM]: YES }, + queryParams: { + [UN_LOGGED_PARAM]: YES, + sbomDigest: artifact.sbomDigest ?? '', + }, }); } else { this.router.navigate(relativeRouterLink, { relativeTo: this.activatedRoute, + queryParams: { sbomDigest: artifact.sbomDigest ?? '' }, }); } } @@ -800,6 +813,26 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { } return false; } + // if has running job, return false + canGenerateSbomNow(): boolean { + if (!this.hasSbomPermission) { + return false; + } + if (this.onSendingSbomCommand) { + 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; + } // Trigger scan scanNow(): void { if (!this.selectedRow.length) { @@ -816,6 +849,22 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { ); }); } + // Generate SBOM + generateSbom(): void { + if (!this.selectedRow.length) { + return; + } + this.sbomFinishedArtifactLength = 0; + this.onSbomArtifactsLength = this.selectedRow.length; + this.onSendingSbomCommand = true; + this.selectedRow.forEach((data: any) => { + let digest = data.digest; + this.eventService.publish( + HarborEvent.START_GENERATE_SBOM, + this.repoName + '/' + digest + ); + }); + } selectedRowHasVul(): boolean { return !!( @@ -941,7 +990,6 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { } } } - // when finished, remove it from selectedRow sbomFinished(artifact: Artifact) { this.scanFinished(artifact); @@ -1019,18 +1067,17 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { }); } } - 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}`; - // } + 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, @@ -1038,16 +1085,21 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { reference: item.digest, page: 1, pageSize: ACCESSORY_PAGE_SIZE, - q: encodeURIComponent( - `type={${AccessoryType.COSIGN} ${AccessoryType.NOTATION}}` - ), + q: encodeURIComponent(`type={${queryTypes}}`), }) .subscribe({ next: res => { - if (res?.length) { - item.signed = TRUE; - } else { - item.signed = FALSE; + item.signed = res?.filter( + item => item.type !== AccessoryType.SBOM + )?.length + ? TRUE + : FALSE; + if (!item.sbomDigest) { + item.sbomDigest = + res?.filter( + item => + item.type === AccessoryType.SBOM + )?.[0]?.digest ?? null; } }, error: err => { @@ -1075,7 +1127,6 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { } return false; } - // return true if all selected rows are in "running" state canStopSbom(): boolean { if (this.onSendingStopSbomCommand) { @@ -1142,6 +1193,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy { } return null; } + deleteAccessory(a: Accessory) { let titleKey: string, summaryKey: string, diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.html b/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.html index 436363325..05465eae3 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.html +++ b/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.html @@ -59,6 +59,8 @@ [projectId]="projectId" [repoName]="repositoryName" [digest]="artifactDigest" + [sbomDigest]="sbomDigest" + [tab]="activeTab" [additionLinks]="artifact?.addition_links">
diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.spec.ts b/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.spec.ts index 9a6a0bf2f..1a7944e83 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.spec.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.spec.ts @@ -30,6 +30,8 @@ describe('ArtifactSummaryComponent', () => { return undefined; }, }; + const mockedSbomDigest = + 'sha256:51a41cec9de9d62ee60e206f5a8a615a028a65653e45539990867417cb486285'; let component: ArtifactSummaryComponent; let fixture: ComponentFixture; const mockActivatedRoute = { @@ -42,6 +44,9 @@ describe('ArtifactSummaryComponent', () => { return of(null); }, }, + queryParams: { + sbomDigest: mockedSbomDigest, + }, parent: { params: { id: 1, @@ -89,6 +94,7 @@ describe('ArtifactSummaryComponent', () => { component = fixture.componentInstance; component.repositoryName = 'demo'; component.artifactDigest = 'sha: acf4234f'; + component.sbomDigest = mockedSbomDigest; fixture.detectChanges(); }); diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.ts b/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.ts index 7aa95189d..de2f96444 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.ts @@ -1,10 +1,7 @@ import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { Artifact } from '../../../../../../ng-swagger-gen/models/artifact'; -import { ErrorHandler } from '../../../../shared/units/error-handler'; import { Label } from '../../../../../../ng-swagger-gen/models/label'; -import { ProjectService } from '../../../../shared/services'; import { ActivatedRoute, Router } from '@angular/router'; -import { AppConfigService } from '../../../../services/app-config.service'; import { Project } from '../../project'; import { artifactDefault } from './artifact'; import { SafeUrl } from '@angular/platform-browser'; @@ -24,6 +21,8 @@ import { export class ArtifactSummaryComponent implements OnInit { tagId: string; artifactDigest: string; + sbomDigest?: string; + activeTab?: string; repositoryName: string; projectId: string | number; referArtifactNameArray: string[] = []; @@ -37,10 +36,7 @@ export class ArtifactSummaryComponent implements OnInit { loading: boolean = false; constructor( - private projectService: ProjectService, - private errorHandler: ErrorHandler, private route: ActivatedRoute, - private appConfigService: AppConfigService, private router: Router, private frontEndArtifactService: ArtifactService, private event: EventService @@ -100,6 +96,8 @@ export class ArtifactSummaryComponent implements OnInit { this.repositoryName = this.route.snapshot.params['repo']; this.artifactDigest = this.route.snapshot.params['digest']; this.projectId = this.route.snapshot.parent.params['id']; + this.sbomDigest = this.route.snapshot.queryParams['sbomDigest']; + this.activeTab = this.route.snapshot.queryParams['tab']; if (this.repositoryName && this.artifactDigest) { const resolverData = this.route.snapshot.data; if (resolverData) { 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 4ff6da2d4..0272818b8 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 @@ -12,6 +12,7 @@ import { SummaryComponent } from './artifact-additions/summary/summary.component import { DependenciesComponent } from './artifact-additions/dependencies/dependencies.component'; import { BuildHistoryComponent } from './artifact-additions/build-history/build-history.component'; import { ArtifactVulnerabilitiesComponent } from './artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component'; +import { ArtifactSbomComponent } from './artifact-additions/artifact-sbom/artifact-sbom.component'; 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'; @@ -80,6 +81,7 @@ const routes: Routes = [ SummaryComponent, DependenciesComponent, BuildHistoryComponent, + ArtifactSbomComponent, ArtifactVulnerabilitiesComponent, ResultBarChartComponent, ResultSbomComponent, diff --git a/src/portal/src/app/base/project/repository/artifact/artifact.ts b/src/portal/src/app/base/project/repository/artifact/artifact.ts index 9a2c379ae..fa3e19c7d 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact.ts @@ -10,6 +10,7 @@ export interface ArtifactFront extends Artifact { annotationsArray?: Array<{ [key: string]: any }>; tagNumber?: number; signed?: string; + sbomDigest?: string; accessoryNumber?: number; } @@ -75,6 +76,7 @@ export enum AccessoryType { COSIGN = 'signature.cosign', NOTATION = 'signature.notation', NYDUS = 'accelerator.nydus', + SBOM = 'harbor.sbom', } export enum ArtifactType { @@ -166,3 +168,196 @@ export enum ClientNames { CHART = 'Helm', CNAB = 'CNAB', } + +export enum ArtifactSbomType { + SPDX = 'SPDX', +} + +export interface ArtifactSbomPackageItem { + name?: string; + versionInfo?: string; + licenseConcluded?: string; + [key: string]: Object; +} + +export interface ArtifactSbomPackage { + packages: ArtifactSbomPackageItem[]; +} + +export interface ArtifactSbom { + sbomType: ArtifactSbomType; + sbomVersion: string; + sbomName?: string; + sbomDataLicense?: string; + sbomId?: string; + sbomDocumentNamespace?: string; + sbomCreated?: string; + sbomPackage?: ArtifactSbomPackage; + sbomJsonRaw?: Object; +} + +export const ArtifactSbomFieldMapper = { + sbomVersion: 'spdxVersion', + sbomName: 'name', + sbomDataLicense: 'dataLicense', + sbomId: 'SPDXID', + sbomDocumentNamespace: 'documentNamespace', + sbomCreated: 'creationInfo.created', + sbomPackage: { + packages: ['name', 'versionInfo', 'licenseConcluded'], + }, +}; + +/** + * Identify the sbomJson contains the two main properties 'spdxVersion' and 'SPDXID'. + * @param sbomJson SBOM JSON report object. + * @returns true or false + * Return true when the sbomJson object contains the attribues 'spdxVersion' and 'SPDXID'. + * else return false. + */ +export function isSpdxSbom(sbomJson?: Object): boolean { + return Object.keys(sbomJson ?? {}).includes(ArtifactSbomFieldMapper.sbomId); +} + +/** + * Update the value to the data object with the field path. + * @param fieldPath field class path eg {a: {b:'test'}}. field path for b is 'a.b' + * @param data The target object to receive the value. + * @param value The value will be set to the data object. + */ +export function updateObjectWithFieldPath( + fieldPath: string, + data: Object, + value: Object +) { + if (fieldPath && data) { + const fields = fieldPath?.split('.'); + let tempData = data; + fields.forEach((field, index) => { + const properties = Object.getOwnPropertyNames(tempData); + if (field !== '__proto__' && field !== 'constructor') { + if (index === fields.length - 1) { + tempData[field] = value; + } else { + if (!properties.includes(field)) { + tempData[field] = {}; + } + tempData = tempData[field]; + } + } + }); + } +} + +/** + * Get value from data object with field path. + * @param fieldPath field class path eg {a: {b:'test'}}. field path for b is 'a.b' + * @param data The data source target object. + * @returns The value read from data object. + */ +export const getValueFromObjectWithFieldPath = ( + fieldPath: string, + data: Object +) => { + let tempObject = data; + if (fieldPath && data) { + const fields = fieldPath?.split('.'); + fields.forEach(field => { + if (tempObject) { + tempObject = tempObject[field] ?? null; + } + }); + } + return tempObject; +}; + +/** + * Get value from source data object with field path. + * @param fieldPathObject The Object that contains the field paths. + * If we have an Object - {a: {b: 'test', c: [{ d: 2, e: 'v'}]}}. + * The field path for b is 'a.b'. + * The field path for c is {'a.c': ['d', 'e']'}. + * @param sourceData The data source target object. + * @returns the value by field class path. + */ +export function readDataFromArtifactSbomJson( + fieldPathObject: Object, + sourceData: Object +): Object { + let result = null; + if (sourceData) { + switch (typeof fieldPathObject) { + case 'string': + result = getValueFromObjectWithFieldPath( + fieldPathObject, + sourceData + ); + break; + case 'object': + if ( + Array.isArray(fieldPathObject) && + Array.isArray(sourceData) + ) { + result = sourceData.map(source => { + let arrayItem = {}; + fieldPathObject.forEach(field => { + updateObjectWithFieldPath( + field, + arrayItem, + readDataFromArtifactSbomJson(field, source) + ); + }); + return arrayItem; + }); + } else { + const fields = Object.getOwnPropertyNames(fieldPathObject); + result = result ? result : {}; + fields.forEach(field => { + if (sourceData[field]) { + updateObjectWithFieldPath( + field, + result, + readDataFromArtifactSbomJson( + fieldPathObject[field], + sourceData[field] + ) + ); + } + }); + } + break; + default: + break; + } + } + return result; +} + +/** + * Convert SBOM Json report to ArtifactSbom + * @param sbomJson SBOM report in Json format + * @returns ArtifactSbom || null + */ +export function getArtifactSbom(sbomJson?: Object): ArtifactSbom { + if (sbomJson) { + if (isSpdxSbom(sbomJson)) { + const artifactSbom = {}; + artifactSbom.sbomJsonRaw = sbomJson; + artifactSbom.sbomType = ArtifactSbomType.SPDX; + // only retrieve the fields defined in ArtifactSbomFieldMapper + const fields = Object.getOwnPropertyNames(ArtifactSbomFieldMapper); + fields.forEach(field => { + updateObjectWithFieldPath( + field, + artifactSbom, + readDataFromArtifactSbomJson( + ArtifactSbomFieldMapper[field], + sbomJson + ) + ); + }); + return artifactSbom; + } + } + return null; +} 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 index af74ddde8..0f8398c59 100644 --- 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 @@ -10,7 +10,6 @@ import { SbomTipHistogramComponent } from './sbom-tip-histogram/sbom-tip-histogr import { SBOMOverview } from './sbom-overview'; import { of, timer } from 'rxjs'; import { ArtifactService, ScanService } from 'ng-swagger-gen/services'; -import { Artifact } from 'ng-swagger-gen/models'; describe('ResultSbomComponent (inline template)', () => { let component: ResultSbomComponent; @@ -21,23 +20,18 @@ describe('ResultSbomComponent (inline template)', () => { }; const mockedSbomDigest = 'sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3'; + const mockScanner = { + name: 'Trivy', + vendor: 'vm', + version: 'v1.2', + }; 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', - }, }; const FakedScanService = { scanArtifact: () => of({}), @@ -120,6 +114,7 @@ describe('ResultSbomComponent (inline template)', () => { fixture = TestBed.createComponent(ResultSbomComponent); component = fixture.componentInstance; component.repoName = 'mockRepo'; + component.inputScanner = mockScanner; component.artifactDigest = mockedSbomDigest; component.sbomDigest = mockedSbomDigest; component.sbomOverview = mockData; @@ -180,9 +175,11 @@ describe('ResultSbomComponent (inline template)', () => { }); it('Test ResultSbomComponent getScanner', () => { fixture.detectChanges(); + component.inputScanner = undefined; expect(component.getScanner()).toBeUndefined(); + component.inputScanner = mockScanner; component.sbomOverview = mockedSbomOverview; - expect(component.getScanner()).toBe(mockedSbomOverview.scanner); + expect(component.getScanner()).toBe(mockScanner); component.projectName = 'test'; component.repoName = 'ui'; component.artifactDigest = 'dg'; @@ -239,7 +236,9 @@ describe('ResultSbomComponent (inline template)', () => { fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); - expect(component.stateCheckTimer).toBeUndefined(); + expect(component.sbomOverview.scan_status).toBe( + SBOM_SCAN_STATUS.SUCCESS + ); }); }); }); 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 index 4cfced2f8..46829dd4f 100644 --- 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 @@ -8,7 +8,6 @@ import { } 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, @@ -27,6 +26,7 @@ import { ScanService } from '../../../../../../../ng-swagger-gen/services/scan.s import { ScanType } from 'ng-swagger-gen/models'; import { ScanTypes } from '../../../../../shared/entities/shared.const'; import { SBOMOverview } from './sbom-overview'; +import { Scanner } from '../../../../left-side-nav/interrogation-services/scanner/scanner'; const STATE_CHECK_INTERVAL: number = 3000; // 3s const RETRY_TIMES: number = 3; @@ -36,7 +36,7 @@ const RETRY_TIMES: number = 3; styleUrls: ['./scanning.scss'], }) export class ResultSbomComponent implements OnInit, OnDestroy { - @Input() inputScanner: ScannerVo; + @Input() inputScanner: Scanner; @Input() repoName: string = ''; @Input() projectName: string = ''; @Input() projectId: string = ''; @@ -176,9 +176,9 @@ export class ResultSbomComponent implements OnInit, OnDestroy { projectName: this.projectName, reference: this.artifactDigest, repositoryName: dbEncodeURIComponent(this.repoName), - // scanType: { - // scan_type: ScanTypes.SBOM, - // }, + scanType: { + scan_type: ScanTypes.SBOM, + }, }) .pipe(finalize(() => this.submitFinish.emit(false))) .subscribe( @@ -219,15 +219,15 @@ export class ResultSbomComponent implements OnInit, OnDestroy { projectName: this.projectName, repositoryName: dbEncodeURIComponent(this.repoName), reference: this.artifactDigest, - // withSbomOverview: true, + 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 (artifact.sbom_overview) { + this.copyValue(artifact.sbom_overview); + } if (!this.queued && !this.generating) { // Scanning should be done if (this.stateCheckTimer) { @@ -271,10 +271,7 @@ export class ResultSbomComponent implements OnInit, OnDestroy { }/scan/${this.sbomOverview.report_id}/log`; } - getScanner(): ScannerVo { - if (this.sbomOverview && this.sbomOverview.scanner) { - return this.sbomOverview.scanner; - } + getScanner(): Scanner { return this.inputScanner; } 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 index 2e948442b..f75050dc1 100644 --- 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 @@ -1,7 +1,7 @@ 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 { SbomSummary } from '../../../../../../shared/services'; import { SBOM_SCAN_STATUS } from '../../../../../../shared/units/utils'; import { UN_LOGGED_PARAM, @@ -9,6 +9,7 @@ import { } from '../../../../../../account/sign-in/sign-in.service'; import { HAS_STYLE_MODE, StyleMode } from '../../../../../../services/theme'; import { ScanTypes } from '../../../../../../shared/entities/shared.const'; +import { Scanner } from '../../../../../left-side-nav/interrogation-services/scanner/scanner'; const MIN = 60; const MIN_STR = 'min '; @@ -21,7 +22,7 @@ const SUCCESS_PCT: number = 100; styleUrls: ['./sbom-tip-histogram.component.scss'], }) export class SbomTipHistogramComponent { - @Input() scanner: ScannerVo; + @Input() scanner: Scanner; @Input() sbomSummary: SbomSummary = { scan_status: SBOM_SCAN_STATUS.NOT_GENERATED_SBOM, }; @@ -54,6 +55,7 @@ export class SbomTipHistogramComponent { ? `100%` : '0%'; } + isLimitedSuccess(): boolean { return ( this.sbomSummary && this.sbomSummary.complete_percent < SUCCESS_PCT 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 9f840d3ec..66534a82b 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 @@ -256,8 +256,8 @@ describe('ResultBarChartComponent (inline template)', () => { }); it('Test ResultBarChartComponent getSummary', () => { fixture.detectChanges(); - // component.summary.scan_status = VULNERABILITY_SCAN_STATUS.SUCCESS; component.getSummary(); + component.summary.scan_status = VULNERABILITY_SCAN_STATUS.SUCCESS; fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); 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 ceb8e25b2..ceef85bbe 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 @@ -173,9 +173,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy { projectName: this.projectName, reference: this.artifactDigest, repositoryName: dbEncodeURIComponent(this.repoName), - // scanType: { - // scan_type: ScanTypes.VULNERABILITY, - // }, + scanType: { + scan_type: ScanTypes.VULNERABILITY, + }, }) .pipe(finalize(() => this.submitFinish.emit(false))) .subscribe( 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 5aa9f8939..c67761e2a 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 @@ -51,7 +51,7 @@ export class ArtifactDetailRoutingResolverService { projectName: project.name, withLabel: true, withScanOverview: true, - // withSbomOverview: true, + withSbomOverview: true, withTag: false, withImmutableStatus: true, }), diff --git a/src/portal/src/app/shared/shared.module.ts b/src/portal/src/app/shared/shared.module.ts index 5409a12e5..b64995e7a 100644 --- a/src/portal/src/app/shared/shared.module.ts +++ b/src/portal/src/app/shared/shared.module.ts @@ -127,6 +127,23 @@ ClarityIcons.add({ 21.18,0,0,0,4,21.42,21,21,0,0,0,7.71,33.58a1,1,0,0,0,.81.42h19a1,1,0,0,0, .81-.42A21,21,0,0,0,32,21.42,21.18,21.18,0,0,0,29.1,10.49Z"/> `, + sbom: ` + + + + + + + + + + + + +SBOM + + +`, }); @NgModule({ diff --git a/src/portal/src/app/shared/units/utils.ts b/src/portal/src/app/shared/units/utils.ts index 4efa2eadb..1a7a91df7 100644 --- a/src/portal/src/app/shared/units/utils.ts +++ b/src/portal/src/app/shared/units/utils.ts @@ -252,6 +252,18 @@ export const DEFAULT_PAGE_SIZE: number = 15; */ export const DEFAULT_SUPPORTED_MIME_TYPES = 'application/vnd.security.vulnerability.report; version=1.1, application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0'; +/** + * The default supported mime type for SBOM + */ +export const DEFAULT_SBOM_SUPPORTED_MIME_TYPES = + 'application/vnd.security.sbom.report+json; version=1.0'; +/** + * The SBOM supported additional mime types + */ +export const SBOM_SUPPORTED_ADDITIONAL_MIME_TYPES = [ + 'application/spdx+json', + // 'application/vnd.cyclonedx+json', // feature release +]; /** * the property name of vulnerability database updated time @@ -483,6 +495,26 @@ export function downloadFile(fileData) { a.remove(); } +/** + * Download the Json Object as a Json file to local. + * @param data Json Object + * @param filename Json filename + */ +export function downloadJson(data, filename) { + const blob = new Blob([JSON.stringify(data)], { + type: 'application/json;charset=utf-8', + }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + document.body.appendChild(a); + a.setAttribute('style', 'display: none'); + a.href = url; + a.download = filename; + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); +} + export function getChanges( original: any, afterChange: any @@ -1030,6 +1062,7 @@ export enum PageSizeMapKeys { ARTIFACT_LIST_TAB_COMPONENT = 'ArtifactListTabComponent', ARTIFACT_TAGS_COMPONENT = 'ArtifactTagComponent', ARTIFACT_VUL_COMPONENT = 'ArtifactVulnerabilitiesComponent', + ARTIFACT_SBOM_COMPONENT = 'ArtifactSbomComponent', MEMBER_COMPONENT = 'MemberComponent', LABEL_COMPONENT = 'LabelComponent', P2P_POLICY_COMPONENT = 'P2pPolicyComponent', diff --git a/src/portal/src/i18n/lang/de-de-lang.json b/src/portal/src/i18n/lang/de-de-lang.json index cae7b7af0..eb640c0ba 100644 --- a/src/portal/src/i18n/lang/de-de-lang.json +++ b/src/portal/src/i18n/lang/de-de-lang.json @@ -804,6 +804,7 @@ "FILTER_BY_LABEL": "Images nach Label filtern", "FILTER_ARTIFACT_BY_LABEL": "Artefakte nach Label filtern", "ADD_LABELS": "Label hinzufügen", + "STOP": "Stop", "RETAG": "Kopieren", "ACTION": "AKTION", "DEPLOY": "Bereitstellen", @@ -1057,7 +1058,7 @@ "NO_SBOM": "No SBOM", "PACKAGES": "SBOM", "REPORTED_BY": "Reported by {{scanner}}", - "GENERATE": "Create SBOM", + "GENERATE": "Generate SBOM", "DOWNLOAD": "Download SBOM", "Details": "SBOM details", "STOP": "Stop SBOM", @@ -1107,7 +1108,7 @@ "PLACEHOLDER": "Filter Schwachstellen", "PACKAGE": "Paket", "PACKAGES": "Pakete", - "SCAN_NOW": "Scan", + "SCAN_NOW": "Scan vulnerability", "SCAN_BY": "SCAN DURCH {{scanner}}", "REPORTED_BY": "GEMELDET VON {{scanner}}", "NO_SCANNER": "KEIN SCANNER", diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 93b802401..27897783a 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -805,6 +805,7 @@ "FILTER_BY_LABEL": "Filter images by label", "FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label", "ADD_LABELS": "Add Labels", + "STOP": "Stop", "RETAG": "Copy", "ACTION": "ACTION", "DEPLOY": "DEPLOY", @@ -1058,7 +1059,7 @@ "NO_SBOM": "No SBOM", "PACKAGES": "SBOM", "REPORTED_BY": "Reported by {{scanner}}", - "GENERATE": "Create SBOM", + "GENERATE": "Generate SBOM ", "DOWNLOAD": "Download SBOM", "Details": "SBOM details", "STOP": "Stop SBOM", @@ -1108,7 +1109,7 @@ "PLACEHOLDER": "Filter Vulnerabilities", "PACKAGE": "package", "PACKAGES": "packages", - "SCAN_NOW": "Scan", + "SCAN_NOW": "Scan vulnerability ", "SCAN_BY": "SCAN BY {{scanner}}", "REPORTED_BY": "Reported by {{scanner}}", "NO_SCANNER": "NO SCANNER", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index 11dc5b0b5..0605cdeaa 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -805,6 +805,7 @@ "FILTER_BY_LABEL": "Filter images by label", "FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label", "ADD_LABELS": "Add Labels", + "STOP": "Stop", "RETAG": "Copy", "ACTION": "ACTION", "DEPLOY": "DEPLOY", @@ -1056,7 +1057,7 @@ "NO_SBOM": "No SBOM", "PACKAGES": "SBOM", "REPORTED_BY": "Reported by {{scanner}}", - "GENERATE": "Create SBOM", + "GENERATE": "Generate SBOM", "DOWNLOAD": "Download SBOM", "Details": "SBOM details", "STOP": "Stop SBOM", @@ -1106,7 +1107,7 @@ "PLACEHOLDER": "Filter Vulnerabilities", "PACKAGE": "package", "PACKAGES": "packages", - "SCAN_NOW": "Scan", + "SCAN_NOW": "Scan vulnerability", "SCAN_BY": "SCAN BY {{scanner}}", "REPORTED_BY": "Reported by {{scanner}}", "NO_SCANNER": "NO SCANNER", diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index bf45e5c53..9e4004300 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -804,6 +804,7 @@ "FILTER_BY_LABEL": "Filtrer les images par label", "FILTER_ARTIFACT_BY_LABEL": "Filtrer les artefact par label", "ADD_LABELS": "Ajouter des labels", + "STOP": "Stop", "RETAG": "Copier", "ACTION": "Action", "DEPLOY": "Déployer", @@ -1056,7 +1057,7 @@ "NO_SBOM": "No SBOM", "PACKAGES": "SBOM", "REPORTED_BY": "Reported by {{scanner}}", - "GENERATE": "Create SBOM", + "GENERATE": "Generate SBOM", "DOWNLOAD": "Download SBOM", "Details": "SBOM details", "STOP": "Stop SBOM", @@ -1106,12 +1107,12 @@ "PLACEHOLDER": "Filtrer les vulnérabilités", "PACKAGE": "paquet", "PACKAGES": "paquets", - "SCAN_NOW": "Analyser", + "SCAN_NOW": "Scan vulnerability", "SCAN_BY": "Scan par {{scanner}}", "REPORTED_BY": "Rapporté par {{scanner}}", "NO_SCANNER": "Aucun scanneur", "TRIGGER_STOP_SUCCESS": "Déclenchement avec succès de l'arrêt d'analyse", - "STOP_NOW": "Arrêter l'analyse" + "STOP_NOW": "Stop Scan" }, "PUSH_IMAGE": { "TITLE": "Commande de push", diff --git a/src/portal/src/i18n/lang/ko-kr-lang.json b/src/portal/src/i18n/lang/ko-kr-lang.json index 49f16e841..9ce8e1a17 100644 --- a/src/portal/src/i18n/lang/ko-kr-lang.json +++ b/src/portal/src/i18n/lang/ko-kr-lang.json @@ -802,6 +802,7 @@ "FILTER_BY_LABEL": "라벨별로 이미지 필터", "FILTER_ARTIFACT_BY_LABEL": "라벨별로 아티팩트 필터", "ADD_LABELS": "라벨 추가", + "STOP": "Stop", "RETAG": "복사", "ACTION": "동작", "DEPLOY": "배포", @@ -1055,7 +1056,7 @@ "NO_SBOM": "No SBOM", "PACKAGES": "SBOM", "REPORTED_BY": "Reported by {{scanner}}", - "GENERATE": "Create SBOM", + "GENERATE": "Generate SBOM", "DOWNLOAD": "Download SBOM", "Details": "SBOM details", "STOP": "Stop SBOM", @@ -1105,12 +1106,12 @@ "PLACEHOLDER": "취약점 필터", "PACKAGE": "패키지", "PACKAGES": "패키지들", - "SCAN_NOW": "스캔", + "SCAN_NOW": "Scan vulnerability", "SCAN_BY": "{{scanner}로 스캔", "REPORTED_BY": "{{scanner}}로 보고 됨", "NO_SCANNER": "스캐너 없음", "TRIGGER_STOP_SUCCESS": "트리거 중지 스캔이 성공적으로 수행되었습니다", - "STOP_NOW": "스캔 중지" + "STOP_NOW": "Stop Scan" }, "PUSH_IMAGE": { "TITLE": "푸시 명령어", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 2ba78f122..1e4431b0f 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -803,6 +803,7 @@ "FILTER_BY_LABEL": "Filtrar imagens por marcadores", "FILTER_ARTIFACT_BY_LABEL": "Filtrar por marcador", "ADD_LABELS": "Adicionar Marcadores", + "STOP": "Stop", "RETAG": "Copiar", "ACTION": "AÇÃO", "DEPLOY": "IMPLANTAR", @@ -1054,7 +1055,7 @@ "NO_SBOM": "No SBOM", "PACKAGES": "SBOM", "REPORTED_BY": "Reported by {{scanner}}", - "GENERATE": "Create SBOM", + "GENERATE": "Generate SBOM", "DOWNLOAD": "Download SBOM", "Details": "SBOM details", "STOP": "Stop SBOM", @@ -1104,12 +1105,12 @@ "PLACEHOLDER": "Filtrar", "PACKAGE": "pacote", "PACKAGES": "pacotes", - "SCAN_NOW": "Examinar", + "SCAN_NOW": "Scan vulnerability", "SCAN_BY": "EXAMINAR COM {{scanner}}", "REPORTED_BY": "Encontrado com {{scanner}}", "NO_SCANNER": "NENHUM", "TRIGGER_STOP_SUCCESS": "Exame foi interrompido", - "STOP_NOW": "Interromper" + "STOP_NOW": "Stop Scan" }, "PUSH_IMAGE": { "TITLE": "Comando Push", diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 0dddab935..3ac8e7c46 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -804,6 +804,7 @@ "FILTER_BY_LABEL": "İmajları etikete göre filtrele", "FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label", "ADD_LABELS": "Etiketler Ekle", + "STOP": "Stop", "RETAG": "Copy", "ACTION": "AKSİYON", "DEPLOY": "YÜKLE", @@ -1057,7 +1058,7 @@ "NO_SBOM": "No SBOM", "PACKAGES": "SBOM", "REPORTED_BY": "Reported by {{scanner}}", - "GENERATE": "Create SBOM", + "GENERATE": "Generate SBOM", "DOWNLOAD": "Download SBOM", "Details": "SBOM details", "STOP": "Stop SBOM", @@ -1107,7 +1108,7 @@ "PLACEHOLDER": "Güvenlik Açıklarını Filtrele", "PACKAGE": "paket", "PACKAGES": "paketler", - "SCAN_NOW": "Tara", + "SCAN_NOW": "Scan vulnerability", "SCAN_BY": "SCAN BY {{scanner}}", "REPORTED_BY": "Reported by {{scanner}}", "NO_SCANNER": "NO SCANNER", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index bb2a58578..c8b0a4258 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -801,6 +801,7 @@ "LABELS": "标签", "ADD_LABEL_TO_IMAGE": "添加标签到此镜像", "ADD_LABELS": "添加标签", + "STOP": "Stop", "RETAG": "拷贝", "FILTER_BY_LABEL": "过滤标签", "FILTER_ARTIFACT_BY_LABEL": "通过标签过滤Artifact", @@ -1055,7 +1056,7 @@ "NO_SBOM": "No SBOM", "PACKAGES": "SBOM", "REPORTED_BY": "Reported by {{scanner}}", - "GENERATE": "Create SBOM", + "GENERATE": "Generate SBOM", "DOWNLOAD": "Download SBOM", "Details": "SBOM details", "STOP": "Stop SBOM", @@ -1105,7 +1106,7 @@ "PLACEHOLDER": "过滤漏洞", "PACKAGE": "组件", "PACKAGES": "组件", - "SCAN_NOW": "扫描", + "SCAN_NOW": "Scan vulnerability", "SCAN_BY": "使用 {{scanner}} 进行扫描", "REPORTED_BY": "结果由 {{scanner}} 提供", "NO_SCANNER": "无扫描器", diff --git a/src/portal/src/i18n/lang/zh-tw-lang.json b/src/portal/src/i18n/lang/zh-tw-lang.json index bc1732775..ca8c90c44 100644 --- a/src/portal/src/i18n/lang/zh-tw-lang.json +++ b/src/portal/src/i18n/lang/zh-tw-lang.json @@ -801,6 +801,7 @@ "LABELS": "標籤", "ADD_LABEL_TO_IMAGE": "新增標籤到此映像檔", "ADD_LABELS": "新增標籤", + "STOP": "Stop", "RETAG": "複製", "FILTER_BY_LABEL": "篩選標籤", "FILTER_ARTIFACT_BY_LABEL": "透過標籤篩選 Artifact", @@ -1054,7 +1055,7 @@ "NO_SBOM": "No SBOM", "PACKAGES": "SBOM", "REPORTED_BY": "Reported by {{scanner}}", - "GENERATE": "Create SBOM", + "GENERATE": "Generate SBOM", "DOWNLOAD": "Download SBOM", "Details": "SBOM details", "STOP": "Stop SBOM",