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 }}
+
+
+
+
+
+
+ {{
+ '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": "值",