Implement tag detail component & refactor vul summary bar chart

This commit is contained in:
Steven Zou 2017-06-12 19:40:51 +08:00
parent 4d2a2363a7
commit 2072fc237e
17 changed files with 692 additions and 315 deletions

View File

@ -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;
} }

View File

@ -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');
} }

View File

@ -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));
}
} }

View File

@ -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
]; ];

View 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;
}
`;

View 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>
`;

View 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");
});
}));
});

View 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";
}
}

View File

@ -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();
})); }));

View File

@ -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");
}); });
})); }));

View File

@ -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);
}
} }

View File

@ -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();

View File

@ -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) })

View File

@ -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");
});
}));
}); });

View File

@ -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"
}
} }
} }

View File

@ -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;
} }
`; `;

View File

@ -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>