mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-05 15:38:20 +01:00
SBOM UI feature implementation (#19946)
* draft: sbom UI feature implementation Signed-off-by: xuelichao <xuel@vmware.com> * refactor based on swagger yaml changes Signed-off-by: xuelichao <xuel@vmware.com> * update scan type for scan and stop sbom request Signed-off-by: xuelichao <xuel@vmware.com> --------- Signed-off-by: xuelichao <xuel@vmware.com>
This commit is contained in:
parent
4fd11ce072
commit
e8907a47ab
@ -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);
|
||||
}));
|
||||
});
|
||||
|
@ -1,62 +1,105 @@
|
||||
<ng-container *ngIf="additionLinks">
|
||||
<h4 class="margin-bottom-025">{{ 'ARTIFACT.ADDITIONS' | translate }}</h4>
|
||||
<div class="min-15">
|
||||
<clr-tabs>
|
||||
<clr-tabs #additionsTab>
|
||||
<clr-tab *ngIf="getVulnerability()">
|
||||
<button clrTabLink id="vulnerability">
|
||||
<button
|
||||
clrTabLink
|
||||
id="vulnerability"
|
||||
(click)="actionTab('vulnerability')">
|
||||
{{ 'REPOSITORY.VULNERABILITY' | translate }}
|
||||
</button>
|
||||
<clr-tab-content id="vulnerability-content" *clrIfActive>
|
||||
<hbr-artifact-vulnerabilities
|
||||
[artifact]="artifact"
|
||||
[projectName]="projectName"
|
||||
[projectId]="projectId"
|
||||
[repoName]="repoName"
|
||||
[digest]="digest"
|
||||
[vulnerabilitiesLink]="
|
||||
getVulnerability()
|
||||
"></hbr-artifact-vulnerabilities>
|
||||
</clr-tab-content>
|
||||
<ng-template
|
||||
[clrIfActive]="currentTabLinkId === 'vulnerability'">
|
||||
<clr-tab-content id="vulnerability-content">
|
||||
<hbr-artifact-vulnerabilities
|
||||
[artifact]="artifact"
|
||||
[projectName]="projectName"
|
||||
[projectId]="projectId"
|
||||
[repoName]="repoName"
|
||||
[digest]="digest"
|
||||
[vulnerabilitiesLink]="
|
||||
getVulnerability()
|
||||
"></hbr-artifact-vulnerabilities>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
<clr-tab *ngIf="getSbom()">
|
||||
<button clrTabLink id="sbom" (click)="actionTab('sbom')">
|
||||
{{ 'REPOSITORY.SBOM' | translate }}
|
||||
</button>
|
||||
<ng-template [clrIfActive]="currentTabLinkId === 'sbom'">
|
||||
<clr-tab-content id="sbom-content">
|
||||
<hbr-artifact-sbom
|
||||
[artifact]="artifact"
|
||||
[projectName]="projectName"
|
||||
[projectId]="projectId"
|
||||
[repoName]="repoName"
|
||||
[sbomDigest]="sbomDigest"></hbr-artifact-sbom>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
<clr-tab *ngIf="getBuildHistory()">
|
||||
<button clrTabLink id="build-history">
|
||||
<button
|
||||
clrTabLink
|
||||
id="build-history"
|
||||
(click)="actionTab('build-history')">
|
||||
{{ 'REPOSITORY.BUILD_HISTORY' | translate }}
|
||||
</button>
|
||||
<clr-tab-content *clrIfActive>
|
||||
<hbr-artifact-build-history
|
||||
[buildHistoryLink]="
|
||||
getBuildHistory()
|
||||
"></hbr-artifact-build-history>
|
||||
</clr-tab-content>
|
||||
<ng-template
|
||||
[clrIfActive]="currentTabLinkId === 'build-history'">
|
||||
<clr-tab-content>
|
||||
<hbr-artifact-build-history
|
||||
[buildHistoryLink]="
|
||||
getBuildHistory()
|
||||
"></hbr-artifact-build-history>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
<clr-tab *ngIf="getSummary()">
|
||||
<button clrTabLink id="summary-link">
|
||||
<button
|
||||
clrTabLink
|
||||
id="summary-link"
|
||||
(click)="actionTab('summary-link')">
|
||||
{{ 'ARTIFACT.SUMMARY' | translate }}
|
||||
</button>
|
||||
<clr-tab-content id="summary-content" *clrIfActive>
|
||||
<hbr-artifact-summary
|
||||
[summaryLink]="getSummary()"></hbr-artifact-summary>
|
||||
</clr-tab-content>
|
||||
<ng-template
|
||||
[clrIfActive]="currentTabLinkId === 'summary-link'">
|
||||
<clr-tab-content id="summary-content">
|
||||
<hbr-artifact-summary
|
||||
[summaryLink]="getSummary()"></hbr-artifact-summary>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
<clr-tab *ngIf="getDependencies()">
|
||||
<button clrTabLink id="depend-link">
|
||||
<button
|
||||
clrTabLink
|
||||
id="depend-link"
|
||||
(click)="actionTab('depend-link')">
|
||||
{{ 'ARTIFACT.DEPENDENCIES' | translate }}
|
||||
</button>
|
||||
<clr-tab-content id="depend-content" *clrIfActive>
|
||||
<hbr-artifact-dependencies
|
||||
[dependenciesLink]="
|
||||
getDependencies()
|
||||
"></hbr-artifact-dependencies>
|
||||
</clr-tab-content>
|
||||
<ng-template [clrIfActive]="currentTabLinkId === 'depend-link'">
|
||||
<clr-tab-content id="depend-content">
|
||||
<hbr-artifact-dependencies
|
||||
[dependenciesLink]="
|
||||
getDependencies()
|
||||
"></hbr-artifact-dependencies>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
<clr-tab *ngIf="getValues()">
|
||||
<button clrTabLink id="value-link">
|
||||
<button
|
||||
clrTabLink
|
||||
id="value-link"
|
||||
(click)="actionTab('value-link')">
|
||||
{{ 'ARTIFACT.VALUES' | translate }}
|
||||
</button>
|
||||
<clr-tab-content id="value-content" *clrIfActive>
|
||||
<hbr-artifact-values
|
||||
[valuesLink]="getValues()"></hbr-artifact-values>
|
||||
</clr-tab-content>
|
||||
<ng-template [clrIfActive]="currentTabLinkId === 'value-link'">
|
||||
<clr-tab-content id="value-content">
|
||||
<hbr-artifact-values
|
||||
[valuesLink]="getValues()"></hbr-artifact-values>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
</clr-tabs>
|
||||
</div>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
<div class="row result-row">
|
||||
<div>
|
||||
<div class="row flex-items-xs-right rightPos">
|
||||
<div class="flex-xs-middle option-right">
|
||||
<span class="refresh-btn" (click)="refresh()"
|
||||
><clr-icon shape="refresh"></clr-icon
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="load($event)">
|
||||
<clr-dg-action-bar>
|
||||
<div class="clr-row center">
|
||||
<div class="ml-05">
|
||||
<button
|
||||
id="sbom-btn"
|
||||
(click)="downloadSbom()"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
[clrLoading]="downloadSbomBtnState"
|
||||
[disabled]="!canDownloadSbom()">
|
||||
<clr-icon
|
||||
shape="download"
|
||||
size="16"
|
||||
*ngIf="!isRunningState()"></clr-icon
|
||||
>
|
||||
<span *ngIf="!isRunningState()">{{
|
||||
'SBOM.DOWNLOAD' | translate
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column [clrDgField]="'package'" class="package-medium">{{
|
||||
'SBOM.GRID.COLUMN_PACKAGE' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'version'" class="version-medium">{{
|
||||
'SBOM.GRID.COLUMN_VERSION' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column>{{
|
||||
'SBOM.GRID.COLUMN_LICENSE' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-placeholder>
|
||||
<span *ngIf="hasGeneratedSbom(); else elseBlock">{{
|
||||
'SBOM.STATE.OTHER_STATUS' | translate
|
||||
}}</span>
|
||||
<ng-template #elseBlock>
|
||||
{{ 'SBOM.CHART.TOOLTIPS_TITLE_ZERO' | translate }}
|
||||
</ng-template>
|
||||
</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let res of artifactSbomPackages()">
|
||||
<clr-dg-cell class="package-medium">{{
|
||||
res.name ?? ''
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell class="version-medium">{{
|
||||
res.versionInfo ?? ''
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ res.licenseConcluded ?? '' }}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
<div class="report">
|
||||
<i *ngIf="artifact?.sbom_overview">{{
|
||||
'SBOM.REPORTED_BY'
|
||||
| translate
|
||||
: {
|
||||
scanner: getScannerInfo(
|
||||
artifact?.sbom_overview?.scanner
|
||||
)
|
||||
}
|
||||
}}</i>
|
||||
</div>
|
||||
<clr-dg-pagination
|
||||
#pagination
|
||||
[clrDgPageSize]="pageSize"
|
||||
[clrDgTotalItems]="artifactSbomPackages().length">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[15, 25, 50]">{{
|
||||
'PAGINATION.PAGE_SIZE' | translate
|
||||
}}</clr-dg-page-size>
|
||||
<span *ngIf="artifactSbomPackages().length"
|
||||
>{{ pagination.firstItem + 1 }} -
|
||||
{{ pagination.lastItem + 1 }}
|
||||
{{ 'SBOM.GRID.FOOT_OF' | translate }}</span
|
||||
>
|
||||
{{ artifactSbomPackages().length }}
|
||||
{{ 'SBOM.GRID.FOOT_ITEMS' | translate }}
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
@ -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;
|
||||
}
|
@ -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<ArtifactSbomComponent>;
|
||||
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');
|
||||
});
|
||||
});
|
@ -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 = <ArtifactService.GetAdditionParams>{
|
||||
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<boolean>) => {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -24,6 +24,7 @@
|
||||
canScanNow() &&
|
||||
selectedRowHasVul() &&
|
||||
hasEnabledScanner &&
|
||||
hasScannerSupportVulnerability &&
|
||||
hasScanImagePermission
|
||||
)
|
||||
"
|
||||
@ -32,44 +33,26 @@
|
||||
>
|
||||
<span>{{ 'VULNERABILITY.SCAN_NOW' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
id="stop-scan"
|
||||
[clrLoading]="stopBtnState"
|
||||
type="button"
|
||||
class="btn btn-secondary scan-btn"
|
||||
[disabled]="!(canStopScan() && hasScanImagePermission)"
|
||||
(click)="stopNow()">
|
||||
<clr-icon shape="stop" size="16"></clr-icon>
|
||||
<span>{{ 'VULNERABILITY.STOP_NOW' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasEnabledSbom()"
|
||||
id="generate-sbom-btn"
|
||||
[clrLoading]="generateSbomBtnState"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
[disabled]="true"
|
||||
(click)="generateSbom()">
|
||||
<clr-icon shape="file" size="16"></clr-icon>
|
||||
<span>{{ 'SBOM.GENERATE' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
id="stop-sbom-btn"
|
||||
*ngIf="hasEnabledSbom()"
|
||||
[clrLoading]="stopBtnState"
|
||||
type="button"
|
||||
class="btn btn-secondary scan-btn"
|
||||
[disabled]="
|
||||
!(
|
||||
canStopSbom() &&
|
||||
canGenerateSbomNow() &&
|
||||
selectedRowHasSbom() &&
|
||||
hasEnabledScanner &&
|
||||
hasSbomPermission &&
|
||||
hasEnabledScanner
|
||||
hasScannerSupportSBOM
|
||||
)
|
||||
"
|
||||
(click)="stopSbom()">
|
||||
<clr-icon shape="stop" size="16"></clr-icon>
|
||||
<span>{{ 'SBOM.STOP' | translate }}</span>
|
||||
(click)="generateSbom()">
|
||||
<clr-icon shape="sbom" size="16"></clr-icon>
|
||||
<span>{{ 'SBOM.GENERATE' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<clr-dropdown class="btn btn-link" *ngIf="!depth">
|
||||
<span
|
||||
clrDropdownTrigger
|
||||
@ -82,6 +65,45 @@
|
||||
class="action-dropdown"
|
||||
clrPosition="bottom-left"
|
||||
*clrIfOpen>
|
||||
<div
|
||||
class="dropdown-divider"
|
||||
role="separator"
|
||||
aria-hidden="true"></div>
|
||||
<button
|
||||
clrDropdownItem
|
||||
id="stop-scan"
|
||||
[clrLoading]="stopBtnState"
|
||||
type="button"
|
||||
class="btn btn-secondary scan-btn action-dropdown-item"
|
||||
[disabled]="
|
||||
!(canStopScan() && hasScanImagePermission)
|
||||
"
|
||||
(click)="stopNow()">
|
||||
<span>{{
|
||||
'VULNERABILITY.STOP_NOW' | translate
|
||||
}}</span>
|
||||
</button>
|
||||
<button
|
||||
clrDropdownItem
|
||||
id="stop-sbom-btn"
|
||||
*ngIf="hasEnabledSbom()"
|
||||
[clrLoading]="stopBtnState"
|
||||
type="button"
|
||||
class="btn btn-secondary scan-btn action-dropdown-item"
|
||||
[disabled]="
|
||||
!(
|
||||
canStopSbom() &&
|
||||
hasSbomPermission &&
|
||||
hasEnabledScanner
|
||||
)
|
||||
"
|
||||
(click)="stopSbom()">
|
||||
<span>{{ 'SBOM.STOP' | translate }}</span>
|
||||
</button>
|
||||
<div
|
||||
class="dropdown-divider"
|
||||
role="separator"
|
||||
aria-hidden="true"></div>
|
||||
<div
|
||||
id="artifact-list-copy-digest"
|
||||
class="action-dropdown-item no-border"
|
||||
@ -93,6 +115,10 @@
|
||||
(click)="showDigestId()">
|
||||
{{ 'REPOSITORY.COPY_DIGEST_ID' | translate }}
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-divider"
|
||||
role="separator"
|
||||
aria-hidden="true"></div>
|
||||
<clr-dropdown>
|
||||
<button
|
||||
id="artifact-list-add-labels"
|
||||
@ -106,6 +132,10 @@
|
||||
">
|
||||
{{ 'REPOSITORY.ADD_LABELS' | translate }}
|
||||
</button>
|
||||
<div
|
||||
class="dropdown-divider"
|
||||
role="separator"
|
||||
aria-hidden="true"></div>
|
||||
<clr-dropdown-menu
|
||||
[hidden]="!selectedRow.length">
|
||||
<div class="filter-grid">
|
||||
@ -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"
|
||||
|
@ -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<ArtifactListTabComponent>;
|
||||
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();
|
||||
|
@ -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,
|
||||
|
@ -59,6 +59,8 @@
|
||||
[projectId]="projectId"
|
||||
[repoName]="repositoryName"
|
||||
[digest]="artifactDigest"
|
||||
[sbomDigest]="sbomDigest"
|
||||
[tab]="activeTab"
|
||||
[additionLinks]="artifact?.addition_links"></artifact-additions>
|
||||
</ng-container>
|
||||
<div *ngIf="loading" class="clr-row mt-3 center">
|
||||
|
@ -30,6 +30,8 @@ describe('ArtifactSummaryComponent', () => {
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
const mockedSbomDigest =
|
||||
'sha256:51a41cec9de9d62ee60e206f5a8a615a028a65653e45539990867417cb486285';
|
||||
let component: ArtifactSummaryComponent;
|
||||
let fixture: ComponentFixture<ArtifactSummaryComponent>;
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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>{};
|
||||
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;
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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: <ScanType>{
|
||||
// scan_type: ScanTypes.SBOM,
|
||||
// },
|
||||
scanType: <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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -173,9 +173,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
||||
projectName: this.projectName,
|
||||
reference: this.artifactDigest,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
// scanType: <ScanType>{
|
||||
// scan_type: ScanTypes.VULNERABILITY,
|
||||
// },
|
||||
scanType: <ScanType>{
|
||||
scan_type: ScanTypes.VULNERABILITY,
|
||||
},
|
||||
})
|
||||
.pipe(finalize(() => this.submitFinish.emit(false)))
|
||||
.subscribe(
|
||||
|
@ -51,7 +51,7 @@ export class ArtifactDetailRoutingResolverService {
|
||||
projectName: project.name,
|
||||
withLabel: true,
|
||||
withScanOverview: true,
|
||||
// withSbomOverview: true,
|
||||
withSbomOverview: true,
|
||||
withTag: false,
|
||||
withImmutableStatus: true,
|
||||
}),
|
||||
|
@ -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"/>
|
||||
<rect class="cls-1" width="36" height="36"/></g></svg>`,
|
||||
sbom: `
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<!-- Generator: imaengine 6.0 -->
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0,0,512,512" style="enable-background:new 0 0 512 512;" version="1.1">
|
||||
<defs/>
|
||||
<g id="layer0">
|
||||
<g transform="matrix(1 0 0 1 0 0)">
|
||||
<path d="M341.333,213.333L362.666,213.333L362.666,117.333C362.619,116.239 362.399,115.159 362.015,114.133C361.93,113.866 361.951,113.557 361.844,113.29C361.285,111.937 360.454,110.713 359.401,109.695L252.885,3.136C250.88,1.135 248.166,0.0079999 245.333,0L10.667,0C4.776,0 0,4.776 0,10.667L0,458.667C0,464.558 4.776,469.334 10.667,469.334L224,469.334L224,448L21.333,448L21.333,21.333L234.666,21.333L234.666,117.333C234.666,123.224 239.442,128 245.333,128L341.333,128L341.333,213.333L341.333,213.333ZM256,106.667L256,36.427L326.219,106.667L256,106.667L256,106.667Z" fill="#175975"/>
|
||||
<path d="M501.333,341.333L480,341.333L480,330.666C479.988,289.429 446.55,256.009 405.312,256.02C402.195,256.021 399.081,256.217 395.989,256.607C358.752,261.151 330.666,294.047 330.666,333.119L330.666,341.332L309.333,341.332C303.442,341.332 298.666,346.108 298.666,351.999L298.666,501.332C298.666,507.223 303.442,511.999 309.333,511.999L501.333,511.999C507.224,511.999 512,507.223 512,501.332L512,352C512,346.109 507.224,341.333 501.333,341.333L501.333,341.333ZM352,333.131C352,304.822 372.021,281.035 398.571,277.792C427.788,274.057 454.502,294.715 458.237,323.932C458.523,326.165 458.666,328.415 458.667,330.666L458.667,341.333L352,341.333L352,333.131L352,333.131ZM490.667,490.667L320,490.667L320,362.667L490.667,362.667L490.667,490.667L490.667,490.667Z" fill="#175975"/>
|
||||
<path d="M394.667,423.797L394.667,458.666C394.667,464.557 399.443,469.333 405.334,469.333C411.225,469.333 416,464.558 416,458.667L416,423.563C426.103,417.57 429.435,404.522 423.443,394.419C419.605,387.948 412.633,383.986 405.11,384C393.369,383.979 383.834,393.479 383.813,405.22C383.798,412.922 387.951,420.028 394.667,423.797L394.667,423.797Z" fill="#175975"/>
|
||||
</g>
|
||||
<text font-size="100" font-family="'MesloLGLDZForPowerline-Bold'" fill="#175975" transform="matrix(1.24404 0 0 1.04972 34.5897 178.002)">
|
||||
<tspan x="0" y="112" textLength="240.82">
|
||||
<![CDATA[SBOM]]></tspan>
|
||||
</text>
|
||||
</g>
|
||||
</svg>`,
|
||||
});
|
||||
|
||||
@NgModule({
|
||||
|
@ -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',
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": "푸시 명령어",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": "无扫描器",
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user