mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-07 08:27:43 +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).toBeTruthy();
|
||||||
expect(ruleNameInput.value.trim()).toEqual('sync_01');
|
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">
|
<ng-container *ngIf="additionLinks">
|
||||||
<h4 class="margin-bottom-025">{{ 'ARTIFACT.ADDITIONS' | translate }}</h4>
|
<h4 class="margin-bottom-025">{{ 'ARTIFACT.ADDITIONS' | translate }}</h4>
|
||||||
<div class="min-15">
|
<div class="min-15">
|
||||||
<clr-tabs>
|
<clr-tabs #additionsTab>
|
||||||
<clr-tab *ngIf="getVulnerability()">
|
<clr-tab *ngIf="getVulnerability()">
|
||||||
<button clrTabLink id="vulnerability">
|
<button
|
||||||
|
clrTabLink
|
||||||
|
id="vulnerability"
|
||||||
|
(click)="actionTab('vulnerability')">
|
||||||
{{ 'REPOSITORY.VULNERABILITY' | translate }}
|
{{ 'REPOSITORY.VULNERABILITY' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<clr-tab-content id="vulnerability-content" *clrIfActive>
|
<ng-template
|
||||||
<hbr-artifact-vulnerabilities
|
[clrIfActive]="currentTabLinkId === 'vulnerability'">
|
||||||
[artifact]="artifact"
|
<clr-tab-content id="vulnerability-content">
|
||||||
[projectName]="projectName"
|
<hbr-artifact-vulnerabilities
|
||||||
[projectId]="projectId"
|
[artifact]="artifact"
|
||||||
[repoName]="repoName"
|
[projectName]="projectName"
|
||||||
[digest]="digest"
|
[projectId]="projectId"
|
||||||
[vulnerabilitiesLink]="
|
[repoName]="repoName"
|
||||||
getVulnerability()
|
[digest]="digest"
|
||||||
"></hbr-artifact-vulnerabilities>
|
[vulnerabilitiesLink]="
|
||||||
</clr-tab-content>
|
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>
|
||||||
<clr-tab *ngIf="getBuildHistory()">
|
<clr-tab *ngIf="getBuildHistory()">
|
||||||
<button clrTabLink id="build-history">
|
<button
|
||||||
|
clrTabLink
|
||||||
|
id="build-history"
|
||||||
|
(click)="actionTab('build-history')">
|
||||||
{{ 'REPOSITORY.BUILD_HISTORY' | translate }}
|
{{ 'REPOSITORY.BUILD_HISTORY' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<clr-tab-content *clrIfActive>
|
<ng-template
|
||||||
<hbr-artifact-build-history
|
[clrIfActive]="currentTabLinkId === 'build-history'">
|
||||||
[buildHistoryLink]="
|
<clr-tab-content>
|
||||||
getBuildHistory()
|
<hbr-artifact-build-history
|
||||||
"></hbr-artifact-build-history>
|
[buildHistoryLink]="
|
||||||
</clr-tab-content>
|
getBuildHistory()
|
||||||
|
"></hbr-artifact-build-history>
|
||||||
|
</clr-tab-content>
|
||||||
|
</ng-template>
|
||||||
</clr-tab>
|
</clr-tab>
|
||||||
<clr-tab *ngIf="getSummary()">
|
<clr-tab *ngIf="getSummary()">
|
||||||
<button clrTabLink id="summary-link">
|
<button
|
||||||
|
clrTabLink
|
||||||
|
id="summary-link"
|
||||||
|
(click)="actionTab('summary-link')">
|
||||||
{{ 'ARTIFACT.SUMMARY' | translate }}
|
{{ 'ARTIFACT.SUMMARY' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<clr-tab-content id="summary-content" *clrIfActive>
|
<ng-template
|
||||||
<hbr-artifact-summary
|
[clrIfActive]="currentTabLinkId === 'summary-link'">
|
||||||
[summaryLink]="getSummary()"></hbr-artifact-summary>
|
<clr-tab-content id="summary-content">
|
||||||
</clr-tab-content>
|
<hbr-artifact-summary
|
||||||
|
[summaryLink]="getSummary()"></hbr-artifact-summary>
|
||||||
|
</clr-tab-content>
|
||||||
|
</ng-template>
|
||||||
</clr-tab>
|
</clr-tab>
|
||||||
<clr-tab *ngIf="getDependencies()">
|
<clr-tab *ngIf="getDependencies()">
|
||||||
<button clrTabLink id="depend-link">
|
<button
|
||||||
|
clrTabLink
|
||||||
|
id="depend-link"
|
||||||
|
(click)="actionTab('depend-link')">
|
||||||
{{ 'ARTIFACT.DEPENDENCIES' | translate }}
|
{{ 'ARTIFACT.DEPENDENCIES' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<clr-tab-content id="depend-content" *clrIfActive>
|
<ng-template [clrIfActive]="currentTabLinkId === 'depend-link'">
|
||||||
<hbr-artifact-dependencies
|
<clr-tab-content id="depend-content">
|
||||||
[dependenciesLink]="
|
<hbr-artifact-dependencies
|
||||||
getDependencies()
|
[dependenciesLink]="
|
||||||
"></hbr-artifact-dependencies>
|
getDependencies()
|
||||||
</clr-tab-content>
|
"></hbr-artifact-dependencies>
|
||||||
|
</clr-tab-content>
|
||||||
|
</ng-template>
|
||||||
</clr-tab>
|
</clr-tab>
|
||||||
<clr-tab *ngIf="getValues()">
|
<clr-tab *ngIf="getValues()">
|
||||||
<button clrTabLink id="value-link">
|
<button
|
||||||
|
clrTabLink
|
||||||
|
id="value-link"
|
||||||
|
(click)="actionTab('value-link')">
|
||||||
{{ 'ARTIFACT.VALUES' | translate }}
|
{{ 'ARTIFACT.VALUES' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<clr-tab-content id="value-content" *clrIfActive>
|
<ng-template [clrIfActive]="currentTabLinkId === 'value-link'">
|
||||||
<hbr-artifact-values
|
<clr-tab-content id="value-content">
|
||||||
[valuesLink]="getValues()"></hbr-artifact-values>
|
<hbr-artifact-values
|
||||||
</clr-tab-content>
|
[valuesLink]="getValues()"></hbr-artifact-values>
|
||||||
|
</clr-tab-content>
|
||||||
|
</ng-template>
|
||||||
</clr-tab>
|
</clr-tab>
|
||||||
</clr-tabs>
|
</clr-tabs>
|
||||||
</div>
|
</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 { ADDITIONS } from './models';
|
||||||
import { AdditionLinks } from '../../../../../../../ng-swagger-gen/models/addition-links';
|
import { AdditionLinks } from '../../../../../../../ng-swagger-gen/models/addition-links';
|
||||||
import { AdditionLink } from '../../../../../../../ng-swagger-gen/models/addition-link';
|
import { AdditionLink } from '../../../../../../../ng-swagger-gen/models/addition-link';
|
||||||
import { Artifact } from '../../../../../../../ng-swagger-gen/models/artifact';
|
import { Artifact } from '../../../../../../../ng-swagger-gen/models/artifact';
|
||||||
|
import { ClrTabs } from '@clr/angular';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'artifact-additions',
|
selector: 'artifact-additions',
|
||||||
templateUrl: './artifact-additions.component.html',
|
templateUrl: './artifact-additions.component.html',
|
||||||
styleUrls: ['./artifact-additions.component.scss'],
|
styleUrls: ['./artifact-additions.component.scss'],
|
||||||
})
|
})
|
||||||
export class ArtifactAdditionsComponent {
|
export class ArtifactAdditionsComponent implements AfterViewChecked, OnInit {
|
||||||
@Input() artifact: Artifact;
|
@Input() artifact: Artifact;
|
||||||
@Input() additionLinks: AdditionLinks;
|
@Input() additionLinks: AdditionLinks;
|
||||||
@Input() projectName: string;
|
@Input() projectName: string;
|
||||||
@ -19,7 +27,28 @@ export class ArtifactAdditionsComponent {
|
|||||||
repoName: string;
|
repoName: string;
|
||||||
@Input()
|
@Input()
|
||||||
digest: string;
|
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 {
|
getVulnerability(): AdditionLink {
|
||||||
if (
|
if (
|
||||||
@ -30,6 +59,12 @@ export class ArtifactAdditionsComponent {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
getSbom(): AdditionLink {
|
||||||
|
if (this.additionLinks && this.additionLinks[ADDITIONS.SBOMS]) {
|
||||||
|
return this.additionLinks[ADDITIONS.SBOMS];
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
getBuildHistory(): AdditionLink {
|
getBuildHistory(): AdditionLink {
|
||||||
if (this.additionLinks && this.additionLinks[ADDITIONS.BUILD_HISTORY]) {
|
if (this.additionLinks && this.additionLinks[ADDITIONS.BUILD_HISTORY]) {
|
||||||
return this.additionLinks[ADDITIONS.BUILD_HISTORY];
|
return this.additionLinks[ADDITIONS.BUILD_HISTORY];
|
||||||
@ -54,4 +89,8 @@ export class ArtifactAdditionsComponent {
|
|||||||
}
|
}
|
||||||
return null;
|
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,
|
USERSTATICPERMISSION,
|
||||||
} from '../../../../../shared/services';
|
} from '../../../../../shared/services';
|
||||||
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||||
|
import { Scanner } from '../../../../left-side-nav/interrogation-services/scanner/scanner';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ArtifactListPageService {
|
export class ArtifactListPageService {
|
||||||
@ -19,6 +20,7 @@ export class ArtifactListPageService {
|
|||||||
private _hasDeleteImagePermission: boolean = false;
|
private _hasDeleteImagePermission: boolean = false;
|
||||||
private _hasScanImagePermission: boolean = false;
|
private _hasScanImagePermission: boolean = false;
|
||||||
private _hasSbomPermission: boolean = false;
|
private _hasSbomPermission: boolean = false;
|
||||||
|
private _scanner: Scanner = undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private scanningService: ScanningResultService,
|
private scanningService: ScanningResultService,
|
||||||
@ -26,6 +28,10 @@ export class ArtifactListPageService {
|
|||||||
private errorHandlerService: ErrorHandler
|
private errorHandlerService: ErrorHandler
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
getProjectScanner(): Scanner {
|
||||||
|
return this._scanner;
|
||||||
|
}
|
||||||
|
|
||||||
getScanBtnState(): ClrLoadingState {
|
getScanBtnState(): ClrLoadingState {
|
||||||
return this._scanBtnState;
|
return this._scanBtnState;
|
||||||
}
|
}
|
||||||
@ -103,29 +109,28 @@ export class ArtifactListPageService {
|
|||||||
this._sbomBtnState = ClrLoadingState.LOADING;
|
this._sbomBtnState = ClrLoadingState.LOADING;
|
||||||
this.scanningService.getProjectScanner(projectId).subscribe(
|
this.scanningService.getProjectScanner(projectId).subscribe(
|
||||||
response => {
|
response => {
|
||||||
if (
|
if (response && '{}' !== JSON.stringify(response)) {
|
||||||
response &&
|
this._scanner = response;
|
||||||
'{}' !== JSON.stringify(response) &&
|
if (!response.disabled && response.health === 'healthy') {
|
||||||
!response.disabled &&
|
this.updateStates(
|
||||||
response.health === 'healthy'
|
true,
|
||||||
) {
|
ClrLoadingState.SUCCESS,
|
||||||
this.updateStates(
|
ClrLoadingState.SUCCESS
|
||||||
true,
|
);
|
||||||
ClrLoadingState.SUCCESS,
|
if (response?.capabilities) {
|
||||||
ClrLoadingState.SUCCESS
|
this.updateCapabilities(response?.capabilities);
|
||||||
);
|
}
|
||||||
if (response?.capabilities) {
|
} else {
|
||||||
this.updateCapabilities(response?.capabilities);
|
this.updateStates(
|
||||||
|
false,
|
||||||
|
ClrLoadingState.ERROR,
|
||||||
|
ClrLoadingState.ERROR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.updateStates(
|
|
||||||
false,
|
|
||||||
ClrLoadingState.ERROR,
|
|
||||||
ClrLoadingState.ERROR
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
|
this._scanner = null;
|
||||||
this.updateStates(
|
this.updateStates(
|
||||||
false,
|
false,
|
||||||
ClrLoadingState.ERROR,
|
ClrLoadingState.ERROR,
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
canScanNow() &&
|
canScanNow() &&
|
||||||
selectedRowHasVul() &&
|
selectedRowHasVul() &&
|
||||||
hasEnabledScanner &&
|
hasEnabledScanner &&
|
||||||
|
hasScannerSupportVulnerability &&
|
||||||
hasScanImagePermission
|
hasScanImagePermission
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@ -32,44 +33,26 @@
|
|||||||
>
|
>
|
||||||
<span>{{ 'VULNERABILITY.SCAN_NOW' | translate }}</span>
|
<span>{{ 'VULNERABILITY.SCAN_NOW' | translate }}</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
*ngIf="hasEnabledSbom()"
|
*ngIf="hasEnabledSbom()"
|
||||||
id="generate-sbom-btn"
|
id="generate-sbom-btn"
|
||||||
[clrLoading]="generateSbomBtnState"
|
[clrLoading]="generateSbomBtnState"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-secondary"
|
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]="
|
[disabled]="
|
||||||
!(
|
!(
|
||||||
canStopSbom() &&
|
canGenerateSbomNow() &&
|
||||||
|
selectedRowHasSbom() &&
|
||||||
|
hasEnabledScanner &&
|
||||||
hasSbomPermission &&
|
hasSbomPermission &&
|
||||||
hasEnabledScanner
|
hasScannerSupportSBOM
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
(click)="stopSbom()">
|
(click)="generateSbom()">
|
||||||
<clr-icon shape="stop" size="16"></clr-icon>
|
<clr-icon shape="sbom" size="16"></clr-icon>
|
||||||
<span>{{ 'SBOM.STOP' | translate }}</span>
|
<span>{{ 'SBOM.GENERATE' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<clr-dropdown class="btn btn-link" *ngIf="!depth">
|
<clr-dropdown class="btn btn-link" *ngIf="!depth">
|
||||||
<span
|
<span
|
||||||
clrDropdownTrigger
|
clrDropdownTrigger
|
||||||
@ -82,6 +65,45 @@
|
|||||||
class="action-dropdown"
|
class="action-dropdown"
|
||||||
clrPosition="bottom-left"
|
clrPosition="bottom-left"
|
||||||
*clrIfOpen>
|
*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
|
<div
|
||||||
id="artifact-list-copy-digest"
|
id="artifact-list-copy-digest"
|
||||||
class="action-dropdown-item no-border"
|
class="action-dropdown-item no-border"
|
||||||
@ -93,6 +115,10 @@
|
|||||||
(click)="showDigestId()">
|
(click)="showDigestId()">
|
||||||
{{ 'REPOSITORY.COPY_DIGEST_ID' | translate }}
|
{{ 'REPOSITORY.COPY_DIGEST_ID' | translate }}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-divider"
|
||||||
|
role="separator"
|
||||||
|
aria-hidden="true"></div>
|
||||||
<clr-dropdown>
|
<clr-dropdown>
|
||||||
<button
|
<button
|
||||||
id="artifact-list-add-labels"
|
id="artifact-list-add-labels"
|
||||||
@ -106,6 +132,10 @@
|
|||||||
">
|
">
|
||||||
{{ 'REPOSITORY.ADD_LABELS' | translate }}
|
{{ 'REPOSITORY.ADD_LABELS' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
<div
|
||||||
|
class="dropdown-divider"
|
||||||
|
role="separator"
|
||||||
|
aria-hidden="true"></div>
|
||||||
<clr-dropdown-menu
|
<clr-dropdown-menu
|
||||||
[hidden]="!selectedRow.length">
|
[hidden]="!selectedRow.length">
|
||||||
<div class="filter-grid">
|
<div class="filter-grid">
|
||||||
@ -432,7 +462,7 @@
|
|||||||
(submitStopFinish)="submitSbomStopFinish($event)"
|
(submitStopFinish)="submitSbomStopFinish($event)"
|
||||||
(scanFinished)="sbomFinished($event)"
|
(scanFinished)="sbomFinished($event)"
|
||||||
*ngIf="hasScannerSupportSBOM"
|
*ngIf="hasScannerSupportSBOM"
|
||||||
[inputScanner]="artifact?.sbom_overview?.scanner"
|
[inputScanner]="projectScanner"
|
||||||
(submitFinish)="submitSbomFinish($event)"
|
(submitFinish)="submitSbomFinish($event)"
|
||||||
[projectName]="projectName"
|
[projectName]="projectName"
|
||||||
[projectId]="projectId"
|
[projectId]="projectId"
|
||||||
|
@ -27,11 +27,17 @@ import { ArtifactModule } from '../../../artifact.module';
|
|||||||
import {
|
import {
|
||||||
SBOM_SCAN_STATUS,
|
SBOM_SCAN_STATUS,
|
||||||
VULNERABILITY_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', () => {
|
describe('ArtifactListTabComponent', () => {
|
||||||
let comp: ArtifactListTabComponent;
|
let comp: ArtifactListTabComponent;
|
||||||
let fixture: ComponentFixture<ArtifactListTabComponent>;
|
let fixture: ComponentFixture<ArtifactListTabComponent>;
|
||||||
|
const mockScanner = {
|
||||||
|
name: 'Trivy',
|
||||||
|
vendor: 'vm',
|
||||||
|
version: 'v1.2',
|
||||||
|
};
|
||||||
const mockActivatedRoute = {
|
const mockActivatedRoute = {
|
||||||
snapshot: {
|
snapshot: {
|
||||||
params: {
|
params: {
|
||||||
@ -274,6 +280,9 @@ describe('ArtifactListTabComponent', () => {
|
|||||||
hasScanImagePermission(): boolean {
|
hasScanImagePermission(): boolean {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
getProjectScanner(): Scanner {
|
||||||
|
return mockScanner;
|
||||||
|
},
|
||||||
init() {},
|
init() {},
|
||||||
};
|
};
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -384,7 +393,7 @@ describe('ArtifactListTabComponent', () => {
|
|||||||
fixture.nativeElement.querySelector('#generate-sbom-btn');
|
fixture.nativeElement.querySelector('#generate-sbom-btn');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
expect(generatedButton.disabled).toBeTruthy();
|
expect(generatedButton.disabled).toBeFalsy();
|
||||||
});
|
});
|
||||||
it('Stop SBOM button should be disabled', async () => {
|
it('Stop SBOM button should be disabled', async () => {
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
|
@ -84,6 +84,7 @@ import { Accessory } from 'ng-swagger-gen/models/accessory';
|
|||||||
import { Tag } from '../../../../../../../../../ng-swagger-gen/models/tag';
|
import { Tag } from '../../../../../../../../../ng-swagger-gen/models/tag';
|
||||||
import { CopyArtifactComponent } from './copy-artifact/copy-artifact.component';
|
import { CopyArtifactComponent } from './copy-artifact/copy-artifact.component';
|
||||||
import { CopyDigestComponent } from './copy-digest/copy-digest.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';
|
export const AVAILABLE_TIME = '0001-01-01T00:00:00.000Z';
|
||||||
|
|
||||||
@ -160,6 +161,10 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
get generateSbomBtnState(): ClrLoadingState {
|
get generateSbomBtnState(): ClrLoadingState {
|
||||||
return this.artifactListPageService.getSbomBtnState();
|
return this.artifactListPageService.getSbomBtnState();
|
||||||
}
|
}
|
||||||
|
get projectScanner(): Scanner {
|
||||||
|
return this.artifactListPageService.getProjectScanner();
|
||||||
|
}
|
||||||
|
|
||||||
onSendingScanCommand: boolean;
|
onSendingScanCommand: boolean;
|
||||||
onSendingStopScanCommand: boolean = false;
|
onSendingStopScanCommand: boolean = false;
|
||||||
onStopScanArtifactsLength: number = 0;
|
onStopScanArtifactsLength: number = 0;
|
||||||
@ -190,7 +195,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@ -269,6 +274,9 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (this.projectId) {
|
||||||
|
this.artifactListPageService.init(this.projectId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -360,7 +368,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
withImmutableStatus: true,
|
withImmutableStatus: true,
|
||||||
withLabel: true,
|
withLabel: true,
|
||||||
withScanOverview: true,
|
withScanOverview: true,
|
||||||
// withSbomOverview: true,
|
withSbomOverview: true,
|
||||||
withTag: false,
|
withTag: false,
|
||||||
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES,
|
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES,
|
||||||
withAccessory: false,
|
withAccessory: false,
|
||||||
@ -385,7 +393,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
withImmutableStatus: true,
|
withImmutableStatus: true,
|
||||||
withLabel: true,
|
withLabel: true,
|
||||||
withScanOverview: true,
|
withScanOverview: true,
|
||||||
// withSbomOverview: true,
|
withSbomOverview: true,
|
||||||
withTag: false,
|
withTag: false,
|
||||||
XAcceptVulnerabilities:
|
XAcceptVulnerabilities:
|
||||||
DEFAULT_SUPPORTED_MIME_TYPES,
|
DEFAULT_SUPPORTED_MIME_TYPES,
|
||||||
@ -435,6 +443,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
withLabel: true,
|
withLabel: true,
|
||||||
withScanOverview: true,
|
withScanOverview: true,
|
||||||
|
withSbomOverview: true,
|
||||||
withTag: false,
|
withTag: false,
|
||||||
sort: getSortingString(state),
|
sort: getSortingString(state),
|
||||||
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES,
|
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES,
|
||||||
@ -749,11 +758,15 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
if (this.activatedRoute.snapshot.queryParams[UN_LOGGED_PARAM] === YES) {
|
if (this.activatedRoute.snapshot.queryParams[UN_LOGGED_PARAM] === YES) {
|
||||||
this.router.navigate(relativeRouterLink, {
|
this.router.navigate(relativeRouterLink, {
|
||||||
relativeTo: this.activatedRoute,
|
relativeTo: this.activatedRoute,
|
||||||
queryParams: { [UN_LOGGED_PARAM]: YES },
|
queryParams: {
|
||||||
|
[UN_LOGGED_PARAM]: YES,
|
||||||
|
sbomDigest: artifact.sbomDigest ?? '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.router.navigate(relativeRouterLink, {
|
this.router.navigate(relativeRouterLink, {
|
||||||
relativeTo: this.activatedRoute,
|
relativeTo: this.activatedRoute,
|
||||||
|
queryParams: { sbomDigest: artifact.sbomDigest ?? '' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -800,6 +813,26 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
return false;
|
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
|
// Trigger scan
|
||||||
scanNow(): void {
|
scanNow(): void {
|
||||||
if (!this.selectedRow.length) {
|
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 {
|
selectedRowHasVul(): boolean {
|
||||||
return !!(
|
return !!(
|
||||||
@ -941,7 +990,6 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// when finished, remove it from selectedRow
|
// when finished, remove it from selectedRow
|
||||||
sbomFinished(artifact: Artifact) {
|
sbomFinished(artifact: Artifact) {
|
||||||
this.scanFinished(artifact);
|
this.scanFinished(artifact);
|
||||||
@ -1019,18 +1067,17 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkCosignAndSbomAsync(artifacts: ArtifactFront[]) {
|
checkCosignAndSbomAsync(artifacts: ArtifactFront[]) {
|
||||||
if (artifacts) {
|
if (artifacts) {
|
||||||
if (artifacts.length) {
|
if (artifacts.length) {
|
||||||
artifacts.forEach(item => {
|
artifacts.forEach(item => {
|
||||||
item.signed = CHECKING;
|
item.signed = CHECKING;
|
||||||
// const sbomOverview = item?.sbom_overview;
|
const sbomOverview = item?.sbom_overview;
|
||||||
// item.sbomDigest = sbomOverview?.sbom_digest;
|
item.sbomDigest = sbomOverview?.sbom_digest;
|
||||||
// let queryTypes = `${AccessoryType.COSIGN} ${AccessoryType.NOTATION}`;
|
let queryTypes = `${AccessoryType.COSIGN} ${AccessoryType.NOTATION}`;
|
||||||
// if (!item.sbomDigest) {
|
if (!item.sbomDigest) {
|
||||||
// queryTypes = `${queryTypes} ${AccessoryType.SBOM}`;
|
queryTypes = `${queryTypes} ${AccessoryType.SBOM}`;
|
||||||
// }
|
}
|
||||||
this.newArtifactService
|
this.newArtifactService
|
||||||
.listAccessories({
|
.listAccessories({
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
@ -1038,16 +1085,21 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
reference: item.digest,
|
reference: item.digest,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: ACCESSORY_PAGE_SIZE,
|
pageSize: ACCESSORY_PAGE_SIZE,
|
||||||
q: encodeURIComponent(
|
q: encodeURIComponent(`type={${queryTypes}}`),
|
||||||
`type={${AccessoryType.COSIGN} ${AccessoryType.NOTATION}}`
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: res => {
|
next: res => {
|
||||||
if (res?.length) {
|
item.signed = res?.filter(
|
||||||
item.signed = TRUE;
|
item => item.type !== AccessoryType.SBOM
|
||||||
} else {
|
)?.length
|
||||||
item.signed = FALSE;
|
? TRUE
|
||||||
|
: FALSE;
|
||||||
|
if (!item.sbomDigest) {
|
||||||
|
item.sbomDigest =
|
||||||
|
res?.filter(
|
||||||
|
item =>
|
||||||
|
item.type === AccessoryType.SBOM
|
||||||
|
)?.[0]?.digest ?? null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: err => {
|
error: err => {
|
||||||
@ -1075,7 +1127,6 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// return true if all selected rows are in "running" state
|
// return true if all selected rows are in "running" state
|
||||||
canStopSbom(): boolean {
|
canStopSbom(): boolean {
|
||||||
if (this.onSendingStopSbomCommand) {
|
if (this.onSendingStopSbomCommand) {
|
||||||
@ -1142,6 +1193,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAccessory(a: Accessory) {
|
deleteAccessory(a: Accessory) {
|
||||||
let titleKey: string,
|
let titleKey: string,
|
||||||
summaryKey: string,
|
summaryKey: string,
|
||||||
|
@ -59,6 +59,8 @@
|
|||||||
[projectId]="projectId"
|
[projectId]="projectId"
|
||||||
[repoName]="repositoryName"
|
[repoName]="repositoryName"
|
||||||
[digest]="artifactDigest"
|
[digest]="artifactDigest"
|
||||||
|
[sbomDigest]="sbomDigest"
|
||||||
|
[tab]="activeTab"
|
||||||
[additionLinks]="artifact?.addition_links"></artifact-additions>
|
[additionLinks]="artifact?.addition_links"></artifact-additions>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div *ngIf="loading" class="clr-row mt-3 center">
|
<div *ngIf="loading" class="clr-row mt-3 center">
|
||||||
|
@ -30,6 +30,8 @@ describe('ArtifactSummaryComponent', () => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const mockedSbomDigest =
|
||||||
|
'sha256:51a41cec9de9d62ee60e206f5a8a615a028a65653e45539990867417cb486285';
|
||||||
let component: ArtifactSummaryComponent;
|
let component: ArtifactSummaryComponent;
|
||||||
let fixture: ComponentFixture<ArtifactSummaryComponent>;
|
let fixture: ComponentFixture<ArtifactSummaryComponent>;
|
||||||
const mockActivatedRoute = {
|
const mockActivatedRoute = {
|
||||||
@ -42,6 +44,9 @@ describe('ArtifactSummaryComponent', () => {
|
|||||||
return of(null);
|
return of(null);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
queryParams: {
|
||||||
|
sbomDigest: mockedSbomDigest,
|
||||||
|
},
|
||||||
parent: {
|
parent: {
|
||||||
params: {
|
params: {
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -89,6 +94,7 @@ describe('ArtifactSummaryComponent', () => {
|
|||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
component.repositoryName = 'demo';
|
component.repositoryName = 'demo';
|
||||||
component.artifactDigest = 'sha: acf4234f';
|
component.artifactDigest = 'sha: acf4234f';
|
||||||
|
component.sbomDigest = mockedSbomDigest;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||||
import { Artifact } from '../../../../../../ng-swagger-gen/models/artifact';
|
import { Artifact } from '../../../../../../ng-swagger-gen/models/artifact';
|
||||||
import { ErrorHandler } from '../../../../shared/units/error-handler';
|
|
||||||
import { Label } from '../../../../../../ng-swagger-gen/models/label';
|
import { Label } from '../../../../../../ng-swagger-gen/models/label';
|
||||||
import { ProjectService } from '../../../../shared/services';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { AppConfigService } from '../../../../services/app-config.service';
|
|
||||||
import { Project } from '../../project';
|
import { Project } from '../../project';
|
||||||
import { artifactDefault } from './artifact';
|
import { artifactDefault } from './artifact';
|
||||||
import { SafeUrl } from '@angular/platform-browser';
|
import { SafeUrl } from '@angular/platform-browser';
|
||||||
@ -24,6 +21,8 @@ import {
|
|||||||
export class ArtifactSummaryComponent implements OnInit {
|
export class ArtifactSummaryComponent implements OnInit {
|
||||||
tagId: string;
|
tagId: string;
|
||||||
artifactDigest: string;
|
artifactDigest: string;
|
||||||
|
sbomDigest?: string;
|
||||||
|
activeTab?: string;
|
||||||
repositoryName: string;
|
repositoryName: string;
|
||||||
projectId: string | number;
|
projectId: string | number;
|
||||||
referArtifactNameArray: string[] = [];
|
referArtifactNameArray: string[] = [];
|
||||||
@ -37,10 +36,7 @@ export class ArtifactSummaryComponent implements OnInit {
|
|||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private projectService: ProjectService,
|
|
||||||
private errorHandler: ErrorHandler,
|
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private appConfigService: AppConfigService,
|
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private frontEndArtifactService: ArtifactService,
|
private frontEndArtifactService: ArtifactService,
|
||||||
private event: EventService
|
private event: EventService
|
||||||
@ -100,6 +96,8 @@ export class ArtifactSummaryComponent implements OnInit {
|
|||||||
this.repositoryName = this.route.snapshot.params['repo'];
|
this.repositoryName = this.route.snapshot.params['repo'];
|
||||||
this.artifactDigest = this.route.snapshot.params['digest'];
|
this.artifactDigest = this.route.snapshot.params['digest'];
|
||||||
this.projectId = this.route.snapshot.parent.params['id'];
|
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) {
|
if (this.repositoryName && this.artifactDigest) {
|
||||||
const resolverData = this.route.snapshot.data;
|
const resolverData = this.route.snapshot.data;
|
||||||
if (resolverData) {
|
if (resolverData) {
|
||||||
|
@ -12,6 +12,7 @@ import { SummaryComponent } from './artifact-additions/summary/summary.component
|
|||||||
import { DependenciesComponent } from './artifact-additions/dependencies/dependencies.component';
|
import { DependenciesComponent } from './artifact-additions/dependencies/dependencies.component';
|
||||||
import { BuildHistoryComponent } from './artifact-additions/build-history/build-history.component';
|
import { BuildHistoryComponent } from './artifact-additions/build-history/build-history.component';
|
||||||
import { ArtifactVulnerabilitiesComponent } from './artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.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 { ArtifactDefaultService, ArtifactService } from './artifact.service';
|
||||||
import { ArtifactDetailRoutingResolverService } from '../../../../services/routing-resolvers/artifact-detail-routing-resolver.service';
|
import { ArtifactDetailRoutingResolverService } from '../../../../services/routing-resolvers/artifact-detail-routing-resolver.service';
|
||||||
import { ResultBarChartComponent } from './vulnerability-scanning/result-bar-chart.component';
|
import { ResultBarChartComponent } from './vulnerability-scanning/result-bar-chart.component';
|
||||||
@ -80,6 +81,7 @@ const routes: Routes = [
|
|||||||
SummaryComponent,
|
SummaryComponent,
|
||||||
DependenciesComponent,
|
DependenciesComponent,
|
||||||
BuildHistoryComponent,
|
BuildHistoryComponent,
|
||||||
|
ArtifactSbomComponent,
|
||||||
ArtifactVulnerabilitiesComponent,
|
ArtifactVulnerabilitiesComponent,
|
||||||
ResultBarChartComponent,
|
ResultBarChartComponent,
|
||||||
ResultSbomComponent,
|
ResultSbomComponent,
|
||||||
|
@ -10,6 +10,7 @@ export interface ArtifactFront extends Artifact {
|
|||||||
annotationsArray?: Array<{ [key: string]: any }>;
|
annotationsArray?: Array<{ [key: string]: any }>;
|
||||||
tagNumber?: number;
|
tagNumber?: number;
|
||||||
signed?: string;
|
signed?: string;
|
||||||
|
sbomDigest?: string;
|
||||||
accessoryNumber?: number;
|
accessoryNumber?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +76,7 @@ export enum AccessoryType {
|
|||||||
COSIGN = 'signature.cosign',
|
COSIGN = 'signature.cosign',
|
||||||
NOTATION = 'signature.notation',
|
NOTATION = 'signature.notation',
|
||||||
NYDUS = 'accelerator.nydus',
|
NYDUS = 'accelerator.nydus',
|
||||||
|
SBOM = 'harbor.sbom',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ArtifactType {
|
export enum ArtifactType {
|
||||||
@ -166,3 +168,196 @@ export enum ClientNames {
|
|||||||
CHART = 'Helm',
|
CHART = 'Helm',
|
||||||
CNAB = 'CNAB',
|
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 { SBOMOverview } from './sbom-overview';
|
||||||
import { of, timer } from 'rxjs';
|
import { of, timer } from 'rxjs';
|
||||||
import { ArtifactService, ScanService } from 'ng-swagger-gen/services';
|
import { ArtifactService, ScanService } from 'ng-swagger-gen/services';
|
||||||
import { Artifact } from 'ng-swagger-gen/models';
|
|
||||||
|
|
||||||
describe('ResultSbomComponent (inline template)', () => {
|
describe('ResultSbomComponent (inline template)', () => {
|
||||||
let component: ResultSbomComponent;
|
let component: ResultSbomComponent;
|
||||||
@ -21,23 +20,18 @@ describe('ResultSbomComponent (inline template)', () => {
|
|||||||
};
|
};
|
||||||
const mockedSbomDigest =
|
const mockedSbomDigest =
|
||||||
'sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3';
|
'sha256:052240e8190b7057439d2bee1dffb9b37c8800e5c1af349f667635ae1debf8f3';
|
||||||
|
const mockScanner = {
|
||||||
|
name: 'Trivy',
|
||||||
|
vendor: 'vm',
|
||||||
|
version: 'v1.2',
|
||||||
|
};
|
||||||
const mockedSbomOverview = {
|
const mockedSbomOverview = {
|
||||||
report_id: '12345',
|
report_id: '12345',
|
||||||
scan_status: 'Error',
|
scan_status: 'Error',
|
||||||
scanner: {
|
|
||||||
name: 'Trivy',
|
|
||||||
vendor: 'vm',
|
|
||||||
version: 'v1.2',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const mockedCloneSbomOverview = {
|
const mockedCloneSbomOverview = {
|
||||||
report_id: '12346',
|
report_id: '12346',
|
||||||
scan_status: 'Pending',
|
scan_status: 'Pending',
|
||||||
scanner: {
|
|
||||||
name: 'Trivy',
|
|
||||||
vendor: 'vm',
|
|
||||||
version: 'v1.2',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const FakedScanService = {
|
const FakedScanService = {
|
||||||
scanArtifact: () => of({}),
|
scanArtifact: () => of({}),
|
||||||
@ -120,6 +114,7 @@ describe('ResultSbomComponent (inline template)', () => {
|
|||||||
fixture = TestBed.createComponent(ResultSbomComponent);
|
fixture = TestBed.createComponent(ResultSbomComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
component.repoName = 'mockRepo';
|
component.repoName = 'mockRepo';
|
||||||
|
component.inputScanner = mockScanner;
|
||||||
component.artifactDigest = mockedSbomDigest;
|
component.artifactDigest = mockedSbomDigest;
|
||||||
component.sbomDigest = mockedSbomDigest;
|
component.sbomDigest = mockedSbomDigest;
|
||||||
component.sbomOverview = mockData;
|
component.sbomOverview = mockData;
|
||||||
@ -180,9 +175,11 @@ describe('ResultSbomComponent (inline template)', () => {
|
|||||||
});
|
});
|
||||||
it('Test ResultSbomComponent getScanner', () => {
|
it('Test ResultSbomComponent getScanner', () => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
component.inputScanner = undefined;
|
||||||
expect(component.getScanner()).toBeUndefined();
|
expect(component.getScanner()).toBeUndefined();
|
||||||
|
component.inputScanner = mockScanner;
|
||||||
component.sbomOverview = mockedSbomOverview;
|
component.sbomOverview = mockedSbomOverview;
|
||||||
expect(component.getScanner()).toBe(mockedSbomOverview.scanner);
|
expect(component.getScanner()).toBe(mockScanner);
|
||||||
component.projectName = 'test';
|
component.projectName = 'test';
|
||||||
component.repoName = 'ui';
|
component.repoName = 'ui';
|
||||||
component.artifactDigest = 'dg';
|
component.artifactDigest = 'dg';
|
||||||
@ -239,7 +236,9 @@ describe('ResultSbomComponent (inline template)', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
fixture.whenStable().then(() => {
|
fixture.whenStable().then(() => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(component.stateCheckTimer).toBeUndefined();
|
expect(component.sbomOverview.scan_status).toBe(
|
||||||
|
SBOM_SCAN_STATUS.SUCCESS
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Subscription, timer } from 'rxjs';
|
import { Subscription, timer } from 'rxjs';
|
||||||
import { finalize } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
import { ScannerVo } from '../../../../../shared/services';
|
|
||||||
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||||
import {
|
import {
|
||||||
clone,
|
clone,
|
||||||
@ -27,6 +26,7 @@ import { ScanService } from '../../../../../../../ng-swagger-gen/services/scan.s
|
|||||||
import { ScanType } from 'ng-swagger-gen/models';
|
import { ScanType } from 'ng-swagger-gen/models';
|
||||||
import { ScanTypes } from '../../../../../shared/entities/shared.const';
|
import { ScanTypes } from '../../../../../shared/entities/shared.const';
|
||||||
import { SBOMOverview } from './sbom-overview';
|
import { SBOMOverview } from './sbom-overview';
|
||||||
|
import { Scanner } from '../../../../left-side-nav/interrogation-services/scanner/scanner';
|
||||||
const STATE_CHECK_INTERVAL: number = 3000; // 3s
|
const STATE_CHECK_INTERVAL: number = 3000; // 3s
|
||||||
const RETRY_TIMES: number = 3;
|
const RETRY_TIMES: number = 3;
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ const RETRY_TIMES: number = 3;
|
|||||||
styleUrls: ['./scanning.scss'],
|
styleUrls: ['./scanning.scss'],
|
||||||
})
|
})
|
||||||
export class ResultSbomComponent implements OnInit, OnDestroy {
|
export class ResultSbomComponent implements OnInit, OnDestroy {
|
||||||
@Input() inputScanner: ScannerVo;
|
@Input() inputScanner: Scanner;
|
||||||
@Input() repoName: string = '';
|
@Input() repoName: string = '';
|
||||||
@Input() projectName: string = '';
|
@Input() projectName: string = '';
|
||||||
@Input() projectId: string = '';
|
@Input() projectId: string = '';
|
||||||
@ -176,9 +176,9 @@ export class ResultSbomComponent implements OnInit, OnDestroy {
|
|||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
reference: this.artifactDigest,
|
reference: this.artifactDigest,
|
||||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
// scanType: <ScanType>{
|
scanType: <ScanType>{
|
||||||
// scan_type: ScanTypes.SBOM,
|
scan_type: ScanTypes.SBOM,
|
||||||
// },
|
},
|
||||||
})
|
})
|
||||||
.pipe(finalize(() => this.submitFinish.emit(false)))
|
.pipe(finalize(() => this.submitFinish.emit(false)))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
@ -219,15 +219,15 @@ export class ResultSbomComponent implements OnInit, OnDestroy {
|
|||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
reference: this.artifactDigest,
|
reference: this.artifactDigest,
|
||||||
// withSbomOverview: true,
|
withSbomOverview: true,
|
||||||
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES,
|
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES,
|
||||||
})
|
})
|
||||||
.subscribe(
|
.subscribe(
|
||||||
(artifact: Artifact) => {
|
(artifact: Artifact) => {
|
||||||
// To keep the same summary reference, use value copy.
|
// To keep the same summary reference, use value copy.
|
||||||
// if (artifact.sbom_overview) {
|
if (artifact.sbom_overview) {
|
||||||
// this.copyValue(artifact.sbom_overview);
|
this.copyValue(artifact.sbom_overview);
|
||||||
// }
|
}
|
||||||
if (!this.queued && !this.generating) {
|
if (!this.queued && !this.generating) {
|
||||||
// Scanning should be done
|
// Scanning should be done
|
||||||
if (this.stateCheckTimer) {
|
if (this.stateCheckTimer) {
|
||||||
@ -271,10 +271,7 @@ export class ResultSbomComponent implements OnInit, OnDestroy {
|
|||||||
}/scan/${this.sbomOverview.report_id}/log`;
|
}/scan/${this.sbomOverview.report_id}/log`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScanner(): ScannerVo {
|
getScanner(): Scanner {
|
||||||
if (this.sbomOverview && this.sbomOverview.scanner) {
|
|
||||||
return this.sbomOverview.scanner;
|
|
||||||
}
|
|
||||||
return this.inputScanner;
|
return this.inputScanner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
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 { SBOM_SCAN_STATUS } from '../../../../../../shared/units/utils';
|
||||||
import {
|
import {
|
||||||
UN_LOGGED_PARAM,
|
UN_LOGGED_PARAM,
|
||||||
@ -9,6 +9,7 @@ import {
|
|||||||
} from '../../../../../../account/sign-in/sign-in.service';
|
} from '../../../../../../account/sign-in/sign-in.service';
|
||||||
import { HAS_STYLE_MODE, StyleMode } from '../../../../../../services/theme';
|
import { HAS_STYLE_MODE, StyleMode } from '../../../../../../services/theme';
|
||||||
import { ScanTypes } from '../../../../../../shared/entities/shared.const';
|
import { ScanTypes } from '../../../../../../shared/entities/shared.const';
|
||||||
|
import { Scanner } from '../../../../../left-side-nav/interrogation-services/scanner/scanner';
|
||||||
|
|
||||||
const MIN = 60;
|
const MIN = 60;
|
||||||
const MIN_STR = 'min ';
|
const MIN_STR = 'min ';
|
||||||
@ -21,7 +22,7 @@ const SUCCESS_PCT: number = 100;
|
|||||||
styleUrls: ['./sbom-tip-histogram.component.scss'],
|
styleUrls: ['./sbom-tip-histogram.component.scss'],
|
||||||
})
|
})
|
||||||
export class SbomTipHistogramComponent {
|
export class SbomTipHistogramComponent {
|
||||||
@Input() scanner: ScannerVo;
|
@Input() scanner: Scanner;
|
||||||
@Input() sbomSummary: SbomSummary = {
|
@Input() sbomSummary: SbomSummary = {
|
||||||
scan_status: SBOM_SCAN_STATUS.NOT_GENERATED_SBOM,
|
scan_status: SBOM_SCAN_STATUS.NOT_GENERATED_SBOM,
|
||||||
};
|
};
|
||||||
@ -54,6 +55,7 @@ export class SbomTipHistogramComponent {
|
|||||||
? `100%`
|
? `100%`
|
||||||
: '0%';
|
: '0%';
|
||||||
}
|
}
|
||||||
|
|
||||||
isLimitedSuccess(): boolean {
|
isLimitedSuccess(): boolean {
|
||||||
return (
|
return (
|
||||||
this.sbomSummary && this.sbomSummary.complete_percent < SUCCESS_PCT
|
this.sbomSummary && this.sbomSummary.complete_percent < SUCCESS_PCT
|
||||||
|
@ -256,8 +256,8 @@ describe('ResultBarChartComponent (inline template)', () => {
|
|||||||
});
|
});
|
||||||
it('Test ResultBarChartComponent getSummary', () => {
|
it('Test ResultBarChartComponent getSummary', () => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
// component.summary.scan_status = VULNERABILITY_SCAN_STATUS.SUCCESS;
|
|
||||||
component.getSummary();
|
component.getSummary();
|
||||||
|
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.SUCCESS;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
fixture.whenStable().then(() => {
|
fixture.whenStable().then(() => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
@ -173,9 +173,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
|||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
reference: this.artifactDigest,
|
reference: this.artifactDigest,
|
||||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
// scanType: <ScanType>{
|
scanType: <ScanType>{
|
||||||
// scan_type: ScanTypes.VULNERABILITY,
|
scan_type: ScanTypes.VULNERABILITY,
|
||||||
// },
|
},
|
||||||
})
|
})
|
||||||
.pipe(finalize(() => this.submitFinish.emit(false)))
|
.pipe(finalize(() => this.submitFinish.emit(false)))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
@ -51,7 +51,7 @@ export class ArtifactDetailRoutingResolverService {
|
|||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
withLabel: true,
|
withLabel: true,
|
||||||
withScanOverview: true,
|
withScanOverview: true,
|
||||||
// withSbomOverview: true,
|
withSbomOverview: true,
|
||||||
withTag: false,
|
withTag: false,
|
||||||
withImmutableStatus: true,
|
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,
|
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"/>
|
.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>`,
|
<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({
|
@NgModule({
|
||||||
|
@ -252,6 +252,18 @@ export const DEFAULT_PAGE_SIZE: number = 15;
|
|||||||
*/
|
*/
|
||||||
export const DEFAULT_SUPPORTED_MIME_TYPES =
|
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';
|
'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
|
* the property name of vulnerability database updated time
|
||||||
@ -483,6 +495,26 @@ export function downloadFile(fileData) {
|
|||||||
a.remove();
|
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(
|
export function getChanges(
|
||||||
original: any,
|
original: any,
|
||||||
afterChange: any
|
afterChange: any
|
||||||
@ -1030,6 +1062,7 @@ export enum PageSizeMapKeys {
|
|||||||
ARTIFACT_LIST_TAB_COMPONENT = 'ArtifactListTabComponent',
|
ARTIFACT_LIST_TAB_COMPONENT = 'ArtifactListTabComponent',
|
||||||
ARTIFACT_TAGS_COMPONENT = 'ArtifactTagComponent',
|
ARTIFACT_TAGS_COMPONENT = 'ArtifactTagComponent',
|
||||||
ARTIFACT_VUL_COMPONENT = 'ArtifactVulnerabilitiesComponent',
|
ARTIFACT_VUL_COMPONENT = 'ArtifactVulnerabilitiesComponent',
|
||||||
|
ARTIFACT_SBOM_COMPONENT = 'ArtifactSbomComponent',
|
||||||
MEMBER_COMPONENT = 'MemberComponent',
|
MEMBER_COMPONENT = 'MemberComponent',
|
||||||
LABEL_COMPONENT = 'LabelComponent',
|
LABEL_COMPONENT = 'LabelComponent',
|
||||||
P2P_POLICY_COMPONENT = 'P2pPolicyComponent',
|
P2P_POLICY_COMPONENT = 'P2pPolicyComponent',
|
||||||
|
@ -804,6 +804,7 @@
|
|||||||
"FILTER_BY_LABEL": "Images nach Label filtern",
|
"FILTER_BY_LABEL": "Images nach Label filtern",
|
||||||
"FILTER_ARTIFACT_BY_LABEL": "Artefakte nach Label filtern",
|
"FILTER_ARTIFACT_BY_LABEL": "Artefakte nach Label filtern",
|
||||||
"ADD_LABELS": "Label hinzufügen",
|
"ADD_LABELS": "Label hinzufügen",
|
||||||
|
"STOP": "Stop",
|
||||||
"RETAG": "Kopieren",
|
"RETAG": "Kopieren",
|
||||||
"ACTION": "AKTION",
|
"ACTION": "AKTION",
|
||||||
"DEPLOY": "Bereitstellen",
|
"DEPLOY": "Bereitstellen",
|
||||||
@ -1057,7 +1058,7 @@
|
|||||||
"NO_SBOM": "No SBOM",
|
"NO_SBOM": "No SBOM",
|
||||||
"PACKAGES": "SBOM",
|
"PACKAGES": "SBOM",
|
||||||
"REPORTED_BY": "Reported by {{scanner}}",
|
"REPORTED_BY": "Reported by {{scanner}}",
|
||||||
"GENERATE": "Create SBOM",
|
"GENERATE": "Generate SBOM",
|
||||||
"DOWNLOAD": "Download SBOM",
|
"DOWNLOAD": "Download SBOM",
|
||||||
"Details": "SBOM details",
|
"Details": "SBOM details",
|
||||||
"STOP": "Stop SBOM",
|
"STOP": "Stop SBOM",
|
||||||
@ -1107,7 +1108,7 @@
|
|||||||
"PLACEHOLDER": "Filter Schwachstellen",
|
"PLACEHOLDER": "Filter Schwachstellen",
|
||||||
"PACKAGE": "Paket",
|
"PACKAGE": "Paket",
|
||||||
"PACKAGES": "Pakete",
|
"PACKAGES": "Pakete",
|
||||||
"SCAN_NOW": "Scan",
|
"SCAN_NOW": "Scan vulnerability",
|
||||||
"SCAN_BY": "SCAN DURCH {{scanner}}",
|
"SCAN_BY": "SCAN DURCH {{scanner}}",
|
||||||
"REPORTED_BY": "GEMELDET VON {{scanner}}",
|
"REPORTED_BY": "GEMELDET VON {{scanner}}",
|
||||||
"NO_SCANNER": "KEIN SCANNER",
|
"NO_SCANNER": "KEIN SCANNER",
|
||||||
|
@ -805,6 +805,7 @@
|
|||||||
"FILTER_BY_LABEL": "Filter images by label",
|
"FILTER_BY_LABEL": "Filter images by label",
|
||||||
"FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label",
|
"FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label",
|
||||||
"ADD_LABELS": "Add Labels",
|
"ADD_LABELS": "Add Labels",
|
||||||
|
"STOP": "Stop",
|
||||||
"RETAG": "Copy",
|
"RETAG": "Copy",
|
||||||
"ACTION": "ACTION",
|
"ACTION": "ACTION",
|
||||||
"DEPLOY": "DEPLOY",
|
"DEPLOY": "DEPLOY",
|
||||||
@ -1058,7 +1059,7 @@
|
|||||||
"NO_SBOM": "No SBOM",
|
"NO_SBOM": "No SBOM",
|
||||||
"PACKAGES": "SBOM",
|
"PACKAGES": "SBOM",
|
||||||
"REPORTED_BY": "Reported by {{scanner}}",
|
"REPORTED_BY": "Reported by {{scanner}}",
|
||||||
"GENERATE": "Create SBOM",
|
"GENERATE": "Generate SBOM ",
|
||||||
"DOWNLOAD": "Download SBOM",
|
"DOWNLOAD": "Download SBOM",
|
||||||
"Details": "SBOM details",
|
"Details": "SBOM details",
|
||||||
"STOP": "Stop SBOM",
|
"STOP": "Stop SBOM",
|
||||||
@ -1108,7 +1109,7 @@
|
|||||||
"PLACEHOLDER": "Filter Vulnerabilities",
|
"PLACEHOLDER": "Filter Vulnerabilities",
|
||||||
"PACKAGE": "package",
|
"PACKAGE": "package",
|
||||||
"PACKAGES": "packages",
|
"PACKAGES": "packages",
|
||||||
"SCAN_NOW": "Scan",
|
"SCAN_NOW": "Scan vulnerability ",
|
||||||
"SCAN_BY": "SCAN BY {{scanner}}",
|
"SCAN_BY": "SCAN BY {{scanner}}",
|
||||||
"REPORTED_BY": "Reported by {{scanner}}",
|
"REPORTED_BY": "Reported by {{scanner}}",
|
||||||
"NO_SCANNER": "NO SCANNER",
|
"NO_SCANNER": "NO SCANNER",
|
||||||
|
@ -805,6 +805,7 @@
|
|||||||
"FILTER_BY_LABEL": "Filter images by label",
|
"FILTER_BY_LABEL": "Filter images by label",
|
||||||
"FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label",
|
"FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label",
|
||||||
"ADD_LABELS": "Add Labels",
|
"ADD_LABELS": "Add Labels",
|
||||||
|
"STOP": "Stop",
|
||||||
"RETAG": "Copy",
|
"RETAG": "Copy",
|
||||||
"ACTION": "ACTION",
|
"ACTION": "ACTION",
|
||||||
"DEPLOY": "DEPLOY",
|
"DEPLOY": "DEPLOY",
|
||||||
@ -1056,7 +1057,7 @@
|
|||||||
"NO_SBOM": "No SBOM",
|
"NO_SBOM": "No SBOM",
|
||||||
"PACKAGES": "SBOM",
|
"PACKAGES": "SBOM",
|
||||||
"REPORTED_BY": "Reported by {{scanner}}",
|
"REPORTED_BY": "Reported by {{scanner}}",
|
||||||
"GENERATE": "Create SBOM",
|
"GENERATE": "Generate SBOM",
|
||||||
"DOWNLOAD": "Download SBOM",
|
"DOWNLOAD": "Download SBOM",
|
||||||
"Details": "SBOM details",
|
"Details": "SBOM details",
|
||||||
"STOP": "Stop SBOM",
|
"STOP": "Stop SBOM",
|
||||||
@ -1106,7 +1107,7 @@
|
|||||||
"PLACEHOLDER": "Filter Vulnerabilities",
|
"PLACEHOLDER": "Filter Vulnerabilities",
|
||||||
"PACKAGE": "package",
|
"PACKAGE": "package",
|
||||||
"PACKAGES": "packages",
|
"PACKAGES": "packages",
|
||||||
"SCAN_NOW": "Scan",
|
"SCAN_NOW": "Scan vulnerability",
|
||||||
"SCAN_BY": "SCAN BY {{scanner}}",
|
"SCAN_BY": "SCAN BY {{scanner}}",
|
||||||
"REPORTED_BY": "Reported by {{scanner}}",
|
"REPORTED_BY": "Reported by {{scanner}}",
|
||||||
"NO_SCANNER": "NO SCANNER",
|
"NO_SCANNER": "NO SCANNER",
|
||||||
|
@ -804,6 +804,7 @@
|
|||||||
"FILTER_BY_LABEL": "Filtrer les images par label",
|
"FILTER_BY_LABEL": "Filtrer les images par label",
|
||||||
"FILTER_ARTIFACT_BY_LABEL": "Filtrer les artefact par label",
|
"FILTER_ARTIFACT_BY_LABEL": "Filtrer les artefact par label",
|
||||||
"ADD_LABELS": "Ajouter des labels",
|
"ADD_LABELS": "Ajouter des labels",
|
||||||
|
"STOP": "Stop",
|
||||||
"RETAG": "Copier",
|
"RETAG": "Copier",
|
||||||
"ACTION": "Action",
|
"ACTION": "Action",
|
||||||
"DEPLOY": "Déployer",
|
"DEPLOY": "Déployer",
|
||||||
@ -1056,7 +1057,7 @@
|
|||||||
"NO_SBOM": "No SBOM",
|
"NO_SBOM": "No SBOM",
|
||||||
"PACKAGES": "SBOM",
|
"PACKAGES": "SBOM",
|
||||||
"REPORTED_BY": "Reported by {{scanner}}",
|
"REPORTED_BY": "Reported by {{scanner}}",
|
||||||
"GENERATE": "Create SBOM",
|
"GENERATE": "Generate SBOM",
|
||||||
"DOWNLOAD": "Download SBOM",
|
"DOWNLOAD": "Download SBOM",
|
||||||
"Details": "SBOM details",
|
"Details": "SBOM details",
|
||||||
"STOP": "Stop SBOM",
|
"STOP": "Stop SBOM",
|
||||||
@ -1106,12 +1107,12 @@
|
|||||||
"PLACEHOLDER": "Filtrer les vulnérabilités",
|
"PLACEHOLDER": "Filtrer les vulnérabilités",
|
||||||
"PACKAGE": "paquet",
|
"PACKAGE": "paquet",
|
||||||
"PACKAGES": "paquets",
|
"PACKAGES": "paquets",
|
||||||
"SCAN_NOW": "Analyser",
|
"SCAN_NOW": "Scan vulnerability",
|
||||||
"SCAN_BY": "Scan par {{scanner}}",
|
"SCAN_BY": "Scan par {{scanner}}",
|
||||||
"REPORTED_BY": "Rapporté par {{scanner}}",
|
"REPORTED_BY": "Rapporté par {{scanner}}",
|
||||||
"NO_SCANNER": "Aucun scanneur",
|
"NO_SCANNER": "Aucun scanneur",
|
||||||
"TRIGGER_STOP_SUCCESS": "Déclenchement avec succès de l'arrêt d'analyse",
|
"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": {
|
"PUSH_IMAGE": {
|
||||||
"TITLE": "Commande de push",
|
"TITLE": "Commande de push",
|
||||||
|
@ -802,6 +802,7 @@
|
|||||||
"FILTER_BY_LABEL": "라벨별로 이미지 필터",
|
"FILTER_BY_LABEL": "라벨별로 이미지 필터",
|
||||||
"FILTER_ARTIFACT_BY_LABEL": "라벨별로 아티팩트 필터",
|
"FILTER_ARTIFACT_BY_LABEL": "라벨별로 아티팩트 필터",
|
||||||
"ADD_LABELS": "라벨 추가",
|
"ADD_LABELS": "라벨 추가",
|
||||||
|
"STOP": "Stop",
|
||||||
"RETAG": "복사",
|
"RETAG": "복사",
|
||||||
"ACTION": "동작",
|
"ACTION": "동작",
|
||||||
"DEPLOY": "배포",
|
"DEPLOY": "배포",
|
||||||
@ -1055,7 +1056,7 @@
|
|||||||
"NO_SBOM": "No SBOM",
|
"NO_SBOM": "No SBOM",
|
||||||
"PACKAGES": "SBOM",
|
"PACKAGES": "SBOM",
|
||||||
"REPORTED_BY": "Reported by {{scanner}}",
|
"REPORTED_BY": "Reported by {{scanner}}",
|
||||||
"GENERATE": "Create SBOM",
|
"GENERATE": "Generate SBOM",
|
||||||
"DOWNLOAD": "Download SBOM",
|
"DOWNLOAD": "Download SBOM",
|
||||||
"Details": "SBOM details",
|
"Details": "SBOM details",
|
||||||
"STOP": "Stop SBOM",
|
"STOP": "Stop SBOM",
|
||||||
@ -1105,12 +1106,12 @@
|
|||||||
"PLACEHOLDER": "취약점 필터",
|
"PLACEHOLDER": "취약점 필터",
|
||||||
"PACKAGE": "패키지",
|
"PACKAGE": "패키지",
|
||||||
"PACKAGES": "패키지들",
|
"PACKAGES": "패키지들",
|
||||||
"SCAN_NOW": "스캔",
|
"SCAN_NOW": "Scan vulnerability",
|
||||||
"SCAN_BY": "{{scanner}로 스캔",
|
"SCAN_BY": "{{scanner}로 스캔",
|
||||||
"REPORTED_BY": "{{scanner}}로 보고 됨",
|
"REPORTED_BY": "{{scanner}}로 보고 됨",
|
||||||
"NO_SCANNER": "스캐너 없음",
|
"NO_SCANNER": "스캐너 없음",
|
||||||
"TRIGGER_STOP_SUCCESS": "트리거 중지 스캔이 성공적으로 수행되었습니다",
|
"TRIGGER_STOP_SUCCESS": "트리거 중지 스캔이 성공적으로 수행되었습니다",
|
||||||
"STOP_NOW": "스캔 중지"
|
"STOP_NOW": "Stop Scan"
|
||||||
},
|
},
|
||||||
"PUSH_IMAGE": {
|
"PUSH_IMAGE": {
|
||||||
"TITLE": "푸시 명령어",
|
"TITLE": "푸시 명령어",
|
||||||
|
@ -803,6 +803,7 @@
|
|||||||
"FILTER_BY_LABEL": "Filtrar imagens por marcadores",
|
"FILTER_BY_LABEL": "Filtrar imagens por marcadores",
|
||||||
"FILTER_ARTIFACT_BY_LABEL": "Filtrar por marcador",
|
"FILTER_ARTIFACT_BY_LABEL": "Filtrar por marcador",
|
||||||
"ADD_LABELS": "Adicionar Marcadores",
|
"ADD_LABELS": "Adicionar Marcadores",
|
||||||
|
"STOP": "Stop",
|
||||||
"RETAG": "Copiar",
|
"RETAG": "Copiar",
|
||||||
"ACTION": "AÇÃO",
|
"ACTION": "AÇÃO",
|
||||||
"DEPLOY": "IMPLANTAR",
|
"DEPLOY": "IMPLANTAR",
|
||||||
@ -1054,7 +1055,7 @@
|
|||||||
"NO_SBOM": "No SBOM",
|
"NO_SBOM": "No SBOM",
|
||||||
"PACKAGES": "SBOM",
|
"PACKAGES": "SBOM",
|
||||||
"REPORTED_BY": "Reported by {{scanner}}",
|
"REPORTED_BY": "Reported by {{scanner}}",
|
||||||
"GENERATE": "Create SBOM",
|
"GENERATE": "Generate SBOM",
|
||||||
"DOWNLOAD": "Download SBOM",
|
"DOWNLOAD": "Download SBOM",
|
||||||
"Details": "SBOM details",
|
"Details": "SBOM details",
|
||||||
"STOP": "Stop SBOM",
|
"STOP": "Stop SBOM",
|
||||||
@ -1104,12 +1105,12 @@
|
|||||||
"PLACEHOLDER": "Filtrar",
|
"PLACEHOLDER": "Filtrar",
|
||||||
"PACKAGE": "pacote",
|
"PACKAGE": "pacote",
|
||||||
"PACKAGES": "pacotes",
|
"PACKAGES": "pacotes",
|
||||||
"SCAN_NOW": "Examinar",
|
"SCAN_NOW": "Scan vulnerability",
|
||||||
"SCAN_BY": "EXAMINAR COM {{scanner}}",
|
"SCAN_BY": "EXAMINAR COM {{scanner}}",
|
||||||
"REPORTED_BY": "Encontrado com {{scanner}}",
|
"REPORTED_BY": "Encontrado com {{scanner}}",
|
||||||
"NO_SCANNER": "NENHUM",
|
"NO_SCANNER": "NENHUM",
|
||||||
"TRIGGER_STOP_SUCCESS": "Exame foi interrompido",
|
"TRIGGER_STOP_SUCCESS": "Exame foi interrompido",
|
||||||
"STOP_NOW": "Interromper"
|
"STOP_NOW": "Stop Scan"
|
||||||
},
|
},
|
||||||
"PUSH_IMAGE": {
|
"PUSH_IMAGE": {
|
||||||
"TITLE": "Comando Push",
|
"TITLE": "Comando Push",
|
||||||
|
@ -804,6 +804,7 @@
|
|||||||
"FILTER_BY_LABEL": "İmajları etikete göre filtrele",
|
"FILTER_BY_LABEL": "İmajları etikete göre filtrele",
|
||||||
"FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label",
|
"FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label",
|
||||||
"ADD_LABELS": "Etiketler Ekle",
|
"ADD_LABELS": "Etiketler Ekle",
|
||||||
|
"STOP": "Stop",
|
||||||
"RETAG": "Copy",
|
"RETAG": "Copy",
|
||||||
"ACTION": "AKSİYON",
|
"ACTION": "AKSİYON",
|
||||||
"DEPLOY": "YÜKLE",
|
"DEPLOY": "YÜKLE",
|
||||||
@ -1057,7 +1058,7 @@
|
|||||||
"NO_SBOM": "No SBOM",
|
"NO_SBOM": "No SBOM",
|
||||||
"PACKAGES": "SBOM",
|
"PACKAGES": "SBOM",
|
||||||
"REPORTED_BY": "Reported by {{scanner}}",
|
"REPORTED_BY": "Reported by {{scanner}}",
|
||||||
"GENERATE": "Create SBOM",
|
"GENERATE": "Generate SBOM",
|
||||||
"DOWNLOAD": "Download SBOM",
|
"DOWNLOAD": "Download SBOM",
|
||||||
"Details": "SBOM details",
|
"Details": "SBOM details",
|
||||||
"STOP": "Stop SBOM",
|
"STOP": "Stop SBOM",
|
||||||
@ -1107,7 +1108,7 @@
|
|||||||
"PLACEHOLDER": "Güvenlik Açıklarını Filtrele",
|
"PLACEHOLDER": "Güvenlik Açıklarını Filtrele",
|
||||||
"PACKAGE": "paket",
|
"PACKAGE": "paket",
|
||||||
"PACKAGES": "paketler",
|
"PACKAGES": "paketler",
|
||||||
"SCAN_NOW": "Tara",
|
"SCAN_NOW": "Scan vulnerability",
|
||||||
"SCAN_BY": "SCAN BY {{scanner}}",
|
"SCAN_BY": "SCAN BY {{scanner}}",
|
||||||
"REPORTED_BY": "Reported by {{scanner}}",
|
"REPORTED_BY": "Reported by {{scanner}}",
|
||||||
"NO_SCANNER": "NO SCANNER",
|
"NO_SCANNER": "NO SCANNER",
|
||||||
|
@ -801,6 +801,7 @@
|
|||||||
"LABELS": "标签",
|
"LABELS": "标签",
|
||||||
"ADD_LABEL_TO_IMAGE": "添加标签到此镜像",
|
"ADD_LABEL_TO_IMAGE": "添加标签到此镜像",
|
||||||
"ADD_LABELS": "添加标签",
|
"ADD_LABELS": "添加标签",
|
||||||
|
"STOP": "Stop",
|
||||||
"RETAG": "拷贝",
|
"RETAG": "拷贝",
|
||||||
"FILTER_BY_LABEL": "过滤标签",
|
"FILTER_BY_LABEL": "过滤标签",
|
||||||
"FILTER_ARTIFACT_BY_LABEL": "通过标签过滤Artifact",
|
"FILTER_ARTIFACT_BY_LABEL": "通过标签过滤Artifact",
|
||||||
@ -1055,7 +1056,7 @@
|
|||||||
"NO_SBOM": "No SBOM",
|
"NO_SBOM": "No SBOM",
|
||||||
"PACKAGES": "SBOM",
|
"PACKAGES": "SBOM",
|
||||||
"REPORTED_BY": "Reported by {{scanner}}",
|
"REPORTED_BY": "Reported by {{scanner}}",
|
||||||
"GENERATE": "Create SBOM",
|
"GENERATE": "Generate SBOM",
|
||||||
"DOWNLOAD": "Download SBOM",
|
"DOWNLOAD": "Download SBOM",
|
||||||
"Details": "SBOM details",
|
"Details": "SBOM details",
|
||||||
"STOP": "Stop SBOM",
|
"STOP": "Stop SBOM",
|
||||||
@ -1105,7 +1106,7 @@
|
|||||||
"PLACEHOLDER": "过滤漏洞",
|
"PLACEHOLDER": "过滤漏洞",
|
||||||
"PACKAGE": "组件",
|
"PACKAGE": "组件",
|
||||||
"PACKAGES": "组件",
|
"PACKAGES": "组件",
|
||||||
"SCAN_NOW": "扫描",
|
"SCAN_NOW": "Scan vulnerability",
|
||||||
"SCAN_BY": "使用 {{scanner}} 进行扫描",
|
"SCAN_BY": "使用 {{scanner}} 进行扫描",
|
||||||
"REPORTED_BY": "结果由 {{scanner}} 提供",
|
"REPORTED_BY": "结果由 {{scanner}} 提供",
|
||||||
"NO_SCANNER": "无扫描器",
|
"NO_SCANNER": "无扫描器",
|
||||||
|
@ -801,6 +801,7 @@
|
|||||||
"LABELS": "標籤",
|
"LABELS": "標籤",
|
||||||
"ADD_LABEL_TO_IMAGE": "新增標籤到此映像檔",
|
"ADD_LABEL_TO_IMAGE": "新增標籤到此映像檔",
|
||||||
"ADD_LABELS": "新增標籤",
|
"ADD_LABELS": "新增標籤",
|
||||||
|
"STOP": "Stop",
|
||||||
"RETAG": "複製",
|
"RETAG": "複製",
|
||||||
"FILTER_BY_LABEL": "篩選標籤",
|
"FILTER_BY_LABEL": "篩選標籤",
|
||||||
"FILTER_ARTIFACT_BY_LABEL": "透過標籤篩選 Artifact",
|
"FILTER_ARTIFACT_BY_LABEL": "透過標籤篩選 Artifact",
|
||||||
@ -1054,7 +1055,7 @@
|
|||||||
"NO_SBOM": "No SBOM",
|
"NO_SBOM": "No SBOM",
|
||||||
"PACKAGES": "SBOM",
|
"PACKAGES": "SBOM",
|
||||||
"REPORTED_BY": "Reported by {{scanner}}",
|
"REPORTED_BY": "Reported by {{scanner}}",
|
||||||
"GENERATE": "Create SBOM",
|
"GENERATE": "Generate SBOM",
|
||||||
"DOWNLOAD": "Download SBOM",
|
"DOWNLOAD": "Download SBOM",
|
||||||
"Details": "SBOM details",
|
"Details": "SBOM details",
|
||||||
"STOP": "Stop SBOM",
|
"STOP": "Stop SBOM",
|
||||||
|
Loading…
Reference in New Issue
Block a user