mirror of https://github.com/goharbor/harbor.git
draft: sbom UI feature implementation
Signed-off-by: xuelichao <xuel@vmware.com>
This commit is contained in:
parent
4fd11ce072
commit
7669e3d771
|
@ -4744,7 +4744,7 @@ paths:
|
|||
summary: Get job log by job id
|
||||
description: Get job log by job id, it is only used by administrator
|
||||
produces:
|
||||
- text/plain
|
||||
- text/plain
|
||||
tags:
|
||||
- jobservice
|
||||
parameters:
|
||||
|
@ -4875,6 +4875,7 @@ paths:
|
|||
'200':
|
||||
description: Get scheduler status successfully.
|
||||
schema:
|
||||
type: object
|
||||
$ref: '#/definitions/SchedulerStatus'
|
||||
'401':
|
||||
$ref: '#/responses/401'
|
||||
|
@ -6096,7 +6097,7 @@ paths:
|
|||
description: Specify whether the dangerous Artifact are included inside summary information
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
default: false
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
|
@ -6115,15 +6116,15 @@ paths:
|
|||
get:
|
||||
summary: Get the vulnerability list.
|
||||
description: |
|
||||
Get the vulnerability list. use q to pass the query condition,
|
||||
Get the vulnerability list. use q to pass the query condition,
|
||||
supported conditions:
|
||||
cve_id(exact match)
|
||||
cvss_score_v3(range condition)
|
||||
severity(exact match)
|
||||
repository_name(exact match)
|
||||
project_id(exact match)
|
||||
repository_name(exact match)
|
||||
project_id(exact match)
|
||||
package(exact match)
|
||||
tag(exact match)
|
||||
tag(exact match)
|
||||
digest(exact match)
|
||||
tags:
|
||||
- securityhub
|
||||
|
@ -6800,6 +6801,12 @@ definitions:
|
|||
format: int64
|
||||
description: 'Time in seconds required to create the report'
|
||||
example: 300
|
||||
complete_percent:
|
||||
type: integer
|
||||
description: 'The complete percent of the scanning which value is between 0 and 100'
|
||||
example: 100
|
||||
scanner:
|
||||
$ref: '#/definitions/Scanner'
|
||||
NativeReportSummary:
|
||||
type: object
|
||||
description: 'The summary for the native report'
|
||||
|
@ -7221,10 +7228,6 @@ definitions:
|
|||
type: string
|
||||
description: 'Whether scan images automatically when pushing. The valid values are "true", "false".'
|
||||
x-nullable: true
|
||||
auto_sbom_generation:
|
||||
type: string
|
||||
description: 'Whether generating SBOM automatically when pushing a subject artifact. The valid values are "true", "false".'
|
||||
x-nullable: true
|
||||
reuse_sys_cve_allowlist:
|
||||
type: string
|
||||
description: 'Whether this project reuse the system level CVE allowlist as the allowlist of its own. The valid values are "true", "false".
|
||||
|
@ -7717,9 +7720,8 @@ definitions:
|
|||
description: The level of the robot, project or system
|
||||
duration:
|
||||
type: integer
|
||||
x-nullable: true
|
||||
format: int64
|
||||
description: The duration of the robot in days, duration must be either -1(Never) or a positive integer
|
||||
description: The duration of the robot in days
|
||||
editable:
|
||||
type: boolean
|
||||
x-omitempty: false
|
||||
|
@ -7766,7 +7768,7 @@ definitions:
|
|||
duration:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The duration of the robot in days, duration must be either -1(Never) or a positive integer
|
||||
description: The duration of the robot in days
|
||||
permissions:
|
||||
type: array
|
||||
items:
|
||||
|
@ -8056,7 +8058,7 @@ definitions:
|
|||
type: string
|
||||
description: |
|
||||
The schedule type. The valid values are 'Hourly', 'Daily', 'Weekly', 'Custom', 'Manual', 'None' and 'Schedule'.
|
||||
'Manual' means to trigger it right away, 'Schedule' means to trigger it by a specified cron schedule and
|
||||
'Manual' means to trigger it right away, 'Schedule' means to trigger it by a specified cron schedule and
|
||||
'None' means to cancel the schedule.
|
||||
enum:
|
||||
- Hourly
|
||||
|
@ -9886,12 +9888,12 @@ definitions:
|
|||
type: object
|
||||
description: the dangerous CVE information
|
||||
properties:
|
||||
cve_id:
|
||||
cve_id:
|
||||
type: string
|
||||
description: the cve id
|
||||
severity:
|
||||
type: string
|
||||
description: the severity of the CVE
|
||||
description: the severity of the CVE
|
||||
cvss_score_v3:
|
||||
type: number
|
||||
format: float64
|
||||
|
@ -9901,7 +9903,7 @@ definitions:
|
|||
description: the description of the CVE
|
||||
package:
|
||||
type: string
|
||||
description: the package of the CVE
|
||||
description: the package of the CVE
|
||||
version:
|
||||
type: string
|
||||
description: the version of the package
|
||||
|
@ -9909,14 +9911,14 @@ definitions:
|
|||
type: object
|
||||
description: the dangerous artifact information
|
||||
properties:
|
||||
project_id:
|
||||
project_id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: the project id of the artifact
|
||||
repository_name:
|
||||
type: string
|
||||
description: the repository name of the artifact
|
||||
digest:
|
||||
digest:
|
||||
type: string
|
||||
description: the digest of the artifact
|
||||
critical_cnt:
|
||||
|
@ -9976,7 +9978,7 @@ definitions:
|
|||
description: The description of the vulnerability
|
||||
links:
|
||||
type: array
|
||||
items:
|
||||
items:
|
||||
type: string
|
||||
description: Links of the vulnerability
|
||||
ScanType:
|
||||
|
@ -9985,4 +9987,4 @@ definitions:
|
|||
scan_type:
|
||||
type: string
|
||||
description: 'The scan type for the scan request. Two options are currently supported, vulnerability and sbom'
|
||||
enum: [ vulnerability, sbom ]
|
||||
enum: [ vulnerability, sbom ]
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DEFAULT_SUPPORTED_MIME_TYPES } from '../../../../../shared/units/utils';
|
||||
import {
|
||||
DEFAULT_SBOM_SUPPORTED_MIME_TYPES,
|
||||
DEFAULT_SUPPORTED_MIME_TYPES,
|
||||
} from '../../../../../shared/units/utils';
|
||||
import { ScanTypes } from 'src/app/shared/entities/shared.const';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
@ -12,7 +16,8 @@ export class AdditionsService {
|
|||
getDetailByLink(
|
||||
link: string,
|
||||
shouldSetHeader: boolean,
|
||||
shouldReturnText: boolean
|
||||
shouldReturnText: boolean,
|
||||
scanType = ScanTypes.VULNERABILITY
|
||||
): Observable<any> {
|
||||
if (shouldReturnText) {
|
||||
return this.http.get(link, {
|
||||
|
@ -22,9 +27,16 @@ export class AdditionsService {
|
|||
}
|
||||
if (shouldSetHeader) {
|
||||
return this.http.get(link, {
|
||||
headers: {
|
||||
'X-Accept-Vulnerabilities': DEFAULT_SUPPORTED_MIME_TYPES,
|
||||
},
|
||||
headers:
|
||||
scanType === ScanTypes.SBOM
|
||||
? {
|
||||
'X-Accept-SBOMs':
|
||||
DEFAULT_SBOM_SUPPORTED_MIME_TYPES,
|
||||
}
|
||||
: {
|
||||
'X-Accept-Vulnerabilities':
|
||||
DEFAULT_SUPPORTED_MIME_TYPES,
|
||||
},
|
||||
});
|
||||
}
|
||||
return this.http.get(link);
|
||||
|
|
|
@ -18,6 +18,20 @@
|
|||
"></hbr-artifact-vulnerabilities>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
<clr-tab *ngIf="getSbom()">
|
||||
<button clrTabLink id="sbom">
|
||||
{{ 'REPOSITORY.SBOM' | translate }}
|
||||
</button>
|
||||
<clr-tab-content id="sbom-content" *clrIfActive>
|
||||
<hbr-artifact-sbom
|
||||
[artifact]="artifact"
|
||||
[projectName]="projectName"
|
||||
[projectId]="projectId"
|
||||
[repoName]="repoName"
|
||||
[sbomDigest]="sbomDigest"
|
||||
[sbomLink]="getSbom()"></hbr-artifact-sbom>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
<clr-tab *ngIf="getBuildHistory()">
|
||||
<button clrTabLink id="build-history">
|
||||
{{ 'REPOSITORY.BUILD_HISTORY' | translate }}
|
||||
|
|
|
@ -19,6 +19,8 @@ export class ArtifactAdditionsComponent {
|
|||
repoName: string;
|
||||
@Input()
|
||||
digest: string;
|
||||
@Input()
|
||||
sbomDigest: string;
|
||||
constructor() {}
|
||||
|
||||
getVulnerability(): AdditionLink {
|
||||
|
@ -30,6 +32,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];
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
<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'">{{
|
||||
'SBOM.GRID.COLUMN_PACKAGE' | translate
|
||||
}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'version'">{{
|
||||
'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>{{ res.name ?? '' }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ 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?.scanner">{{
|
||||
'SBOM.REPORTED_BY'
|
||||
| translate
|
||||
: { scanner: getScannerInfo(artifact?.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,64 @@
|
|||
.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;
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
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';
|
||||
|
||||
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 mockedLink: AdditionLink = {
|
||||
absolute: false,
|
||||
href: '/test',
|
||||
};
|
||||
const fakedArtifactService = {
|
||||
getSbomAddition() {
|
||||
return of(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';
|
||||
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 },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ArtifactSbomComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.hasSbomPermission = true;
|
||||
component.hasEnabledSbom = true;
|
||||
component.sbomLink = mockedLink;
|
||||
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,236 @@
|
|||
import { 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';
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-artifact-sbom',
|
||||
templateUrl: './artifact-sbom.component.html',
|
||||
styleUrls: ['./artifact-sbom.component.scss'],
|
||||
})
|
||||
export class ArtifactSbomComponent implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
sbomLink: AdditionLink;
|
||||
@Input()
|
||||
projectName: string;
|
||||
@Input()
|
||||
projectId: number;
|
||||
@Input()
|
||||
repoName: string;
|
||||
@Input()
|
||||
sbomDigest: string;
|
||||
@Input() artifact: Artifact;
|
||||
|
||||
artifactSbom: ArtifactSbom;
|
||||
loading: boolean = false;
|
||||
hasEnabledSbom: 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 userPermissionService: UserPermissionService,
|
||||
private eventService: EventService,
|
||||
private session: SessionService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.getSbom();
|
||||
this.getSbomPermission();
|
||||
this.hasEnabledSbom = this.appConfigService.getConfig().sbom_enabled;
|
||||
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 &&
|
||||
this.sbomLink &&
|
||||
!this.sbomLink.absolute &&
|
||||
this.sbomLink.href
|
||||
) {
|
||||
if (!this.hasShowLoading) {
|
||||
this.loading = true;
|
||||
this.hasShowLoading = true;
|
||||
}
|
||||
const sbomAdditionParams = <ArtifactService.GetSbomAdditionParams>{
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
reference: this.sbomDigest,
|
||||
projectName: this.projectName,
|
||||
};
|
||||
this.artifactService
|
||||
.getSbomAddition(sbomAdditionParams)
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.loading = false;
|
||||
this.hasShowLoading = false;
|
||||
})
|
||||
)
|
||||
.subscribe(
|
||||
res => {
|
||||
this.artifactSbom = getArtifactSbom(res);
|
||||
},
|
||||
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.sbom_status ===
|
||||
SBOM_SCAN_STATUS.PENDING ||
|
||||
this.artifact.sbom_overview.sbom_status ===
|
||||
SBOM_SCAN_STATUS.RUNNING)
|
||||
);
|
||||
}
|
||||
|
||||
downloadSbom() {
|
||||
this.downloadSbomBtnState = ClrLoadingState.LOADING;
|
||||
if (
|
||||
this.artifact?.sbom_overview?.sbom_status ===
|
||||
SBOM_SCAN_STATUS.SUCCESS
|
||||
) {
|
||||
downloadJson(
|
||||
this.artifactSbom.sbomJsonRaw,
|
||||
`${this.artifactSbom.sbomName}.json`
|
||||
);
|
||||
}
|
||||
this.downloadSbomBtnState = ClrLoadingState.DEFAULT;
|
||||
}
|
||||
|
||||
canDownloadSbom(): boolean {
|
||||
return (
|
||||
this.hasEnabledSbom &&
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -170,8 +170,6 @@ export class ArtifactListPageService {
|
|||
this._hasDeleteImagePermission = results[2];
|
||||
this._hasScanImagePermission = results[3];
|
||||
this._hasSbomPermission = results?.[4] ?? false;
|
||||
// TODO need to remove the static code
|
||||
this._hasSbomPermission = true;
|
||||
},
|
||||
error => this.errorHandlerService.error(error)
|
||||
);
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
canScanNow() &&
|
||||
selectedRowHasVul() &&
|
||||
hasEnabledScanner &&
|
||||
hasScannerSupportVulnerability &&
|
||||
hasScanImagePermission
|
||||
)
|
||||
"
|
||||
|
@ -48,7 +49,15 @@
|
|||
[clrLoading]="generateSbomBtnState"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
[disabled]="true"
|
||||
[disabled]="
|
||||
!(
|
||||
canGenerateSbomNow() &&
|
||||
selectedRowHasSbom() &&
|
||||
hasEnabledSbom &&
|
||||
hasScannerSupportSBOM &&
|
||||
hasSbomPermission
|
||||
)
|
||||
"
|
||||
(click)="generateSbom()">
|
||||
<clr-icon shape="file" size="16"></clr-icon>
|
||||
<span>{{ 'SBOM.GENERATE' | translate }}</span>
|
||||
|
|
|
@ -46,6 +46,7 @@ import {
|
|||
ConfirmationButtons,
|
||||
ConfirmationState,
|
||||
ConfirmationTargets,
|
||||
ScanTypes,
|
||||
} from '../../../../../../../shared/entities/shared.const';
|
||||
import {
|
||||
operateChanges,
|
||||
|
@ -101,6 +102,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
projectName: string;
|
||||
repoName: string;
|
||||
registryUrl: string;
|
||||
sbomEnabled: boolean;
|
||||
artifactList: ArtifactFront[] = [];
|
||||
availableTime = AVAILABLE_TIME;
|
||||
inprogress: boolean;
|
||||
|
@ -190,7 +192,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
|
@ -238,6 +240,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
ngOnInit() {
|
||||
const appConfig = this.appConfigService.getConfig();
|
||||
this.registryUrl = appConfig.registry_url;
|
||||
this.sbomEnabled = appConfig.sbom_enabled;
|
||||
this.initRouterData();
|
||||
if (!this.updateArtifactSub) {
|
||||
this.updateArtifactSub = this.eventService.subscribe(
|
||||
|
@ -360,7 +363,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 +388,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 +438,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,
|
||||
|
@ -800,6 +804,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 +840,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 !!(
|
||||
|
@ -1019,18 +1059,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 +1077,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 +1119,6 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// return true if all selected rows are in "running" state
|
||||
canStopSbom(): boolean {
|
||||
if (this.onSendingStopSbomCommand) {
|
||||
|
@ -1084,7 +1127,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
if (this.selectedRow && this.selectedRow.length) {
|
||||
let flag: boolean = true;
|
||||
this.selectedRow.forEach(item => {
|
||||
const st: string = this.sbomStatus(item);
|
||||
const st: string = this.scanStatus(item);
|
||||
if (!this.isRunningState(st)) {
|
||||
flag = false;
|
||||
}
|
||||
|
@ -1142,6 +1185,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
deleteAccessory(a: Accessory) {
|
||||
let titleKey: string,
|
||||
summaryKey: string,
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
[projectId]="projectId"
|
||||
[repoName]="repositoryName"
|
||||
[digest]="artifactDigest"
|
||||
[sbomDigest]="sbomDigest"
|
||||
[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,7 @@ import {
|
|||
export class ArtifactSummaryComponent implements OnInit {
|
||||
tagId: string;
|
||||
artifactDigest: string;
|
||||
sbomDigest?: string;
|
||||
repositoryName: string;
|
||||
projectId: string | number;
|
||||
referArtifactNameArray: string[] = [];
|
||||
|
@ -37,10 +35,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 +95,7 @@ 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'];
|
||||
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,200 @@ 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 sbomJson[ArtifactSbomFieldMapper.sbomVersion] ||
|
||||
sbomJson[ArtifactSbomFieldMapper.sbomId]
|
||||
? true
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// only support to parse spdx json
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
withScanOverview: 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) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ScannerVo, SbomSummary } from '../../../../../../shared/services';
|
||||
|
@ -20,8 +20,9 @@ const SUCCESS_PCT: number = 100;
|
|||
templateUrl: './sbom-tip-histogram.component.html',
|
||||
styleUrls: ['./sbom-tip-histogram.component.scss'],
|
||||
})
|
||||
export class SbomTipHistogramComponent {
|
||||
export class SbomTipHistogramComponent implements OnInit {
|
||||
@Input() scanner: ScannerVo;
|
||||
_sbomPackages: number = 0;
|
||||
@Input() sbomSummary: SbomSummary = {
|
||||
scan_status: SBOM_SCAN_STATUS.NOT_GENERATED_SBOM,
|
||||
};
|
||||
|
@ -33,6 +34,22 @@ export class SbomTipHistogramComponent {
|
|||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._sbomPackages = this.sbomSummary?.summary?.total ?? 0;
|
||||
}
|
||||
|
||||
get sbomPackages(): number {
|
||||
return this._sbomPackages;
|
||||
}
|
||||
|
||||
get sevSummary(): { [key: string]: number } {
|
||||
if (this.sbomSummary && this.sbomSummary.summary) {
|
||||
return this.sbomSummary.summary.summary;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
duration(): string {
|
||||
if (this.sbomSummary && this.sbomSummary.duration) {
|
||||
let str = '';
|
||||
|
@ -54,6 +71,7 @@ export class SbomTipHistogramComponent {
|
|||
? `100%`
|
||||
: '0%';
|
||||
}
|
||||
|
||||
isLimitedSuccess(): boolean {
|
||||
return (
|
||||
this.sbomSummary && this.sbomSummary.complete_percent < SUCCESS_PCT
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -29,6 +29,7 @@ export class AppConfig {
|
|||
registry_storage_provider_name: string;
|
||||
read_only: boolean;
|
||||
show_popular_repo: boolean;
|
||||
sbom_enabled: boolean;
|
||||
banner_message: string;
|
||||
current_time: string;
|
||||
oidc_provider_name: string;
|
||||
|
@ -43,6 +44,7 @@ export class AppConfig {
|
|||
this.project_creation_restriction = 'everyone';
|
||||
this.self_registration = true;
|
||||
this.has_ca_root = false;
|
||||
this.sbom_enabled = false;
|
||||
this.harbor_version = 'unknown';
|
||||
this.clair_vulnerability_status = {
|
||||
overall_last_update: 0,
|
||||
|
|
|
@ -51,7 +51,7 @@ export class ArtifactDetailRoutingResolverService {
|
|||
projectName: project.name,
|
||||
withLabel: true,
|
||||
withScanOverview: true,
|
||||
// withSbomOverview: true,
|
||||
withSbomOverview: true,
|
||||
withTag: false,
|
||||
withImmutableStatus: true,
|
||||
}),
|
||||
|
|
|
@ -213,9 +213,12 @@ export interface VulnerabilitySummary {
|
|||
}
|
||||
export interface SbomSummary {
|
||||
report_id?: string;
|
||||
mime_type?: string;
|
||||
sbom_digest?: string;
|
||||
scan_status?: string;
|
||||
severity?: string;
|
||||
duration?: number;
|
||||
summary?: SeveritySummary;
|
||||
start_time?: Date;
|
||||
end_time?: Date;
|
||||
scanner?: ScannerVo;
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue