Modify UI to match scanner upgrading (#13915)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Will Sun 2021-01-07 17:16:52 +08:00 committed by GitHub
parent 0cf43d766c
commit 4ea881564e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 150 additions and 441 deletions

View File

@ -39,7 +39,6 @@ import { ScannerComponent } from "./scanner/scanner.component";
import { ConfigScannerService } from "../config/scanner/config-scanner.service";
import { RepositoryGridviewComponent } from "./repository/repository-gridview.component";
import { ResultTipHistogramComponent } from "./repository/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component";
import { ResultGridComponent } from "./repository/vulnerability-scanning/result-grid.component";
import { ResultBarChartComponent } from "./repository/vulnerability-scanning/result-bar-chart.component";
import { HistogramChartComponent } from "./repository/vulnerability-scanning/histogram-chart/histogram-chart.component";
import { ResultTipComponent } from "./repository/vulnerability-scanning/result-tip.component";
@ -100,7 +99,6 @@ import { P2pProviderComponent } from './p2p-provider/p2p-provider.component';
HistogramChartComponent,
ResultTipHistogramComponent,
ResultBarChartComponent,
ResultGridComponent,
ResultTipComponent,
ArtifactListPageComponent,
ArtifactListComponent,

View File

@ -92,7 +92,9 @@
<clr-dg-action-bar>
<button [clrLoading]="scanBtnState" type="button" class="btn btn-secondary scan-btn"
[disabled]="!(canScanNow() && selectedRowHasVul() && hasEnabledScanner && hasScanImagePermission )" (click)="scanNow()">
<clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}
<clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;
<span *ngIf="projectScanner">{{'VULNERABILITY.SCAN_BY' | translate: {scanner: getScannerInfo()} }}</span>
<span *ngIf="!projectScanner">{{'VULNERABILITY.NO_SCANNER' | translate}}</span>
</button>
<clr-dropdown class="btn btn-link" *ngIf="!depth">

View File

@ -382,6 +382,7 @@ clr-datagrid {
}
.scan-btn{
margin-top: -.3rem;
text-transform: none;
}
.eslip {

View File

@ -26,15 +26,15 @@ import { ClrLoadingState, ClrDatagridStateInterface, ClrDatagridComparatorInterf
import { ActivatedRoute, Router } from "@angular/router";
import {
Comparator, Label, LabelService, ScanningResultService,
Comparator, Label, LabelService, ScannerVo, ScanningResultService,
UserPermissionService, USERSTATICPERMISSION, VulnerabilitySummary
} from "../../../../../../lib/services";
import {
calculatePage,
clone,
CustomComparator,
DEFAULT_PAGE_SIZE, DEFAULT_SUPPORTED_MIME_TYPE,
formatSize, VULNERABILITY_SCAN_STATUS, dbEncodeURIComponent, doSorting
DEFAULT_PAGE_SIZE,
formatSize, VULNERABILITY_SCAN_STATUS, dbEncodeURIComponent, doSorting, DEFAULT_SUPPORTED_MIME_TYPES
} from "../../../../../../lib/utils/utils";
import {
ConfirmationAcknowledgement,
@ -151,6 +151,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
hasDeleteImagePermission: boolean;
hasScanImagePermission: boolean;
hasEnabledScanner: boolean;
projectScanner: ScannerVo;
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
onSendingScanCommand: boolean;
@ -341,7 +342,8 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
withImmutableStatus: true,
withLabel: true,
withScanOverview: true,
withTag: false
withTag: false,
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES
};
this.newArtifactService.getArtifact(artifactParam).subscribe(
res => {
@ -357,7 +359,8 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
withImmutableStatus: true,
withLabel: true,
withScanOverview: true,
withTag: false
withTag: false,
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES
};
platFormAttr.push({platform: child.platform});
observableLists.push(this.newArtifactService.getArtifact(childParams));
@ -386,7 +389,8 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
repositoryName: dbEncodeURIComponent(this.repoName),
withLabel: true,
withScanOverview: true,
withTag: false
withTag: false,
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES
};
Object.assign(listArtifactParams, params);
this.newArtifactService.listArtifactsResponse(listArtifactParams)
@ -914,6 +918,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
} else {
this.scanBtnState = ClrLoadingState.ERROR;
}
this.projectScanner = response;
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
@ -921,7 +926,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
handleScanOverview(scanOverview: any): any {
if (scanOverview) {
return scanOverview[DEFAULT_SUPPORTED_MIME_TYPE];
return Object.values(scanOverview)[0];
}
return null;
}
@ -1038,4 +1043,15 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
});
}
}
getScannerInfo(): string {
if (this.projectScanner) {
if (this.projectScanner.name && this.projectScanner.version) {
return `${this.projectScanner.name}@${this.projectScanner.version}`;
}
if (this.projectScanner.name && !this.projectScanner.version) {
return `${this.projectScanner.name}`;
}
}
return "";
}
}

View File

@ -17,7 +17,7 @@ describe('TagRetentionService', () => {
it('should be created and get right data', inject([AdditionsService], (service: AdditionsService) => {
expect(service).toBeTruthy();
service.getDetailByLink(testLink).subscribe(res => {
service.getDetailByLink(testLink, false, false).subscribe(res => {
expect(res).toEqual(data);
}
);

View File

@ -1,6 +1,7 @@
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { DEFAULT_SUPPORTED_MIME_TYPES } from "../../../../../lib/utils/utils";
@Injectable({
providedIn: 'root',
@ -9,10 +10,13 @@ export class AdditionsService {
constructor(private http: HttpClient) {
}
getDetailByLink(link: string, shouldReturnText?: boolean): Observable<any> {
getDetailByLink(link: string, shouldSetHeader: boolean, shouldReturnText: boolean): Observable<any> {
if (shouldReturnText) {
return this.http.get(link, { observe: 'body', responseType: 'text'} );
}
if (shouldSetHeader) {
return this.http.get(link, {headers: {"X-Accept-Vulnerabilities": DEFAULT_SUPPORTED_MIME_TYPES}});
}
return this.http.get(link);
}
}

View File

@ -11,7 +11,11 @@
<clr-dg-action-bar>
<div class="clr-row center">
<div class="ml-05">
<button (click)="scanNow()" type="button" class="btn btn-secondary" [clrLoading]="scanBtnState" [disabled]="!(hasEnabledScanner && hasScanningPermission && !onSendingScanCommand)"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button (click)="scanNow()" type="button" class="btn btn-secondary text-transform-none" [clrLoading]="scanBtnState" [disabled]="!(hasEnabledScanner && hasScanningPermission && !onSendingScanCommand)">
<clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;
<span *ngIf="projectScanner">{{'VULNERABILITY.SCAN_BY' | translate: {scanner: getScannerInfo(projectScanner)} }}</span>
<span *ngIf="!projectScanner">{{'VULNERABILITY.NO_SCANNER' | translate}}</span>
</button>
</div>
<div class="ml-1">
<div [hidden]="!shouldShowBar()">
@ -21,10 +25,12 @@
</hbr-vulnerability-bar>
</div>
</div>
</div>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="severitySort">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="cvssSort">{{'VULNERABILITY.GRID.CVSS3' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'fix_version'">{{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}}</clr-dg-column>
@ -59,6 +65,7 @@
<span *ngSwitchCase="'Unknown'" class="label label-unknown no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchDefault>{{severityText(res.severity) | translate}}</span>
</clr-dg-cell>
<clr-dg-cell>{{res.preferred_cvss?.score_v3}}</clr-dg-cell>
<clr-dg-cell>{{res.package}}</clr-dg-cell>
<clr-dg-cell>{{res.version}}</clr-dg-cell>
<clr-dg-cell>
@ -76,6 +83,9 @@
</clr-dg-row>
<clr-dg-footer>
<div class="report">
<i>{{'VULNERABILITY.REPORTED_BY' | translate: {scanner: getScannerInfo(scanner)} }}</i>
</div>
<clr-dg-pagination #pagination [clrDgPageSize]="25" [clrDgTotalItems]="scanningResults?.length">
<clr-dg-page-size [clrPageSizeOptions]="[15,25,50]">{{"PAGINATION.PAGE_SIZE" | translate}}</clr-dg-page-size>
<span *ngIf="scanningResults?.length">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'VULNERABILITY.GRID.FOOT_OF' | translate}}</span> {{scanningResults?.length}} {{'VULNERABILITY.GRID.FOOT_ITEMS' | translate}}

View File

@ -49,3 +49,9 @@
.ml-05 {
margin-left: 0.5rem;
}
.report {
text-align: left;
}
.text-transform-none {
text-transform: none;
}

View File

@ -16,7 +16,6 @@ import {
import { AdditionLink } from "../../../../../../../ng-swagger-gen/models/addition-link";
import { ErrorHandler } from "../../../../../../lib/utils/error-handler";
import { ChannelService } from "../../../../../../lib/services/channel.service";
import { DEFAULT_SUPPORTED_MIME_TYPE } from "../../../../../../lib/utils/utils";
import {SessionService} from "../../../../../shared/session.service";
import {SessionUser} from "../../../../../shared/session-user";
import {delay} from "rxjs/operators";
@ -46,8 +45,8 @@ describe('ArtifactVulnerabilitiesComponent', () => {
},
];
let scanOverview = {};
scanOverview[DEFAULT_SUPPORTED_MIME_TYPE] = {};
scanOverview[DEFAULT_SUPPORTED_MIME_TYPE].vulnerabilities = mockedVulnerabilities;
scanOverview["keyForTest"] = {};
scanOverview["keyForTest"].vulnerabilities = mockedVulnerabilities;
const mockedLink: AdditionLink = {
absolute: false,
href: '/test'
@ -156,7 +155,7 @@ describe('ArtifactVulnerabilitiesComponent', () => {
await fixture.whenStable();
const cols = fixture.nativeElement.querySelectorAll("clr-dg-column");
expect(cols).toBeTruthy();
expect(cols.length).toEqual(6);
expect(cols.length).toEqual(7);
const firstRow = fixture.nativeElement.querySelector("clr-dg-row");
const cells = firstRow.querySelectorAll("clr-dg-cell");
expect(cells[cells.length - 1].innerText).toEqual("TAG_RETENTION.YES");

View File

@ -13,7 +13,6 @@ import {
} from "../../../../../../lib/services";
import { ErrorHandler } from "../../../../../../lib/utils/error-handler";
import {
DEFAULT_SUPPORTED_MIME_TYPE,
SEVERITY_LEVEL_MAP,
VULNERABILITY_SEVERITY
} from "../../../../../../lib/utils/utils";
@ -42,12 +41,14 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
@Input() artifact: Artifact;
scan_overview: any;
scanner: ScannerVo;
projectScanner: ScannerVo;
scanningResults: VulnerabilityItem[] = [];
loading: boolean = false;
hasEnabledScanner: boolean = false;
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
severitySort: ClrDatagridComparatorInterface<VulnerabilityItem>;
cvssSort: ClrDatagridComparatorInterface<VulnerabilityItem>;
hasScanningPermission: boolean = false;
onSendingScanCommand: boolean = false;
hasShowLoading: boolean = false;
@ -72,6 +73,14 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
return that.getLevel(a) - that.getLevel(b);
}
};
this.cvssSort = {
compare(a: VulnerabilityItem, b: VulnerabilityItem): number {
if (a && a.preferred_cvss && a.preferred_cvss.score_v3 && b && b.preferred_cvss && b.preferred_cvss.score_v3) {
return (+a.preferred_cvss.score_v3) - (+b.preferred_cvss.score_v3);
}
return 0;
}
};
}
ngOnInit() {
@ -105,7 +114,7 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
this.loading = true;
this.hasShowLoading = true;
}
this.additionsService.getDetailByLink(this.vulnerabilitiesLink.href)
this.additionsService.getDetailByLink(this.vulnerabilitiesLink.href, true, false)
.pipe(finalize(() => {
this.loading = false;
this.hasShowLoading = false;
@ -113,13 +122,13 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
.subscribe(
res => {
this.scan_overview = res;
if (this.scan_overview && this.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]) {
this.scanningResults = this.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE].vulnerabilities || [];
if (this.scan_overview && Object.values(this.scan_overview)[0]) {
this.scanningResults = (Object.values(this.scan_overview)[0] as any).vulnerabilities || [];
// sort
if (this.scanningResults) {
this.scanningResults.sort(((a, b) => this.getLevel(b) - this.getLevel(a)));
}
this.scanner = this.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE].scanner;
this.scanner = (Object.values(this.scan_overview)[0] as any).scanner;
}
}, error => {
this.errorHandler.error(error);
@ -148,6 +157,7 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
} else {
this.scanBtnState = ClrLoadingState.ERROR;
}
this.projectScanner = response;
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
@ -200,7 +210,7 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
}
handleScanOverview(scanOverview: any): any {
if (scanOverview) {
return scanOverview[DEFAULT_SUPPORTED_MIME_TYPE];
return Object.values(scanOverview)[0];
}
return null;
}
@ -237,4 +247,15 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
}
return false;
}
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 "";
}
}

View File

@ -29,7 +29,7 @@ export class BuildHistoryComponent implements OnInit {
&& !this.buildHistoryLink.absolute
&& this.buildHistoryLink.href) {
this.loading = true;
this.additionsService.getDetailByLink(this.buildHistoryLink.href)
this.additionsService.getDetailByLink(this.buildHistoryLink.href, false, false)
.pipe(finalize(() => this.loading = false))
.subscribe(
res => {

View File

@ -32,7 +32,7 @@ export class DependenciesComponent implements OnInit {
&& !this.dependenciesLink.absolute
&& this.dependenciesLink.href) {
this.loading = true;
this.additionsService.getDetailByLink(this.dependenciesLink.href)
this.additionsService.getDetailByLink(this.dependenciesLink.href, false, false)
.pipe(finalize(() => this.loading = false))
.subscribe(
res => {

View File

@ -31,7 +31,7 @@ export class SummaryComponent implements OnInit {
&& !this.summaryLink.absolute
&& this.summaryLink.href) {
this.loading = true;
this.additionsService.getDetailByLink(this.summaryLink.href, true)
this.additionsService.getDetailByLink(this.summaryLink.href, false, true)
.pipe(finalize(() => this.loading = false))
.subscribe(
res => {

View File

@ -34,7 +34,7 @@ export class ValuesComponent implements OnInit {
ngOnInit(): void {
if (this.valuesLink && !this.valuesLink.absolute && this.valuesLink.href) {
this.loading = true;
this.additionsService.getDetailByLink(this.valuesLink.href, true)
this.additionsService.getDetailByLink(this.valuesLink.href, false, true)
.pipe(finalize(() => this.loading = false))
.subscribe(
res => {

View File

@ -13,7 +13,6 @@ import { ChannelService } from "../../../../lib/services/channel.service";
import {
clone,
CURRENT_BASE_HREF,
DEFAULT_SUPPORTED_MIME_TYPE,
VULNERABILITY_SCAN_STATUS,
dbEncodeURIComponent
} from "../../../../lib/utils/utils";
@ -159,7 +158,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
.subscribe((artifact: Artifact) => {
// To keep the same summary reference, use value copy.
if (artifact.scan_overview) {
this.copyValue(artifact.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]);
this.copyValue(Object.values(artifact.scan_overview)[0]);
}
if (!this.queued && !this.scanning) {
// Scanning should be done

View File

@ -1,65 +0,0 @@
<div class="row result-row">
<div>
<div class="row flex-items-xs-right rightPos">
<div class="flex-xs-middle option-right">
<hbr-filter [withDivider]="true" filterPlaceholder="{{'VULNERABILITY.PLACEHOLDER' | translate}}" (filterEvt)="filterVulnerabilities($event)"></hbr-filter>
<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">
<clr-dg-action-bar>
<button type="button" class="btn btn-secondary" [clrLoading]="scanBtnState" [disabled]="!hasScanImagePermission || !hasEnabledScanner" (click)="scanNow()"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="severitySort">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'fix_version'">{{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'VULNERABILITY.CHART.TOOLTIPS_TITLE_ZERO' | translate}}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let res of scanningResults">
<clr-dg-cell>
<span *ngIf="!res.links || res.links.length === 0">{{res.id}}</span>
<a *ngIf="res.links && res.links.length === 1" href="{{res.links[0]}}" target="_blank">{{res.id}}</a>
<span *ngIf="res.links && res.links.length > 1">
{{res.id}}
<clr-signpost>
<clr-signpost-content *clrIfOpen>
<div class="mt-5px" *ngFor="let link of res.links">
<a href="{{link}}" target="_blank">{{link}}</a>
</div>
</clr-signpost-content>
</clr-signpost>
</span>
</clr-dg-cell>
<clr-dg-cell [ngSwitch]="res.severity">
<span *ngSwitchCase="'Critical'" class="label label-critical no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'High'" class="label label-danger no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'Medium'" class="label label-medium no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'Low'" class="label label-low no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'Negligible'" class="label label-negligible no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'Unknown'" class="label label-unknown no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchDefault>{{severityText(res.severity) | translate}}</span>
</clr-dg-cell>
<clr-dg-cell>{{res.package}}</clr-dg-cell>
<clr-dg-cell>{{res.version}}</clr-dg-cell>
<clr-dg-cell>
<div *ngIf="res.fix_version; else elseBlock">
<clr-icon shape="wrench" class="is-success" size="20"></clr-icon>&nbsp;<span class="font-color-green">{{res.fix_version}}</span>
</div>
<ng-template #elseBlock>{{res.fix_version}}</ng-template>
</clr-dg-cell>
<clr-dg-row-detail *clrIfExpanded>
{{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}}
</clr-dg-row-detail>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="scanningResults?.length">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'VULNERABILITY.GRID.FOOT_OF' | translate}}</span> {{scanningResults?.length}} {{'VULNERABILITY.GRID.FOOT_ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="25" [clrDgTotalItems]="scanningResults?.length"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -1,121 +0,0 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { ResultGridComponent } from './result-grid.component';
import { of } from "rxjs";
import { IServiceConfig, SERVICE_CONFIG } from "../../../../lib/entities/service.config";
import {
ScanningResultDefaultService,
ScanningResultService,
UserPermissionDefaultService,
UserPermissionService, USERSTATICPERMISSION, VulnerabilityItem
} from "../../../../lib/services";
import { SharedModule } from "../../../../lib/utils/shared/shared.module";
import { FilterComponent } from "../../../../lib/components/filter/filter.component";
import { ChannelService } from "../../../../lib/services/channel.service";
import { ErrorHandler } from "../../../../lib/utils/error-handler";
import { CURRENT_BASE_HREF, DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SEVERITY } from "../../../../lib/utils/utils";
describe('ResultGridComponent (inline template)', () => {
let component: ResultGridComponent;
let fixture: ComponentFixture<ResultGridComponent>;
let serviceConfig: IServiceConfig;
let scanningService: ScanningResultService;
let userPermissionService: UserPermissionService;
let spy: jasmine.Spy;
let mockHasScanImagePermission: boolean = true;
let testConfig: IServiceConfig = {
vulnerabilityScanningBaseEndpoint: CURRENT_BASE_HREF + "/vulnerability/testing"
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
NoopAnimationsModule
],
declarations: [ResultGridComponent, FilterComponent],
providers: [
ErrorHandler,
ChannelService,
{ provide: SERVICE_CONFIG, useValue: testConfig },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService },
{ provide: UserPermissionService, useClass: UserPermissionDefaultService }
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(ResultGridComponent);
component = fixture.componentInstance;
component.tagId = "mockTag";
component.projectId = 1;
serviceConfig = TestBed.get(SERVICE_CONFIG);
scanningService = fixture.debugElement.injector.get(ScanningResultService);
let mockData: any = {};
mockData[DEFAULT_SUPPORTED_MIME_TYPE] = {};
mockData[DEFAULT_SUPPORTED_MIME_TYPE].vulnerabilities = [];
for (let i = 0; i < 30; i++) {
let res: VulnerabilityItem = {
id: "CVE-2016-" + (8859 + i),
severity: i % 2 === 0 ? VULNERABILITY_SEVERITY.HIGH : VULNERABILITY_SEVERITY.MEDIUM,
package: "package_" + i,
links: ["https://security-tracker.debian.org/tracker/CVE-2016-4484"],
layer: "layer_" + i,
version: '4.' + i + ".0",
fix_version: '4.' + i + '.11',
description: "Mock data"
};
mockData[DEFAULT_SUPPORTED_MIME_TYPE].vulnerabilities.push(res);
}
spy = spyOn(scanningService, 'getVulnerabilityScanningResults')
.and.returnValue(of(mockData));
userPermissionService = fixture.debugElement.injector.get(UserPermissionService);
spyOn(userPermissionService, "getPermission")
.withArgs(component.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY,
USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE )
.and.returnValue(of(mockHasScanImagePermission));
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
it('should inject the SERVICE_CONFIG', () => {
expect(serviceConfig).toBeTruthy();
expect(serviceConfig.vulnerabilityScanningBaseEndpoint).toEqual(CURRENT_BASE_HREF + "/vulnerability/testing");
});
it('should inject and call the ScanningResultService', () => {
expect(scanningService).toBeTruthy();
expect(spy.calls.any()).toBe(true, 'getScanningResults called');
});
it('should get data from ScanningResultService', waitForAsync(() => {
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs
fixture.detectChanges();
expect(component.scanningResults).toBeTruthy();
expect(component.scanningResults.length).toEqual(30);
});
}));
it('should render data to view', waitForAsync(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector('.datagrid-cell a');
expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual('CVE-2016-8859');
});
}));
});

View File

@ -1,162 +0,0 @@
import { Component, OnInit, Input, OnDestroy } from '@angular/core';
import { forkJoin, Subscription } from "rxjs";
import { finalize } from "rxjs/operators";
import { ClrDatagridComparatorInterface, ClrLoadingState } from "@clr/angular";
import {
ScanningResultService,
UserPermissionService,
USERSTATICPERMISSION,
VulnerabilityItem
} from "../../../../lib/services";
import { ChannelService } from "../../../../lib/services/channel.service";
import { ErrorHandler } from "../../../../lib/utils/error-handler";
import { DEFAULT_SUPPORTED_MIME_TYPE, SEVERITY_LEVEL_MAP, VULNERABILITY_SEVERITY } from "../../../../lib/utils/utils";
@Component({
selector: 'hbr-vulnerabilities-grid',
templateUrl: './result-grid.component.html',
styleUrls: ['./scanning.scss']
})
export class ResultGridComponent implements OnInit, OnDestroy {
scanningResults: VulnerabilityItem[] = [];
dataCache: VulnerabilityItem[] = [];
loading: boolean = false;
shouldShowLoading: boolean = true;
@Input() tagId: string;
@Input() repositoryId: string;
@Input() projectId: number;
hasScanImagePermission: boolean;
hasEnabledScanner: boolean = false;
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
severitySort: ClrDatagridComparatorInterface<VulnerabilityItem>;
sub: Subscription;
constructor(
private scanningService: ScanningResultService,
private channel: ChannelService,
private userPermissionService: UserPermissionService,
private errorHandler: ErrorHandler,
) {
const that = this;
this.severitySort = {
compare(a: VulnerabilityItem, b: VulnerabilityItem): number {
return that.getLevel(a) - that.getLevel(b);
}
};
}
ngOnInit(): void {
this.loadResults(this.repositoryId, this.tagId);
this.getScanPermissions(this.projectId);
if (!this.sub) {
this.channel.ArtifactDetail$.subscribe(tag => {
this.loadResults(this.repositoryId, this.tagId);
});
}
}
ngOnDestroy() {
if (this.sub) {
this.sub.unsubscribe();
this.sub = null;
}
}
getLevel(v: VulnerabilityItem): number {
if (v && v.severity && SEVERITY_LEVEL_MAP[v.severity]) {
return SEVERITY_LEVEL_MAP[v.severity];
}
return 0;
}
getProjectScanner(): void {
this.hasEnabledScanner = false;
this.scanBtnState = ClrLoadingState.LOADING;
this.scanningService.getProjectScanner(this.projectId)
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response) && !response.disabled
&& response.health === "healthy") {
this.scanBtnState = ClrLoadingState.SUCCESS;
this.hasEnabledScanner = true;
} else {
this.scanBtnState = ClrLoadingState.ERROR;
}
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
loadResults(repositoryId: string, tagId: string): void {
// only show loading for one time
if (this.shouldShowLoading) {
this.loading = true;
this.shouldShowLoading = false;
}
this.scanningService.getVulnerabilityScanningResults(repositoryId, tagId)
.pipe(finalize(() => this.loading = false))
.subscribe((results) => {
if (results && results[DEFAULT_SUPPORTED_MIME_TYPE]) {
let report = results[DEFAULT_SUPPORTED_MIME_TYPE];
if (report.vulnerabilities) {
this.dataCache = report.vulnerabilities;
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== '');
// sort
if (this.scanningResults) {
this.scanningResults.sort(((a, b) => this.getLevel(b) - this.getLevel(a)));
}
return;
}
}
});
}
// TODO: Should query from back-end service
filterVulnerabilities(terms: string): void {
if (terms.trim() === '') {
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== '');
} else {
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => this._regexpFilter(terms, item.package));
}
}
refresh(): void {
this.loadResults(this.repositoryId, this.tagId);
}
severityText(severity: string): string {
switch (severity) {
case VULNERABILITY_SEVERITY.CRITICAL:
return 'VULNERABILITY.SEVERITY.CRITICAL';
case VULNERABILITY_SEVERITY.HIGH:
return 'VULNERABILITY.SEVERITY.HIGH';
case VULNERABILITY_SEVERITY.MEDIUM:
return 'VULNERABILITY.SEVERITY.MEDIUM';
case VULNERABILITY_SEVERITY.LOW:
return 'VULNERABILITY.SEVERITY.LOW';
case VULNERABILITY_SEVERITY.NEGLIGIBLE:
return 'VULNERABILITY.SEVERITY.NEGLIGIBLE';
case VULNERABILITY_SEVERITY.UNKNOWN:
return 'VULNERABILITY.SEVERITY.UNKNOWN';
default:
return 'UNKNOWN';
}
}
_regexpFilter(terms: string, testedValue: any): boolean {
let reg = new RegExp('.*' + terms + '.*', 'i');
return reg.test(testedValue);
}
scanNow(): void {
this.channel.publishScanEvent(this.repositoryId + "/" + this.tagId);
}
getScanPermissions(projectId: number): void {
const hasScanImagePermission = this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE);
forkJoin(hasScanImagePermission).subscribe(permissions => {
this.hasScanImagePermission = permissions[0] as boolean;
if (this.projectId && this.hasScanImagePermission) {
this.getProjectScanner();
}
}, error => {
this.errorHandler.error(error);
});
}
}

View File

@ -62,7 +62,7 @@
</div>
<div *ngIf="scanner">
<span class="bar-scanning-time">{{'SCANNER.SCANNED_BY' | translate }}</span>
<span class="margin-left-5">{{scanner?.name}}</span>
<span class="margin-left-5">{{getScannerInfo()}}</span>
</div>
<div>
<span class="bar-scanning-time">{{'SCANNER.DURATION' | translate }}</span>

View File

@ -242,4 +242,15 @@ export class ResultTipHistogramComponent implements OnInit {
isThemeLight() {
return localStorage.getItem('styleModeLocal') === 'LIGHT';
}
getScannerInfo(): string {
if (this.scanner) {
if (this.scanner.name && this.scanner.version) {
return `${this.scanner.name}@${this.scanner.version}`;
}
if (this.scanner.name && !this.scanner.version) {
return `${this.scanner.name}`;
}
}
return "";
}
}

View File

@ -1022,7 +1022,8 @@
"COLUMN_DESCRIPTION": "Beschreibung",
"FOOT_ITEMS": "Elemente",
"FOOT_OF": "von",
"IN_ALLOW_LIST": "In CVE Allowliste enthalten"
"IN_ALLOW_LIST": "In CVE Allowliste enthalten",
"CVSS3": "CVSS3"
},
"CHART": {
"SCANNING_TIME": "Scan abgeschlossen um:",
@ -1048,7 +1049,10 @@
"PLACEHOLDER": "Filter Schwachstellen",
"PACKAGE": "Paket",
"PACKAGES": "Pakete",
"SCAN_NOW": "Scan"
"SCAN_NOW": "Scan",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
},
"PUSH_IMAGE": {
"TITLE": "Push Befehl",

View File

@ -1022,7 +1022,8 @@
"COLUMN_DESCRIPTION": "Description",
"FOOT_ITEMS": "Items",
"FOOT_OF": "of",
"IN_ALLOW_LIST": "Listed In CVE Allowlist"
"IN_ALLOW_LIST": "Listed In CVE Allowlist",
"CVSS3": "CVSS3"
},
"CHART": {
"SCANNING_TIME": "Scan completed time:",
@ -1048,7 +1049,10 @@
"PLACEHOLDER": "Filter Vulnerabilities",
"PACKAGE": "package",
"PACKAGES": "packages",
"SCAN_NOW": "Scan"
"SCAN_NOW": "Scan",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
},
"PUSH_IMAGE": {
"TITLE": "Push Command",

View File

@ -1022,7 +1022,8 @@
"COLUMN_DESCRIPTION": "Description",
"FOOT_ITEMS": "Items",
"FOOT_OF": "of",
"IN_ALLOW_LIST": "Listed In CVE Allowlist"
"IN_ALLOW_LIST": "Listed In CVE Allowlist",
"CVSS3": "CVSS3"
},
"CHART": {
"SCANNING_TIME": "Scan completed time:",
@ -1048,7 +1049,10 @@
"PLACEHOLDER": "Filter Vulnerabilities",
"PACKAGE": "package",
"PACKAGES": "packages",
"SCAN_NOW": "Scan"
"SCAN_NOW": "Scan",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
},
"PUSH_IMAGE": {
"TITLE": "Push Command",

View File

@ -995,7 +995,8 @@
"COLUMN_DESCRIPTION": "Description",
"FOOT_ITEMS": "Items",
"FOOT_OF": "de",
"IN_ALLOW_LIST": "Listed In CVE Allowlist"
"IN_ALLOW_LIST": "Listed In CVE Allowlist",
"CVSS3": "CVSS3"
},
"CHART": {
"SCANNING_TIME": "Temps d'analyse complète :",
@ -1021,7 +1022,10 @@
"PLACEHOLDER": "Filtrer les Vulnérabilitiés",
"PACKAGE": "paquet",
"PACKAGES": "paquets",
"SCAN_NOW": "Analyser"
"SCAN_NOW": "Analyser",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
},
"PUSH_IMAGE": {
"TITLE": "Push Command",

View File

@ -1018,7 +1018,8 @@
"COLUMN_DESCRIPTION": "Descrição",
"FOOT_ITEMS": "Itens",
"FOOT_OF": "de",
"IN_ALLOW_LIST": "Listed In CVE Allowlist"
"IN_ALLOW_LIST": "Listed In CVE Allowlist",
"CVSS3": "CVSS3"
},
"CHART": {
"SCANNING_TIME": "Tempo de conclusão da análise:",
@ -1044,7 +1045,10 @@
"PLACEHOLDER": "Filtrar Vulnerabilidades",
"PACKAGE": "pacote",
"PACKAGES": "pacotes",
"SCAN_NOW": "Analisar"
"SCAN_NOW": "Analisar",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
},
"PUSH_IMAGE": {
"TITLE": "Push Command",

View File

@ -1022,7 +1022,8 @@
"COLUMN_DESCRIPTION": "Açıklama",
"FOOT_ITEMS": "Öğeler",
"FOOT_OF": "of",
"IN_ALLOW_LIST": "Listed In CVE Allowlist"
"IN_ALLOW_LIST": "Listed In CVE Allowlist",
"CVSS3": "CVSS3"
},
"CHART": {
"SCANNING_TIME": "Tarama tamamlanma zamanı:",
@ -1048,7 +1049,10 @@
"PLACEHOLDER": "Güvenlik Açıklarını Filtrele",
"PACKAGE": "paket",
"PACKAGES": "paketler",
"SCAN_NOW": "Tara"
"SCAN_NOW": "Tara",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
},
"PUSH_IMAGE": {
"TITLE": "Push Command",

View File

@ -1022,7 +1022,8 @@
"COLUMN_DESCRIPTION": "简介",
"FOOT_ITEMS": "项目",
"FOOT_OF": "总共",
"IN_ALLOW_LIST": "已入特赦名单"
"IN_ALLOW_LIST": "已入特赦名单",
"CVSS3": "CVSS3"
},
"CHART": {
"SCANNING_TIME": "扫描完成时间:",
@ -1048,7 +1049,10 @@
"PLACEHOLDER": "过滤漏洞",
"PACKAGE": "组件",
"PACKAGES": "组件",
"SCAN_NOW": "扫描"
"SCAN_NOW": "扫描",
"SCAN_BY": "使用 {{scanner}} 进行扫描",
"REPORTED_BY": "结果由 {{scanner}} 提供",
"NO_SCANNER": "无扫描器"
},
"PUSH_IMAGE": {
"TITLE": "推送命令",

View File

@ -1017,7 +1017,8 @@
"COLUMN_DESCRIPTION": "簡介",
"FOOT_ITEMS": "項目",
"FOOT_OF": "總共",
"IN_ALLOW_LIST": "Listed In CVE Allowlist"
"IN_ALLOW_LIST": "Listed In CVE Allowlist",
"CVSS3": "CVSS3"
},
"CHART":{
"SCANNING_TIME": "掃描完成時間:",
@ -1043,7 +1044,10 @@
"PLACEHOLDER": "過濾漏洞",
"PACKAGE": "組件",
"PACKAGES": "組件",
"SCAN_NOW":"掃描"
"SCAN_NOW":"掃描",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
},
"PUSH_IMAGE":{
"TITLE": "推送鏡像的Docker命令",

View File

@ -246,7 +246,8 @@ export interface VulnerabilityItem extends VulnerabilityBase {
links: string[];
fix_version: string;
layer?: string;
description: string;
description?: string;
preferred_cvss?: {[key: string]: string | number};
}
export interface VulnerabilitySummary {

View File

@ -3,13 +3,11 @@ import { Injectable, Inject } from "@angular/core";
import { SERVICE_CONFIG, IServiceConfig } from "../entities/service.config";
import {
buildHttpRequestOptions,
CURRENT_BASE_HREF,
DEFAULT_SUPPORTED_MIME_TYPE,
HTTP_JSON_OPTIONS
} from "../utils/utils";
import { RequestQueryParams } from "./RequestQueryParams";
import { VulnerabilityDetail, VulnerabilitySummary } from "./interface";
import { VulnerabilitySummary } from "./interface";
import { map, catchError } from "rxjs/operators";
import { Observable, of, throwError as observableThrowError } from "rxjs";
@ -37,23 +35,6 @@ export abstract class ScanningResultService {
queryParams?: RequestQueryParams
):
| Observable<VulnerabilitySummary>;
/**
* Get the detailed vulnerabilities scanning results.
*
* @abstract
* ** deprecated param {string} tagId
* returns {(Observable<VulnerabilityItem[]>)}
*
* @memberOf ScanningResultService
*/
abstract getVulnerabilityScanningResults(
repoName: string,
tagId: string,
queryParams?: RequestQueryParams
):
| Observable<any>;
/**
* Start a new vulnerability scanning
*
@ -120,31 +101,6 @@ export class ScanningResultDefaultService extends ScanningResultService {
return of({} as VulnerabilitySummary);
}
getVulnerabilityScanningResults(
repoName: string,
tagId: string,
queryParams?: RequestQueryParams
):
| Observable<any> {
if (!repoName || repoName.trim() === "" || !tagId || tagId.trim() === "") {
return observableThrowError("Bad argument");
}
let httpOptions = buildHttpRequestOptions(queryParams);
let requestHeaders = httpOptions.headers as HttpHeaders;
// Change the accept header to the supported report mime types
httpOptions.headers = requestHeaders.set("Accept", DEFAULT_SUPPORTED_MIME_TYPE);
return this.http
.get(
`${this._baseUrl}/${repoName}/tags/${tagId}/scan`,
httpOptions
)
.pipe(map(response => response as VulnerabilityDetail)
, catchError(error => observableThrowError(error)));
}
startVulnerabilityScanning(
projectName: string,
repoName: string,

View File

@ -245,7 +245,8 @@ export const DEFAULT_PAGE_SIZE: number = 15;
/**
* The default supported mime type
*/
export const DEFAULT_SUPPORTED_MIME_TYPE = "application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0";
export const DEFAULT_SUPPORTED_MIME_TYPES =
"application/vnd.security.vulnerability.report; version=1.1, application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0";
/**
* the property name of vulnerability database updated time