mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-30 06:03:45 +01:00
Implement tag detail component & refactor vul summary bar chart
This commit is contained in:
parent
4d2a2363a7
commit
2072fc237e
@ -45,6 +45,7 @@ export interface Tag extends Base {
|
|||||||
author: string;
|
author: string;
|
||||||
created: Date;
|
created: Date;
|
||||||
signature?: string;
|
signature?: string;
|
||||||
|
vulnerability?: VulnerabilitySummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,28 +158,28 @@ export interface SystemInfo {
|
|||||||
|
|
||||||
//Not finalized yet
|
//Not finalized yet
|
||||||
export enum VulnerabilitySeverity {
|
export enum VulnerabilitySeverity {
|
||||||
LOW, MEDIUM, HIGH, UNKNOWN, NONE
|
NONE, UNKNOWN, LOW, MEDIUM, HIGH
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScanningBaseResult {
|
export interface VulnerabilityBase {
|
||||||
id: string;
|
id: string;
|
||||||
severity: VulnerabilitySeverity;
|
severity: VulnerabilitySeverity;
|
||||||
package: string;
|
package: string;
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScanningDetailResult extends ScanningBaseResult {
|
export interface VulnerabilityItem extends VulnerabilityBase {
|
||||||
fixedVersion: string;
|
fixedVersion: string;
|
||||||
layer: string;
|
layer: string;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScanningResultSummary {
|
export interface VulnerabilitySummary {
|
||||||
totalComponents: number;
|
total_package: number;
|
||||||
noneComponents: number;
|
package_with_none: number;
|
||||||
completeTimestamp: Date;
|
package_with_high?: number;
|
||||||
high: ScanningBaseResult[];
|
package_with_medium?: number;
|
||||||
medium: ScanningBaseResult[];
|
package_With_low?: number;
|
||||||
low: ScanningBaseResult[];
|
package_with_unknown?: number;
|
||||||
unknown: ScanningBaseResult[];
|
complete_timestamp: Date;
|
||||||
}
|
}
|
@ -5,8 +5,10 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
|||||||
import { Http, URLSearchParams } from '@angular/http';
|
import { Http, URLSearchParams } from '@angular/http';
|
||||||
import { HTTP_JSON_OPTIONS } from '../utils';
|
import { HTTP_JSON_OPTIONS } from '../utils';
|
||||||
|
|
||||||
import { ScanningDetailResult } from './interface';
|
import {
|
||||||
import { VulnerabilitySeverity, ScanningBaseResult, ScanningResultSummary } from './interface';
|
VulnerabilityItem,
|
||||||
|
VulnerabilitySummary
|
||||||
|
} from './interface';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the vulnerabilities scanning results for the specified tag.
|
* Get the vulnerabilities scanning results for the specified tag.
|
||||||
@ -21,22 +23,22 @@ export abstract class ScanningResultService {
|
|||||||
*
|
*
|
||||||
* @abstract
|
* @abstract
|
||||||
* @param {string} tagId
|
* @param {string} tagId
|
||||||
* @returns {(Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary)}
|
* @returns {(Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary)}
|
||||||
*
|
*
|
||||||
* @memberOf ScanningResultService
|
* @memberOf ScanningResultService
|
||||||
*/
|
*/
|
||||||
abstract getScanningResultSummary(tagId: string): Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary;
|
abstract getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the detailed vulnerabilities scanning results.
|
* Get the detailed vulnerabilities scanning results.
|
||||||
*
|
*
|
||||||
* @abstract
|
* @abstract
|
||||||
* @param {string} tagId
|
* @param {string} tagId
|
||||||
* @returns {(Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[])}
|
* @returns {(Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[])}
|
||||||
*
|
*
|
||||||
* @memberOf ScanningResultService
|
* @memberOf ScanningResultService
|
||||||
*/
|
*/
|
||||||
abstract getScanningResults(tagId: string): Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[];
|
abstract getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -47,7 +49,7 @@ export class ScanningResultDefaultService extends ScanningResultService {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
getScanningResultSummary(tagId: string): Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary {
|
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary {
|
||||||
if (!tagId || tagId.trim() === '') {
|
if (!tagId || tagId.trim() === '') {
|
||||||
return Promise.reject('Bad argument');
|
return Promise.reject('Bad argument');
|
||||||
}
|
}
|
||||||
@ -55,7 +57,7 @@ export class ScanningResultDefaultService extends ScanningResultService {
|
|||||||
return Observable.of({});
|
return Observable.of({});
|
||||||
}
|
}
|
||||||
|
|
||||||
getScanningResults(tagId: string): Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[] {
|
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[] {
|
||||||
if (!tagId || tagId.trim() === '') {
|
if (!tagId || tagId.trim() === '') {
|
||||||
return Promise.reject('Bad argument');
|
return Promise.reject('Bad argument');
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,19 @@ export abstract class TagService {
|
|||||||
*
|
*
|
||||||
* @memberOf TagService
|
* @memberOf TagService
|
||||||
*/
|
*/
|
||||||
abstract deleteTag(repositoryName: string, tag: string): Observable<any> | Promise<Tag> | any;
|
abstract deleteTag(repositoryName: string, tag: string): Observable<any> | Promise<any> | any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the specified tag.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {string} repositoryName
|
||||||
|
* @param {string} tag
|
||||||
|
* @returns {(Observable<Tag> | Promise<Tag> | Tag)}
|
||||||
|
*
|
||||||
|
* @memberOf TagService
|
||||||
|
*/
|
||||||
|
abstract getTag(repositoryName: string, tag: string, queryParams?: RequestQueryParams): Observable<Tag> | Promise<Tag> | Tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,4 +125,15 @@ export class TagDefaultService extends TagService {
|
|||||||
.then(response => response)
|
.then(response => response)
|
||||||
.catch(error => Promise.reject(error));
|
.catch(error => Promise.reject(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getTag(repositoryName: string, tag: string, queryParams?: RequestQueryParams): Observable<Tag> | Promise<Tag> | Tag {
|
||||||
|
if (!repositoryName || !tag) {
|
||||||
|
return Promise.reject("Bad argument");
|
||||||
|
}
|
||||||
|
|
||||||
|
let url: string = `${this._baseUrl}/${repositoryName}/tags/${tag}`;
|
||||||
|
return this.http.get(url, HTTP_JSON_OPTIONS).toPromise()
|
||||||
|
.then(response => response.json() as Tag)
|
||||||
|
.catch(error => Promise.reject(error));
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,7 +1,11 @@
|
|||||||
import { Type } from '@angular/core';
|
import { Type } from '@angular/core';
|
||||||
import { TagComponent } from './tag.component';
|
import { TagComponent } from './tag.component';
|
||||||
|
import { TagDetailComponent } from './tag-detail.component';
|
||||||
|
|
||||||
|
export * from './tag.component';
|
||||||
|
export * from './tag-detail.component';
|
||||||
|
|
||||||
export const TAG_DIRECTIVES: Type<any>[] = [
|
export const TAG_DIRECTIVES: Type<any>[] = [
|
||||||
TagComponent
|
TagComponent,
|
||||||
|
TagDetailComponent
|
||||||
];
|
];
|
109
src/ui_ng/lib/src/tag/tag-detail.component.css.ts
Normal file
109
src/ui_ng/lib/src/tag/tag-detail.component.css.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
export const TAG_DETAIL_STYLES: string = `
|
||||||
|
.overview-section {
|
||||||
|
background-color: white;
|
||||||
|
padding-bottom: 36px;
|
||||||
|
border-bottom: 1px solid #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
background-color: #fafafa;
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrapper {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-name {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-timestamp {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-90 {
|
||||||
|
-webkit-transform: rotate(-90deg);
|
||||||
|
/*Firefox*/
|
||||||
|
-moz-transform: rotate(-90deg);
|
||||||
|
/*Chrome*/
|
||||||
|
-ms-transform: rotate(-90deg);
|
||||||
|
/*IE9 、IE10*/
|
||||||
|
-o-transform: rotate(-90deg);
|
||||||
|
/*Opera*/
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-back {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-block {
|
||||||
|
border-right: 2px solid #cccccc;
|
||||||
|
margin-right: 6px;
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 6px 6px 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vulnerability-block {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-block {
|
||||||
|
margin-top: 24px;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: row wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-summary {
|
||||||
|
margin-right: 36px;
|
||||||
|
margin-left: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-block {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: row wrap;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vulnerabilities-info {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vulnerabilities-info .third-column {
|
||||||
|
margin-left: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vulnerabilities-info .second-column,
|
||||||
|
.vulnerabilities-info .fourth-column {
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vulnerabilities-info .second-row {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-title {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-detail-label {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-detail-value {
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
`;
|
77
src/ui_ng/lib/src/tag/tag-detail.component.html.ts
Normal file
77
src/ui_ng/lib/src/tag/tag-detail.component.html.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
export const TAG_DETAIL_HTML: string = `
|
||||||
|
<div>
|
||||||
|
<section class="overview-section">
|
||||||
|
<div class="title-wrapper">
|
||||||
|
<div class="title-block arrow-block">
|
||||||
|
<clr-icon class="rotate-90 arrow-back" shape="arrow" size="36" (click)="onBack()"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="title-block">
|
||||||
|
<div class="tag-name">
|
||||||
|
{{tagDetails.name}}:v{{tagDetails.docker_version}}
|
||||||
|
</div>
|
||||||
|
<div class="tag-timestamp">
|
||||||
|
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{tagDetails.author}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-block">
|
||||||
|
<div class="image-summary">
|
||||||
|
<div class="detail-title">
|
||||||
|
{{'TAG.IMAGE_DETAILS' | translate }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-block">
|
||||||
|
<div class="image-detail-label">
|
||||||
|
<div>{{'TAG.ARCHITECTURE' | translate }}</div>
|
||||||
|
<div>{{'TAG.OS' | translate }}</div>
|
||||||
|
<div>{{'TAG.SCAN_COMPLETION_TIME' | translate }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-detail-value">
|
||||||
|
<div>{{tagDetails.architecture}}</div>
|
||||||
|
<div>{{tagDetails.os}}</div>
|
||||||
|
<div>{{scanCompletedDatetime | date}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="detail-title">
|
||||||
|
{{'TAG.IMAGE_VULNERABILITIES' | translate }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-block vulnerabilities-info">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<clr-icon shape="error" size="24" class="is-error"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="second-row">
|
||||||
|
<clr-icon shape="exclamation-triangle" size="24" class="is-warning"></clr-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="second-column">
|
||||||
|
<div>{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{suffixForHigh | translate }}</div>
|
||||||
|
<div class="second-row">{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{suffixForMedium | translate }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="third-column">
|
||||||
|
<div>
|
||||||
|
<clr-icon shape="play" size="20" class="is-warning rotate-90"></clr-icon>
|
||||||
|
</div>
|
||||||
|
<div class="second-row">
|
||||||
|
<clr-icon shape="help" size="20"></clr-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fourth-column">
|
||||||
|
<div>{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{suffixForLow | translate }}</div>
|
||||||
|
<div class="second-row">{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{suffixForUnknown | translate }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="detail-section">
|
||||||
|
<div class="vulnerability-block">
|
||||||
|
<hbr-vulnerabilities-grid tagId="tagId"></hbr-vulnerabilities-grid>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
`;
|
118
src/ui_ng/lib/src/tag/tag-detail.component.spec.ts
Normal file
118
src/ui_ng/lib/src/tag/tag-detail.component.spec.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { ResultGridComponent } from '../vulnerability-scanning/result-grid.component';
|
||||||
|
import { TagDetailComponent } from './tag-detail.component';
|
||||||
|
|
||||||
|
import { ErrorHandler } from '../error-handler/error-handler';
|
||||||
|
import { Tag, VulnerabilitySummary } from '../service/interface';
|
||||||
|
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||||
|
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
|
||||||
|
|
||||||
|
describe('TagDetailComponent (inline template)', () => {
|
||||||
|
|
||||||
|
let comp: TagDetailComponent;
|
||||||
|
let fixture: ComponentFixture<TagDetailComponent>;
|
||||||
|
let tagService: TagService;
|
||||||
|
let spy: jasmine.Spy;
|
||||||
|
let mockVulnerability: VulnerabilitySummary = {
|
||||||
|
total_package: 124,
|
||||||
|
package_with_none: 92,
|
||||||
|
package_with_high: 10,
|
||||||
|
package_with_medium: 6,
|
||||||
|
package_With_low: 13,
|
||||||
|
package_with_unknown: 3,
|
||||||
|
complete_timestamp: new Date()
|
||||||
|
};
|
||||||
|
let mockTag: Tag = {
|
||||||
|
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
|
||||||
|
"name": "nginx",
|
||||||
|
"architecture": "amd64",
|
||||||
|
"os": "linux",
|
||||||
|
"docker_version": "1.12.3",
|
||||||
|
"author": "steven",
|
||||||
|
"created": new Date("2016-11-08T22:41:15.912313785Z"),
|
||||||
|
"signature": null,
|
||||||
|
vulnerability: mockVulnerability
|
||||||
|
};
|
||||||
|
|
||||||
|
let config: IServiceConfig = {
|
||||||
|
repositoryBaseEndpoint: '/api/repositories/testing'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
SharedModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
TagDetailComponent,
|
||||||
|
ResultGridComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ErrorHandler,
|
||||||
|
{ provide: SERVICE_CONFIG, useValue: config },
|
||||||
|
{ provide: TagService, useClass: TagDefaultService },
|
||||||
|
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(TagDetailComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
|
comp.tagId = "mock_tag";
|
||||||
|
comp.repositoryId = "mock_repo";
|
||||||
|
|
||||||
|
tagService = fixture.debugElement.injector.get(TagService);
|
||||||
|
spy = spyOn(tagService, 'getTag').and.returnValues(Promise.resolve(mockTag));
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load data', async(() => {
|
||||||
|
expect(spy.calls.any).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should rightly display tag name and version', async(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name');
|
||||||
|
expect(el).toBeTruthy();
|
||||||
|
expect(el.textContent.trim()).toEqual('nginx:v1.12.3');
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display tag details', async(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let el: HTMLElement = fixture.nativeElement.querySelector('.image-detail-value');
|
||||||
|
expect(el).toBeTruthy();
|
||||||
|
let el2: HTMLElement = el.querySelector('div');
|
||||||
|
expect(el2).toBeTruthy();
|
||||||
|
expect(el2.textContent).toEqual("amd64");
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display vulnerability details', async(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let el: HTMLElement = fixture.nativeElement.querySelector('.second-column');
|
||||||
|
expect(el).toBeTruthy();
|
||||||
|
let el2: HTMLElement = el.querySelector('div');
|
||||||
|
expect(el2).toBeTruthy();
|
||||||
|
expect(el2.textContent.trim()).toEqual("10 VULNERABILITY.SEVERITY.HIGH VULNERABILITY.PLURAL");
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
88
src/ui_ng/lib/src/tag/tag-detail.component.ts
Normal file
88
src/ui_ng/lib/src/tag/tag-detail.component.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { TAG_DETAIL_STYLES } from './tag-detail.component.css';
|
||||||
|
import { TAG_DETAIL_HTML } from './tag-detail.component.html';
|
||||||
|
|
||||||
|
import { TagService, Tag } from '../service/index';
|
||||||
|
import { toPromise } from '../utils';
|
||||||
|
import { ErrorHandler } from '../error-handler/index';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'hbr-tag-detail',
|
||||||
|
styles: [TAG_DETAIL_STYLES],
|
||||||
|
template: TAG_DETAIL_HTML,
|
||||||
|
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
export class TagDetailComponent implements OnInit {
|
||||||
|
@Input() tagId: string;
|
||||||
|
@Input() repositoryId: string;
|
||||||
|
tagDetails: Tag = {
|
||||||
|
name: "--",
|
||||||
|
author: "--",
|
||||||
|
created: new Date(),
|
||||||
|
architecture: "--",
|
||||||
|
os: "--",
|
||||||
|
docker_version: "--",
|
||||||
|
digest: "--"
|
||||||
|
};
|
||||||
|
|
||||||
|
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private tagService: TagService,
|
||||||
|
private errorHandler: ErrorHandler) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.repositoryId && this.tagId) {
|
||||||
|
toPromise<Tag>(this.tagService.getTag(this.repositoryId, this.tagId))
|
||||||
|
.then(response => this.tagDetails = response)
|
||||||
|
.catch(error => this.errorHandler.error(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBack(): void {
|
||||||
|
this.backEvt.emit(this.tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get highCount(): number {
|
||||||
|
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||||
|
this.tagDetails.vulnerability.package_with_high : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get mediumCount(): number {
|
||||||
|
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||||
|
this.tagDetails.vulnerability.package_with_medium : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get lowCount(): number {
|
||||||
|
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||||
|
this.tagDetails.vulnerability.package_With_low : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get unknownCount(): number {
|
||||||
|
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||||
|
this.tagDetails.vulnerability.package_with_unknown : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get scanCompletedDatetime(): Date {
|
||||||
|
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||||
|
this.tagDetails.vulnerability.complete_timestamp : new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get suffixForHigh(): string {
|
||||||
|
return this.highCount > 1 ? "VULNERABILITY.PLURAL" : "VULNERABILITY.SINGULAR";
|
||||||
|
}
|
||||||
|
|
||||||
|
public get suffixForMedium(): string {
|
||||||
|
return this.mediumCount > 1 ? "VULNERABILITY.PLURAL" : "VULNERABILITY.SINGULAR";
|
||||||
|
}
|
||||||
|
|
||||||
|
public get suffixForLow(): string {
|
||||||
|
return this.lowCount > 1 ? "VULNERABILITY.PLURAL" : "VULNERABILITY.SINGULAR";
|
||||||
|
}
|
||||||
|
|
||||||
|
public get suffixForUnknown(): string {
|
||||||
|
return this.unknownCount > 1 ? "VULNERABILITY.PLURAL" : "VULNERABILITY.SINGULAR";
|
||||||
|
}
|
||||||
|
}
|
@ -86,7 +86,7 @@ describe('TagComponent (inline template)', ()=> {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should load data', async(()=>{
|
it('should load data', async(()=>{
|
||||||
expect(spy.calls.any).toBeTruthy();
|
expect(spy.calls.any).toBeTruthy();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { HttpModule } from '@angular/http';
|
import { HttpModule } from '@angular/http';
|
||||||
import { DebugElement } from '@angular/core';
|
import { DebugElement } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { ScanningResultSummary, VulnerabilitySeverity, ScanningBaseResult } from '../service/index';
|
import { VulnerabilitySummary } from '../service/index';
|
||||||
|
|
||||||
import { ResultBarChartComponent, ScanState } from './result-bar-chart.component';
|
import { ResultBarChartComponent, ScanState } from './result-bar-chart.component';
|
||||||
import { ResultTipComponent } from './result-tip.component';
|
import { ResultTipComponent } from './result-tip.component';
|
||||||
@ -16,11 +16,18 @@ describe('ResultBarChartComponent (inline template)', () => {
|
|||||||
let component: ResultBarChartComponent;
|
let component: ResultBarChartComponent;
|
||||||
let fixture: ComponentFixture<ResultBarChartComponent>;
|
let fixture: ComponentFixture<ResultBarChartComponent>;
|
||||||
let serviceConfig: IServiceConfig;
|
let serviceConfig: IServiceConfig;
|
||||||
let scanningService: ScanningResultService;
|
|
||||||
let spy: jasmine.Spy;
|
|
||||||
let testConfig: IServiceConfig = {
|
let testConfig: IServiceConfig = {
|
||||||
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
||||||
};
|
};
|
||||||
|
let mockData: VulnerabilitySummary = {
|
||||||
|
total_package: 124,
|
||||||
|
package_with_none: 92,
|
||||||
|
package_with_high: 10,
|
||||||
|
package_with_medium: 6,
|
||||||
|
package_With_low: 13,
|
||||||
|
package_with_unknown: 3,
|
||||||
|
complete_timestamp: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -32,8 +39,7 @@ describe('ResultBarChartComponent (inline template)', () => {
|
|||||||
ResultTipComponent],
|
ResultTipComponent],
|
||||||
providers: [
|
providers: [
|
||||||
ErrorHandler,
|
ErrorHandler,
|
||||||
{ provide: SERVICE_CONFIG, useValue: testConfig },
|
{ provide: SERVICE_CONFIG, useValue: testConfig }
|
||||||
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -43,52 +49,9 @@ describe('ResultBarChartComponent (inline template)', () => {
|
|||||||
fixture = TestBed.createComponent(ResultBarChartComponent);
|
fixture = TestBed.createComponent(ResultBarChartComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
component.tagId = "mockTag";
|
component.tagId = "mockTag";
|
||||||
component.state = ScanState.COMPLETED;
|
component.state = ScanState.UNKNOWN;
|
||||||
|
|
||||||
serviceConfig = TestBed.get(SERVICE_CONFIG);
|
serviceConfig = TestBed.get(SERVICE_CONFIG);
|
||||||
scanningService = fixture.debugElement.injector.get(ScanningResultService);
|
|
||||||
let mockData: ScanningResultSummary = {
|
|
||||||
totalComponents: 21,
|
|
||||||
noneComponents: 7,
|
|
||||||
completeTimestamp: new Date(),
|
|
||||||
high: [],
|
|
||||||
medium: [],
|
|
||||||
low: [],
|
|
||||||
unknown: []
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < 14; i++) {
|
|
||||||
let res: ScanningBaseResult = {
|
|
||||||
id: "CVE-2016-" + (8859 + i),
|
|
||||||
package: "package_" + i,
|
|
||||||
version: '4.' + i + ".0",
|
|
||||||
severity: VulnerabilitySeverity.UNKNOWN
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (i % 4) {
|
|
||||||
case 0:
|
|
||||||
res.severity = VulnerabilitySeverity.HIGH;
|
|
||||||
mockData.high.push(res);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
res.severity = VulnerabilitySeverity.MEDIUM;
|
|
||||||
mockData.medium.push(res);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
res.severity = VulnerabilitySeverity.LOW;
|
|
||||||
mockData.low.push(res);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
res.severity = VulnerabilitySeverity.UNKNOWN;
|
|
||||||
mockData.unknown.push(res);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
spy = spyOn(scanningService, 'getScanningResultSummary')
|
|
||||||
.and.returnValue(Promise.resolve(mockData));
|
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@ -102,22 +65,57 @@ describe('ResultBarChartComponent (inline template)', () => {
|
|||||||
expect(serviceConfig.vulnerabilityScanningBaseEndpoint).toEqual("/api/vulnerability/testing");
|
expect(serviceConfig.vulnerabilityScanningBaseEndpoint).toEqual("/api/vulnerability/testing");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should inject and call the ScanningResultService', () => {
|
it('should show a button if status is PENDING', async(() => {
|
||||||
expect(scanningService).toBeTruthy();
|
component.state = ScanState.PENDING;
|
||||||
expect(spy.calls.any()).toBe(true, 'getScanningResultSummary called');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get data from ScanningResultService', async(() => {
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
fixture.whenStable().then(() => { // wait for async getRecentLogs
|
fixture.whenStable().then(() => { // wait for async getRecentLogs
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(component.summary).toBeTruthy();
|
|
||||||
expect(component.summary.totalComponents).toEqual(21);
|
let el: HTMLElement = fixture.nativeElement.querySelector('.scanning-button');
|
||||||
expect(component.summary.high.length).toEqual(4);
|
expect(el).toBeTruthy();
|
||||||
expect(component.summary.medium.length).toEqual(4);
|
});
|
||||||
expect(component.summary.low.length).toEqual(3);
|
}));
|
||||||
expect(component.summary.noneComponents).toEqual(7);
|
|
||||||
|
it('should show progress if status is SCANNING', async(() => {
|
||||||
|
component.state = ScanState.SCANNING;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
fixture.whenStable().then(() => { // wait for async getRecentLogs
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let el: HTMLElement = fixture.nativeElement.querySelector('.progress');
|
||||||
|
expect(el).toBeTruthy();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should show QUEUED if status is QUEUED', async(() => {
|
||||||
|
component.state = ScanState.QUEUED;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
fixture.whenStable().then(() => { // wait for async getRecentLogs
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-state');
|
||||||
|
expect(el).toBeTruthy();
|
||||||
|
let el2: HTMLElement = el.querySelector('span');
|
||||||
|
expect(el2).toBeTruthy();
|
||||||
|
expect(el2.textContent).toEqual('VULNERABILITY.STATE.QUEUED');
|
||||||
|
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should show summary bar chart if status is COMPLETED', async(() => {
|
||||||
|
component.state = ScanState.COMPLETED;
|
||||||
|
component.summary = mockData;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
fixture.whenStable().then(() => { // wait for async getRecentLogs
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');
|
||||||
|
expect(el).not.toBeNull();
|
||||||
|
expect(el.style.width).toEqual("74px");
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -2,16 +2,9 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
EventEmitter,
|
EventEmitter
|
||||||
OnInit
|
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import { VulnerabilitySummary } from '../service/index';
|
||||||
ScanningResultService,
|
|
||||||
ScanningResultSummary
|
|
||||||
} from '../service/index';
|
|
||||||
import { ErrorHandler } from '../error-handler/index';
|
|
||||||
import { toPromise } from '../utils';
|
|
||||||
import { MAX_TIP_WIDTH } from './result-tip.component';
|
|
||||||
import { SCANNING_STYLES } from './scanning.css';
|
import { SCANNING_STYLES } from './scanning.css';
|
||||||
import { BAR_CHART_COMPONENT_HTML } from './scanning.html';
|
import { BAR_CHART_COMPONENT_HTML } from './scanning.html';
|
||||||
|
|
||||||
@ -25,37 +18,21 @@ export enum ScanState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hbr-scan-result-bar',
|
selector: 'hbr-vulnerability-bar',
|
||||||
styles: [SCANNING_STYLES],
|
styles: [SCANNING_STYLES],
|
||||||
template: BAR_CHART_COMPONENT_HTML
|
template: BAR_CHART_COMPONENT_HTML
|
||||||
})
|
})
|
||||||
export class ResultBarChartComponent implements OnInit {
|
export class ResultBarChartComponent {
|
||||||
@Input() tagId: string = "";
|
@Input() tagId: string = "";
|
||||||
@Input() state: ScanState = ScanState.UNKNOWN;
|
@Input() state: ScanState = ScanState.UNKNOWN;
|
||||||
@Input() summary: ScanningResultSummary = {
|
@Input() summary: VulnerabilitySummary = {
|
||||||
totalComponents: 0,
|
total_package: 0,
|
||||||
noneComponents: 0,
|
package_with_none: 0,
|
||||||
completeTimestamp: new Date(),
|
complete_timestamp: new Date()
|
||||||
high: [],
|
|
||||||
medium: [],
|
|
||||||
low: [],
|
|
||||||
unknown: []
|
|
||||||
};
|
};
|
||||||
@Output() startScanning: EventEmitter<string> = new EventEmitter<string>();
|
@Output() startScanning: EventEmitter<string> = new EventEmitter<string>();
|
||||||
|
|
||||||
constructor(
|
constructor() { }
|
||||||
private scanningService: ScanningResultService,
|
|
||||||
private errorHandler: ErrorHandler) { }
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
toPromise<ScanningResultSummary>(this.scanningService.getScanningResultSummary(this.tagId))
|
|
||||||
.then((summary: ScanningResultSummary) => {
|
|
||||||
this.summary = summary;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.errorHandler.error(error);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public get completed(): boolean {
|
public get completed(): boolean {
|
||||||
return this.state === ScanState.COMPLETED;
|
return this.state === ScanState.COMPLETED;
|
||||||
@ -86,66 +63,4 @@ export class ResultBarChartComponent implements OnInit {
|
|||||||
this.startScanning.emit(this.tagId);
|
this.startScanning.emit(this.tagId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get hasHigh(): boolean {
|
|
||||||
return this.summary && this.summary.high && this.summary.high.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get hasMedium(): boolean {
|
|
||||||
return this.summary && this.summary.medium && this.summary.medium.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get hasLow(): boolean {
|
|
||||||
return this.summary && this.summary.low && this.summary.low.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get hasUnknown(): boolean {
|
|
||||||
return this.summary && this.summary.unknown && this.summary.unknown.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get hasNone(): boolean {
|
|
||||||
return this.summary && this.summary.noneComponents > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the percent width of each severity.
|
|
||||||
*
|
|
||||||
* @param {string} flag
|
|
||||||
* 'h': high
|
|
||||||
* 'm': medium
|
|
||||||
* 'l': low
|
|
||||||
* 'u': unknown
|
|
||||||
* 'n': none
|
|
||||||
* @returns {number}
|
|
||||||
*
|
|
||||||
* @memberOf ResultBarChartComponent
|
|
||||||
*/
|
|
||||||
percent(flag: string): number {
|
|
||||||
if (!this.summary || this.summary.totalComponents === 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let numerator: number = 0;
|
|
||||||
switch (flag) {
|
|
||||||
case 'h':
|
|
||||||
numerator = this.summary.high.length;
|
|
||||||
break;
|
|
||||||
case 'm':
|
|
||||||
numerator = this.summary.medium.length;
|
|
||||||
break;
|
|
||||||
case 'l':
|
|
||||||
numerator = this.summary.low.length;
|
|
||||||
break;
|
|
||||||
case 'u':
|
|
||||||
numerator = this.summary.unknown.length;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
numerator = this.summary.noneComponents;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.round((numerator / this.summary.totalComponents) * MAX_TIP_WIDTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { HttpModule } from '@angular/http';
|
import { HttpModule } from '@angular/http';
|
||||||
import { DebugElement } from '@angular/core';
|
import { DebugElement } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { ScanningDetailResult, VulnerabilitySeverity, RequestQueryParams } from '../service/index';
|
import { VulnerabilityItem, VulnerabilitySeverity, RequestQueryParams } from '../service/index';
|
||||||
|
|
||||||
import { ResultGridComponent } from './result-grid.component';
|
import { ResultGridComponent } from './result-grid.component';
|
||||||
import { ScanningResultService, ScanningResultDefaultService } from '../service/scanning.service';
|
import { ScanningResultService, ScanningResultDefaultService } from '../service/scanning.service';
|
||||||
@ -43,9 +43,9 @@ describe('ResultGridComponent (inline template)', () => {
|
|||||||
|
|
||||||
serviceConfig = TestBed.get(SERVICE_CONFIG);
|
serviceConfig = TestBed.get(SERVICE_CONFIG);
|
||||||
scanningService = fixture.debugElement.injector.get(ScanningResultService);
|
scanningService = fixture.debugElement.injector.get(ScanningResultService);
|
||||||
let mockData: ScanningDetailResult[] = [];
|
let mockData: VulnerabilityItem[] = [];
|
||||||
for (let i = 0; i < 30; i++) {
|
for (let i = 0; i < 30; i++) {
|
||||||
let res: ScanningDetailResult = {
|
let res: VulnerabilityItem = {
|
||||||
id: "CVE-2016-" + (8859 + i),
|
id: "CVE-2016-" + (8859 + i),
|
||||||
severity: i % 2 === 0 ? VulnerabilitySeverity.HIGH : VulnerabilitySeverity.MEDIUM,
|
severity: i % 2 === 0 ? VulnerabilitySeverity.HIGH : VulnerabilitySeverity.MEDIUM,
|
||||||
package: "package_" + i,
|
package: "package_" + i,
|
||||||
@ -57,7 +57,7 @@ describe('ResultGridComponent (inline template)', () => {
|
|||||||
mockData.push(res);
|
mockData.push(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
spy = spyOn(scanningService, 'getScanningResults')
|
spy = spyOn(scanningService, 'getVulnerabilityScanningResults')
|
||||||
.and.returnValue(Promise.resolve(mockData));
|
.and.returnValue(Promise.resolve(mockData));
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, OnInit, Input } from '@angular/core';
|
import { Component, OnInit, Input } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ScanningResultService,
|
ScanningResultService,
|
||||||
ScanningDetailResult
|
VulnerabilityItem
|
||||||
} from '../service/index';
|
} from '../service/index';
|
||||||
import { ErrorHandler } from '../error-handler/index';
|
import { ErrorHandler } from '../error-handler/index';
|
||||||
|
|
||||||
@ -10,12 +10,12 @@ import { GRID_COMPONENT_HTML } from './scanning.html';
|
|||||||
import { SCANNING_STYLES } from './scanning.css';
|
import { SCANNING_STYLES } from './scanning.css';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hbr-scan-result-grid',
|
selector: 'hbr-vulnerabilities-grid',
|
||||||
styles: [SCANNING_STYLES],
|
styles: [SCANNING_STYLES],
|
||||||
template: GRID_COMPONENT_HTML
|
template: GRID_COMPONENT_HTML
|
||||||
})
|
})
|
||||||
export class ResultGridComponent implements OnInit {
|
export class ResultGridComponent implements OnInit {
|
||||||
scanningResults: ScanningDetailResult[] = [];
|
scanningResults: VulnerabilityItem[] = [];
|
||||||
@Input() tagId: string;
|
@Input() tagId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -27,13 +27,13 @@ export class ResultGridComponent implements OnInit {
|
|||||||
this.loadResults(this.tagId);
|
this.loadResults(this.tagId);
|
||||||
}
|
}
|
||||||
|
|
||||||
showDetail(result: ScanningDetailResult): void {
|
showDetail(result: VulnerabilityItem): void {
|
||||||
console.log(result.id);
|
console.log(result.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadResults(tagId: string): void {
|
loadResults(tagId: string): void {
|
||||||
toPromise<ScanningDetailResult[]>(this.scanningService.getScanningResults(tagId))
|
toPromise<VulnerabilityItem[]>(this.scanningService.getVulnerabilityScanningResults(tagId))
|
||||||
.then((results: ScanningDetailResult[]) => {
|
.then((results: VulnerabilityItem[]) => {
|
||||||
this.scanningResults = results;
|
this.scanningResults = results;
|
||||||
})
|
})
|
||||||
.catch(error => { this.errorHandler.error(error) })
|
.catch(error => { this.errorHandler.error(error) })
|
||||||
|
@ -3,7 +3,7 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { HttpModule } from '@angular/http';
|
import { HttpModule } from '@angular/http';
|
||||||
import { DebugElement } from '@angular/core';
|
import { DebugElement } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { ScanningDetailResult, VulnerabilitySeverity } from '../service/index';
|
import { VulnerabilitySummary } from '../service/index';
|
||||||
|
|
||||||
import { ResultTipComponent } from './result-tip.component';
|
import { ResultTipComponent } from './result-tip.component';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
@ -16,6 +16,15 @@ describe('ResultTipComponent (inline template)', () => {
|
|||||||
let testConfig: IServiceConfig = {
|
let testConfig: IServiceConfig = {
|
||||||
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
||||||
};
|
};
|
||||||
|
let mockData:VulnerabilitySummary = {
|
||||||
|
total_package: 124,
|
||||||
|
package_with_none: 90,
|
||||||
|
package_with_high: 13,
|
||||||
|
package_with_medium: 10,
|
||||||
|
package_With_low: 10,
|
||||||
|
package_with_unknown: 1,
|
||||||
|
complete_timestamp: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -31,14 +40,26 @@ describe('ResultTipComponent (inline template)', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(ResultTipComponent);
|
fixture = TestBed.createComponent(ResultTipComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
component.percent = 50;
|
component.summary = mockData;
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
expect(component.severity).toEqual(VulnerabilitySeverity.UNKNOWN);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reader the bar with different width', async(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');
|
||||||
|
expect(el).not.toBeNull();
|
||||||
|
expect(el.style.width).toEqual("73px");
|
||||||
|
let el2: HTMLElement = fixture.nativeElement.querySelector('.bar-block-high');
|
||||||
|
expect(el2).not.toBeNull();
|
||||||
|
expect(el2.style.width).toEqual("10px");
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { VulnerabilitySummary, VulnerabilitySeverity } from '../service/index';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import {
|
|
||||||
ScanningBaseResult,
|
|
||||||
VulnerabilitySeverity
|
|
||||||
} from '../service/index';
|
|
||||||
import { SCANNING_STYLES } from './scanning.css';
|
import { SCANNING_STYLES } from './scanning.css';
|
||||||
import { TIP_COMPONENT_HTML } from './scanning.html';
|
import { TIP_COMPONENT_HTML } from './scanning.html';
|
||||||
|
|
||||||
@ -11,127 +9,141 @@ export const MIN_TIP_WIDTH = 5;
|
|||||||
export const MAX_TIP_WIDTH = 100;
|
export const MAX_TIP_WIDTH = 100;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hbr-scan-result-tip',
|
selector: 'hbr-vulnerability-summary-chart',
|
||||||
template: TIP_COMPONENT_HTML,
|
template: TIP_COMPONENT_HTML,
|
||||||
styles: [SCANNING_STYLES]
|
styles: [SCANNING_STYLES]
|
||||||
})
|
})
|
||||||
export class ResultTipComponent implements OnInit {
|
export class ResultTipComponent implements OnInit {
|
||||||
_percent: number = 5;
|
_tipTitle: string = "";
|
||||||
_tipTitle: string = '';
|
|
||||||
|
|
||||||
@Input() severity: VulnerabilitySeverity = VulnerabilitySeverity.UNKNOWN;
|
@Input() summary: VulnerabilitySummary = {
|
||||||
@Input() completeDateTime: Date = new Date(); //Temp
|
total_package: 0,
|
||||||
@Input() data: ScanningBaseResult[] = [];
|
package_with_none: 0,
|
||||||
@Input() noneNumber: number = 0;
|
complete_timestamp: new Date()
|
||||||
@Input()
|
};
|
||||||
public get percent(): number {
|
|
||||||
return this._percent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public set percent(percent: number) {
|
constructor(private translate: TranslateService) { }
|
||||||
this._percent = percent;
|
|
||||||
if (this._percent < MIN_TIP_WIDTH) {
|
|
||||||
this._percent = MIN_TIP_WIDTH;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._percent > MAX_TIP_WIDTH) {
|
|
||||||
this._percent = MAX_TIP_WIDTH;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getSeverityKey(): string {
|
|
||||||
switch (this.severity) {
|
|
||||||
case VulnerabilitySeverity.HIGH:
|
|
||||||
return 'VULNERABILITY.CHART.SEVERITY_HIGH';
|
|
||||||
case VulnerabilitySeverity.MEDIUM:
|
|
||||||
return 'VULNERABILITY.CHART.SEVERITY_MEDIUM';
|
|
||||||
case VulnerabilitySeverity.LOW:
|
|
||||||
return 'VULNERABILITY.CHART.SEVERITY_LOW';
|
|
||||||
case VulnerabilitySeverity.NONE:
|
|
||||||
return 'VULNERABILITY.CHART.SEVERITY_NONE';
|
|
||||||
default:
|
|
||||||
return 'VULNERABILITY.CHART.SEVERITY_UNKNOWN';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(private translateService: TranslateService) { }
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.translateService.get(this._getSeverityKey())
|
this.translate.get('VULNERABILITY.CHART.TOOLTIPS_TITLE',
|
||||||
|
{ totalVulnerability: this.totalVulnerabilities, totalPackages: this.summary.total_package })
|
||||||
.subscribe((res: string) => this._tipTitle = res);
|
.subscribe((res: string) => this._tipTitle = res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tipWidth(severity: VulnerabilitySeverity): string {
|
||||||
|
let n: number = 0;
|
||||||
|
let m: number = this.summary ? this.summary.total_package : 0;
|
||||||
|
|
||||||
|
if (m === 0) {
|
||||||
|
return 0 + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (severity) {
|
||||||
|
case VulnerabilitySeverity.HIGH:
|
||||||
|
n = this.highCount;
|
||||||
|
break;
|
||||||
|
case VulnerabilitySeverity.MEDIUM:
|
||||||
|
n = this.mediumCount;
|
||||||
|
break;
|
||||||
|
case VulnerabilitySeverity.LOW:
|
||||||
|
n = this.lowCount;
|
||||||
|
break;
|
||||||
|
case VulnerabilitySeverity.UNKNOWN:
|
||||||
|
n = this.unknownCount;
|
||||||
|
break;
|
||||||
|
case VulnerabilitySeverity.NONE:
|
||||||
|
n = this.noneCount;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
n = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width: number = Math.round((n/m)*MAX_TIP_WIDTH);
|
||||||
|
if(width < MIN_TIP_WIDTH){
|
||||||
|
width = MIN_TIP_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
return width + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
unitText(count: number): string {
|
||||||
|
if (count > 1) {
|
||||||
|
return "VULNERABILITY.PLURAL";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "VULNERABILITY.SINGULAR";
|
||||||
|
}
|
||||||
|
|
||||||
|
public get totalVulnerabilities(): number {
|
||||||
|
return this.summary.total_package - this.summary.package_with_none;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hasHigh(): boolean {
|
||||||
|
return this.highCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hasMedium(): boolean {
|
||||||
|
return this.mediumCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hasLow(): boolean {
|
||||||
|
return this.lowCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hasUnknown(): boolean {
|
||||||
|
return this.unknownCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hasNone(): boolean {
|
||||||
|
return this.noneCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public get tipTitle(): string {
|
public get tipTitle(): string {
|
||||||
if (!this.data) {
|
return this._tipTitle;
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let dataSize: number = this.data.length;
|
public get highCount(): number {
|
||||||
return this._tipTitle + ' (' + dataSize + ')';
|
return this.summary && this.summary.package_with_high ? this.summary.package_with_high : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get hasResultsToList(): boolean {
|
public get mediumCount(): number {
|
||||||
return this.data &&
|
return this.summary && this.summary.package_with_medium ? this.summary.package_with_medium : 0;
|
||||||
this.data.length > 0 && (
|
|
||||||
this.severity !== VulnerabilitySeverity.NONE &&
|
|
||||||
this.severity !== VulnerabilitySeverity.UNKNOWN
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get tipWidth(): string {
|
public get lowCount(): number {
|
||||||
return this.percent + 'px';
|
return this.summary && this.summary.package_With_low ? this.summary.package_With_low : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get tipClass(): string {
|
public get unknownCount(): number {
|
||||||
let baseClass: string = "tip-wrapper tip-block";
|
return this.summary && this.summary.package_with_unknown ? this.summary.package_with_unknown : 0;
|
||||||
|
}
|
||||||
switch (this.severity) {
|
public get noneCount(): number {
|
||||||
case VulnerabilitySeverity.HIGH:
|
return this.summary && this.summary.package_with_none ? this.summary.package_with_none : 0;
|
||||||
return baseClass + " bar-block-high";
|
|
||||||
case VulnerabilitySeverity.MEDIUM:
|
|
||||||
return baseClass + " bar-block-medium";
|
|
||||||
case VulnerabilitySeverity.LOW:
|
|
||||||
return baseClass + " bar-block-low";
|
|
||||||
case VulnerabilitySeverity.NONE:
|
|
||||||
return baseClass + " bar-block-none";
|
|
||||||
default:
|
|
||||||
return baseClass + " bar-block-unknown"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get highSuffix(): string {
|
||||||
|
return this.unitText(this.highCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isHigh(): boolean {
|
public get mediumSuffix(): string {
|
||||||
return this.severity === VulnerabilitySeverity.HIGH;
|
return this.unitText(this.mediumCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isMedium(): boolean {
|
public get lowSuffix(): string {
|
||||||
return this.severity === VulnerabilitySeverity.MEDIUM;
|
return this.unitText(this.lowCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isLow(): boolean {
|
public get unknownSuffix(): string {
|
||||||
return this.severity === VulnerabilitySeverity.LOW;
|
return this.unitText(this.unknownCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isNone(): boolean {
|
public get noneSuffix(): string {
|
||||||
return this.severity === VulnerabilitySeverity.NONE;
|
return this.unitText(this.noneCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isUnknown(): boolean {
|
public get maxWidth(): string {
|
||||||
return this.severity === VulnerabilitySeverity.UNKNOWN;
|
return MAX_TIP_WIDTH+"px";
|
||||||
}
|
|
||||||
|
|
||||||
public get tipIconClass(): string {
|
|
||||||
switch (this.severity) {
|
|
||||||
case VulnerabilitySeverity.HIGH:
|
|
||||||
return "is-error";
|
|
||||||
case VulnerabilitySeverity.MEDIUM:
|
|
||||||
return "is-warning";
|
|
||||||
case VulnerabilitySeverity.LOW:
|
|
||||||
return "is-info";
|
|
||||||
case VulnerabilitySeverity.NONE:
|
|
||||||
return "is-success";
|
|
||||||
default:
|
|
||||||
return "is-highlight"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ export const SCANNING_STYLES: string = `
|
|||||||
|
|
||||||
.bar-tooltip-font {
|
.bar-tooltip-font {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #565656;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar-tooltip-font-title {
|
.bar-tooltip-font-title {
|
||||||
@ -63,19 +63,16 @@ export const SCANNING_STYLES: string = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bar-summary {
|
.bar-summary {
|
||||||
margin-top: 5px;
|
margin-top: 12px;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar-scanning-time {
|
.bar-scanning-time {
|
||||||
margin-left: 26px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar-summary ul {
|
.bar-summary-item {
|
||||||
margin-left: 24px;
|
margin-top: 3px;
|
||||||
}
|
margin-bottom: 3px;
|
||||||
|
|
||||||
.bar-summary ul li {
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 2px;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
@ -1,24 +1,40 @@
|
|||||||
export const TIP_COMPONENT_HTML: string = `
|
export const TIP_COMPONENT_HTML: string = `
|
||||||
<div class="tip-wrapper tip-position" [style.width]='tipWidth'>
|
<div class="tip-wrapper tip-position" [style.width]='maxWidth'>
|
||||||
<clr-tooltip [clrTooltipDirection]="'top-right'" [clrTooltipSize]="'lg'">
|
<clr-tooltip [clrTooltipDirection]="'top-right'" [clrTooltipSize]="'lg'">
|
||||||
<div class="{{tipClass}}" [style.width]='tipWidth'></div>
|
<div class="tip-wrapper tip-block bar-block-high" [style.width]='tipWidth(4)'></div>
|
||||||
|
<div class="tip-wrapper tip-block bar-block-medium" [style.width]='tipWidth(3)'></div>
|
||||||
|
<div class="tip-wrapper tip-block bar-block-low" [style.width]='tipWidth(2)'></div>
|
||||||
|
<div class="tip-wrapper tip-block bar-block-unknown" [style.width]='tipWidth(1)'></div>
|
||||||
|
<div class="tip-wrapper tip-block bar-block-none" [style.width]='tipWidth(0)'></div>
|
||||||
<clr-tooltip-content>
|
<clr-tooltip-content>
|
||||||
<div>
|
<div>
|
||||||
<clr-icon *ngIf="isHigh" shape="exclamation-circle" class="{{tipIconClass}}" size="24"></clr-icon>
|
|
||||||
<clr-icon *ngIf="isMedium" shape="exclamation-triangle" class="{{tipIconClass}}" size="24"></clr-icon>
|
|
||||||
<clr-icon *ngIf="isLow" shape="info-circle" class="{{tipIconClass}}" size="24"></clr-icon>
|
|
||||||
<clr-icon *ngIf="isNone" shape="check-circle" class="{{tipIconClass}}" size="24"></clr-icon>
|
|
||||||
<clr-icon *ngIf="isUnknown" shape="help" class="{{tipIconClass}}" size="16"></clr-icon>
|
|
||||||
<span class="bar-tooltip-font bar-tooltip-font-title">{{tipTitle}}</span>
|
<span class="bar-tooltip-font bar-tooltip-font-title">{{tipTitle}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bar-summary bar-tooltip-font">
|
<div class="bar-summary bar-tooltip-fon">
|
||||||
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
|
<div *ngIf="hasHigh" class="bar-summary-item">
|
||||||
<span>{{completeDateTime | date}}</span>
|
<clr-icon shape="exclamation-circle" class="is-error" size="24"></clr-icon>
|
||||||
<div *ngIf="hasResultsToList">
|
<span>{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{ highSuffix | translate }}</span>
|
||||||
<ul *ngFor="let item of data">
|
|
||||||
<li>{{item.id}} {{item.version}} {{item.package}}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div *ngIf="hasMedium" class="bar-summary-item">
|
||||||
|
<clr-icon *ngIf="hasMedium" shape="exclamation-triangle" class="is-warning" size="24"></clr-icon>
|
||||||
|
<span>{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{ mediumSuffix | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="hasLow" class="bar-summary-item">
|
||||||
|
<clr-icon shape="info-circle" class="is-info" size="24"></clr-icon>
|
||||||
|
<span>{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{ lowSuffix | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="hasUnknown" class="bar-summary-item">
|
||||||
|
<clr-icon shape="help" size="24"></clr-icon>
|
||||||
|
<span>{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{ unknownSuffix | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="hasNone" class="bar-summary-item">
|
||||||
|
<clr-icon shape="check-circle" class="is-success" size="24"></clr-icon>
|
||||||
|
<span>{{noneCount}} {{'VULNERABILITY.SEVERITY.NONE' | translate }} {{ noneSuffix | translate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
|
||||||
|
<span>{{summary.complete_timestamp | date}}</span>
|
||||||
</div>
|
</div>
|
||||||
</clr-tooltip-content>
|
</clr-tooltip-content>
|
||||||
</clr-tooltip>
|
</clr-tooltip>
|
||||||
@ -75,11 +91,7 @@ export const BAR_CHART_COMPONENT_HTML: string = `
|
|||||||
<div class="progress loop" style="height:2px;min-height:2px;"><progress></progress></div>
|
<div class="progress loop" style="height:2px;min-height:2px;"><progress></progress></div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="completed" class="bar-state">
|
<div *ngIf="completed" class="bar-state">
|
||||||
<hbr-scan-result-tip *ngIf="hasHigh" [severity]="2" [completeDateTime]="summary.completeTimestamp" [data]="summary.high" [percent]='percent("h")'></hbr-scan-result-tip>
|
<hbr-vulnerability-summary-chart [summary]="summary"></hbr-vulnerability-summary-chart>
|
||||||
<hbr-scan-result-tip *ngIf="hasMedium" [severity]="1" [completeDateTime]="summary.completeTimestamp" [data]="summary.medium" [percent]='percent("m")'></hbr-scan-result-tip>
|
|
||||||
<hbr-scan-result-tip *ngIf="hasLow" [severity]="0" [completeDateTime]="summary.completeTimestamp" [data]="summary.low" [percent]='percent("l")'></hbr-scan-result-tip>
|
|
||||||
<hbr-scan-result-tip *ngIf="hasUnknown" [severity]="3" [completeDateTime]="summary.completeTimestamp" [data]="summary.unknown" [percent]='percent("u")'></hbr-scan-result-tip>
|
|
||||||
<hbr-scan-result-tip *ngIf="hasNone" [severity]="4" [completeDateTime]="summary.completeTimestamp" [noneNumber]="summary.noneComponents" [percent]='percent("n")'></hbr-scan-result-tip>
|
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="unknown" class="bar-state">
|
<div *ngIf="unknown" class="bar-state">
|
||||||
<clr-icon shape="warning" class="is-warning" size="24"></clr-icon>
|
<clr-icon shape="warning" class="is-warning" size="24"></clr-icon>
|
||||||
|
Loading…
Reference in New Issue
Block a user