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;
created: Date;
signature?: string;
vulnerability?: VulnerabilitySummary;
}
/**
@ -157,28 +158,28 @@ export interface SystemInfo {
//Not finalized yet
export enum VulnerabilitySeverity {
LOW, MEDIUM, HIGH, UNKNOWN, NONE
NONE, UNKNOWN, LOW, MEDIUM, HIGH
}
export interface ScanningBaseResult {
export interface VulnerabilityBase {
id: string;
severity: VulnerabilitySeverity;
package: string;
version: string;
}
export interface ScanningDetailResult extends ScanningBaseResult {
export interface VulnerabilityItem extends VulnerabilityBase {
fixedVersion: string;
layer: string;
description: string;
}
export interface ScanningResultSummary {
totalComponents: number;
noneComponents: number;
completeTimestamp: Date;
high: ScanningBaseResult[];
medium: ScanningBaseResult[];
low: ScanningBaseResult[];
unknown: ScanningBaseResult[];
export interface VulnerabilitySummary {
total_package: number;
package_with_none: number;
package_with_high?: number;
package_with_medium?: number;
package_With_low?: number;
package_with_unknown?: number;
complete_timestamp: Date;
}

View File

@ -5,8 +5,10 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { Http, URLSearchParams } from '@angular/http';
import { HTTP_JSON_OPTIONS } from '../utils';
import { ScanningDetailResult } from './interface';
import { VulnerabilitySeverity, ScanningBaseResult, ScanningResultSummary } from './interface';
import {
VulnerabilityItem,
VulnerabilitySummary
} from './interface';
/**
* Get the vulnerabilities scanning results for the specified tag.
@ -21,22 +23,22 @@ export abstract class ScanningResultService {
*
* @abstract
* @param {string} tagId
* @returns {(Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary)}
* @returns {(Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary)}
*
* @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.
*
* @abstract
* @param {string} tagId
* @returns {(Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[])}
* @returns {(Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[])}
*
* @memberOf ScanningResultService
*/
abstract getScanningResults(tagId: string): Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[];
abstract getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[];
}
@Injectable()
@ -47,7 +49,7 @@ export class ScanningResultDefaultService extends ScanningResultService {
super();
}
getScanningResultSummary(tagId: string): Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary {
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary {
if (!tagId || tagId.trim() === '') {
return Promise.reject('Bad argument');
}
@ -55,7 +57,7 @@ export class ScanningResultDefaultService extends ScanningResultService {
return Observable.of({});
}
getScanningResults(tagId: string): Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[] {
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[] {
if (!tagId || tagId.trim() === '') {
return Promise.reject('Bad argument');
}

View File

@ -52,7 +52,19 @@ export abstract class 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)
.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 { 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>[] = [
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();
});
it('Should load data', async(()=>{
it('should load data', async(()=>{
expect(spy.calls.any).toBeTruthy();
}));

View File

@ -3,7 +3,7 @@ import { By } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { DebugElement } from '@angular/core';
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 { ResultTipComponent } from './result-tip.component';
@ -16,11 +16,18 @@ describe('ResultBarChartComponent (inline template)', () => {
let component: ResultBarChartComponent;
let fixture: ComponentFixture<ResultBarChartComponent>;
let serviceConfig: IServiceConfig;
let scanningService: ScanningResultService;
let spy: jasmine.Spy;
let testConfig: IServiceConfig = {
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(() => {
TestBed.configureTestingModule({
@ -32,8 +39,7 @@ describe('ResultBarChartComponent (inline template)', () => {
ResultTipComponent],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: testConfig },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
{ provide: SERVICE_CONFIG, useValue: testConfig }
]
});
@ -43,52 +49,9 @@ describe('ResultBarChartComponent (inline template)', () => {
fixture = TestBed.createComponent(ResultBarChartComponent);
component = fixture.componentInstance;
component.tagId = "mockTag";
component.state = ScanState.COMPLETED;
component.state = ScanState.UNKNOWN;
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();
});
@ -102,22 +65,57 @@ describe('ResultBarChartComponent (inline template)', () => {
expect(serviceConfig.vulnerabilityScanningBaseEndpoint).toEqual("/api/vulnerability/testing");
});
it('should inject and call the ScanningResultService', () => {
expect(scanningService).toBeTruthy();
expect(spy.calls.any()).toBe(true, 'getScanningResultSummary called');
});
it('should get data from ScanningResultService', async(() => {
it('should show a button if status is PENDING', async(() => {
component.state = ScanState.PENDING;
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs
fixture.detectChanges();
expect(component.summary).toBeTruthy();
expect(component.summary.totalComponents).toEqual(21);
expect(component.summary.high.length).toEqual(4);
expect(component.summary.medium.length).toEqual(4);
expect(component.summary.low.length).toEqual(3);
expect(component.summary.noneComponents).toEqual(7);
let el: HTMLElement = fixture.nativeElement.querySelector('.scanning-button');
expect(el).toBeTruthy();
});
}));
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,
Input,
Output,
EventEmitter,
OnInit
EventEmitter
} from '@angular/core';
import {
ScanningResultService,
ScanningResultSummary
} from '../service/index';
import { ErrorHandler } from '../error-handler/index';
import { toPromise } from '../utils';
import { MAX_TIP_WIDTH } from './result-tip.component';
import { VulnerabilitySummary } from '../service/index';
import { SCANNING_STYLES } from './scanning.css';
import { BAR_CHART_COMPONENT_HTML } from './scanning.html';
@ -25,37 +18,21 @@ export enum ScanState {
}
@Component({
selector: 'hbr-scan-result-bar',
selector: 'hbr-vulnerability-bar',
styles: [SCANNING_STYLES],
template: BAR_CHART_COMPONENT_HTML
})
export class ResultBarChartComponent implements OnInit {
export class ResultBarChartComponent {
@Input() tagId: string = "";
@Input() state: ScanState = ScanState.UNKNOWN;
@Input() summary: ScanningResultSummary = {
totalComponents: 0,
noneComponents: 0,
completeTimestamp: new Date(),
high: [],
medium: [],
low: [],
unknown: []
@Input() summary: VulnerabilitySummary = {
total_package: 0,
package_with_none: 0,
complete_timestamp: new Date()
};
@Output() startScanning: EventEmitter<string> = new EventEmitter<string>();
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);
})
}
constructor() { }
public get completed(): boolean {
return this.state === ScanState.COMPLETED;
@ -86,66 +63,4 @@ export class ResultBarChartComponent implements OnInit {
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 { DebugElement } from '@angular/core';
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 { ScanningResultService, ScanningResultDefaultService } from '../service/scanning.service';
@ -43,9 +43,9 @@ describe('ResultGridComponent (inline template)', () => {
serviceConfig = TestBed.get(SERVICE_CONFIG);
scanningService = fixture.debugElement.injector.get(ScanningResultService);
let mockData: ScanningDetailResult[] = [];
let mockData: VulnerabilityItem[] = [];
for (let i = 0; i < 30; i++) {
let res: ScanningDetailResult = {
let res: VulnerabilityItem = {
id: "CVE-2016-" + (8859 + i),
severity: i % 2 === 0 ? VulnerabilitySeverity.HIGH : VulnerabilitySeverity.MEDIUM,
package: "package_" + i,
@ -57,7 +57,7 @@ describe('ResultGridComponent (inline template)', () => {
mockData.push(res);
}
spy = spyOn(scanningService, 'getScanningResults')
spy = spyOn(scanningService, 'getVulnerabilityScanningResults')
.and.returnValue(Promise.resolve(mockData));
fixture.detectChanges();

View File

@ -1,7 +1,7 @@
import { Component, OnInit, Input } from '@angular/core';
import {
ScanningResultService,
ScanningDetailResult
VulnerabilityItem
} from '../service/index';
import { ErrorHandler } from '../error-handler/index';
@ -10,12 +10,12 @@ import { GRID_COMPONENT_HTML } from './scanning.html';
import { SCANNING_STYLES } from './scanning.css';
@Component({
selector: 'hbr-scan-result-grid',
selector: 'hbr-vulnerabilities-grid',
styles: [SCANNING_STYLES],
template: GRID_COMPONENT_HTML
})
export class ResultGridComponent implements OnInit {
scanningResults: ScanningDetailResult[] = [];
scanningResults: VulnerabilityItem[] = [];
@Input() tagId: string;
constructor(
@ -27,13 +27,13 @@ export class ResultGridComponent implements OnInit {
this.loadResults(this.tagId);
}
showDetail(result: ScanningDetailResult): void {
showDetail(result: VulnerabilityItem): void {
console.log(result.id);
}
loadResults(tagId: string): void {
toPromise<ScanningDetailResult[]>(this.scanningService.getScanningResults(tagId))
.then((results: ScanningDetailResult[]) => {
toPromise<VulnerabilityItem[]>(this.scanningService.getVulnerabilityScanningResults(tagId))
.then((results: VulnerabilityItem[]) => {
this.scanningResults = results;
})
.catch(error => { this.errorHandler.error(error) })

View File

@ -3,7 +3,7 @@ import { By } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { DebugElement } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ScanningDetailResult, VulnerabilitySeverity } from '../service/index';
import { VulnerabilitySummary } from '../service/index';
import { ResultTipComponent } from './result-tip.component';
import { SharedModule } from '../shared/shared.module';
@ -16,6 +16,15 @@ describe('ResultTipComponent (inline template)', () => {
let testConfig: IServiceConfig = {
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(() => {
TestBed.configureTestingModule({
@ -31,14 +40,26 @@ describe('ResultTipComponent (inline template)', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ResultTipComponent);
component = fixture.componentInstance;
component.percent = 50;
component.summary = mockData;
fixture.detectChanges();
});
it('should be created', () => {
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 { VulnerabilitySummary, VulnerabilitySeverity } from '../service/index';
import { TranslateService } from '@ngx-translate/core';
import {
ScanningBaseResult,
VulnerabilitySeverity
} from '../service/index';
import { SCANNING_STYLES } from './scanning.css';
import { TIP_COMPONENT_HTML } from './scanning.html';
@ -11,127 +9,141 @@ export const MIN_TIP_WIDTH = 5;
export const MAX_TIP_WIDTH = 100;
@Component({
selector: 'hbr-scan-result-tip',
selector: 'hbr-vulnerability-summary-chart',
template: TIP_COMPONENT_HTML,
styles: [SCANNING_STYLES]
})
export class ResultTipComponent implements OnInit {
_percent: number = 5;
_tipTitle: string = '';
_tipTitle: string = "";
@Input() severity: VulnerabilitySeverity = VulnerabilitySeverity.UNKNOWN;
@Input() completeDateTime: Date = new Date(); //Temp
@Input() data: ScanningBaseResult[] = [];
@Input() noneNumber: number = 0;
@Input()
public get percent(): number {
return this._percent;
}
@Input() summary: VulnerabilitySummary = {
total_package: 0,
package_with_none: 0,
complete_timestamp: new Date()
};
public set percent(percent: number) {
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) { }
constructor(private translate: TranslateService) { }
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);
}
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 {
if (!this.data) {
return '';
return this._tipTitle;
}
let dataSize: number = this.data.length;
return this._tipTitle + ' (' + dataSize + ')';
public get highCount(): number {
return this.summary && this.summary.package_with_high ? this.summary.package_with_high : 0;
}
public get hasResultsToList(): boolean {
return this.data &&
this.data.length > 0 && (
this.severity !== VulnerabilitySeverity.NONE &&
this.severity !== VulnerabilitySeverity.UNKNOWN
);
public get mediumCount(): number {
return this.summary && this.summary.package_with_medium ? this.summary.package_with_medium : 0;
}
public get tipWidth(): string {
return this.percent + 'px';
public get lowCount(): number {
return this.summary && this.summary.package_With_low ? this.summary.package_With_low : 0;
}
public get tipClass(): string {
let baseClass: string = "tip-wrapper tip-block";
switch (this.severity) {
case VulnerabilitySeverity.HIGH:
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 unknownCount(): number {
return this.summary && this.summary.package_with_unknown ? this.summary.package_with_unknown : 0;
}
public get noneCount(): number {
return this.summary && this.summary.package_with_none ? this.summary.package_with_none : 0;
}
public get highSuffix(): string {
return this.unitText(this.highCount);
}
public get isHigh(): boolean {
return this.severity === VulnerabilitySeverity.HIGH;
public get mediumSuffix(): string {
return this.unitText(this.mediumCount);
}
public get isMedium(): boolean {
return this.severity === VulnerabilitySeverity.MEDIUM;
public get lowSuffix(): string {
return this.unitText(this.lowCount);
}
public get isLow(): boolean {
return this.severity === VulnerabilitySeverity.LOW;
public get unknownSuffix(): string {
return this.unitText(this.unknownCount);
}
public get isNone(): boolean {
return this.severity === VulnerabilitySeverity.NONE;
public get noneSuffix(): string {
return this.unitText(this.noneCount);
}
public get isUnknown(): boolean {
return this.severity === VulnerabilitySeverity.UNKNOWN;
}
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"
}
public get maxWidth(): string {
return MAX_TIP_WIDTH+"px";
}
}

View File

@ -55,7 +55,7 @@ export const SCANNING_STYLES: string = `
.bar-tooltip-font {
font-size: 13px;
color: #565656;
color: #ffffff;
}
.bar-tooltip-font-title {
@ -63,19 +63,16 @@ export const SCANNING_STYLES: string = `
}
.bar-summary {
margin-top: 5px;
margin-top: 12px;
text-align: left;
}
.bar-scanning-time {
margin-left: 26px;
margin-top: 12px;
}
.bar-summary ul {
margin-left: 24px;
}
.bar-summary ul li {
list-style-type: none;
margin: 2px;
.bar-summary-item {
margin-top: 3px;
margin-bottom: 3px;
}
`;

View File

@ -1,24 +1,40 @@
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'">
<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>
<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>
</div>
<div class="bar-summary bar-tooltip-font">
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
<span>{{completeDateTime | date}}</span>
<div *ngIf="hasResultsToList">
<ul *ngFor="let item of data">
<li>{{item.id}} {{item.version}} {{item.package}}</li>
</ul>
<div class="bar-summary bar-tooltip-fon">
<div *ngIf="hasHigh" class="bar-summary-item">
<clr-icon shape="exclamation-circle" class="is-error" size="24"></clr-icon>
<span>{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{ highSuffix | translate }}</span>
</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>
</clr-tooltip-content>
</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>
<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-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>
<hbr-vulnerability-summary-chart [summary]="summary"></hbr-vulnerability-summary-chart>
</div>
<div *ngIf="unknown" class="bar-state">
<clr-icon shape="warning" class="is-warning" size="24"></clr-icon>