Merge pull request #2375 from steven-zou/master

Implement vulnerability scanning components [unstable]
This commit is contained in:
kun wang 2017-05-25 12:07:00 +08:00 committed by GitHub
commit 565110d9f1
22 changed files with 1036 additions and 16 deletions

View File

@ -39,7 +39,8 @@
"mutationobserver-shim": "^0.3.2",
"@ngx-translate/core": "^6.0.0",
"@ngx-translate/http-loader": "0.0.3",
"ngx-cookie": "^1.0.0"
"ngx-cookie": "^1.0.0",
"intl": "^1.2.5"
},
"devDependencies": {
"@angular/cli": "^1.0.0",

View File

@ -18,6 +18,7 @@ import { SERVICE_CONFIG, IServiceConfig } from './service.config';
import { CONFIRMATION_DIALOG_DIRECTIVES } from './confirmation-dialog/index';
import { INLINE_ALERT_DIRECTIVES } from './inline-alert/index';
import { DATETIME_PICKER_DIRECTIVES } from './datetime-picker/index';
import { VULNERABILITY_DIRECTIVES } from './vulnerability-scanning/index';
import {
AccessLogService,
@ -29,7 +30,9 @@ import {
RepositoryService,
RepositoryDefaultService,
TagService,
TagDefaultService
TagDefaultService,
ScanningResultService,
ScanningResultDefaultService
} from './service/index';
import {
ErrorHandler,
@ -82,7 +85,10 @@ export interface HarborModuleConfig {
repositoryService?: Provider,
//Service implementation for tag
tagService?: Provider
tagService?: Provider,
//Service implementation for vulnerability scanning
scanningService?: Provider
}
/**
@ -116,7 +122,7 @@ export function initConfig(translateService: TranslateService, config: IServiceC
}
translateService.use(selectedLang);
console.log('initConfig => ', translateService.currentLang);
console.log('initConfig => ', translateService.currentLang);
};
}
@ -137,7 +143,8 @@ export function initConfig(translateService: TranslateService, config: IServiceC
REPLICATION_DIRECTIVES,
LIST_REPLICATION_RULE_DIRECTIVES,
CREATE_EDIT_RULE_DIRECTIVES,
DATETIME_PICKER_DIRECTIVES
DATETIME_PICKER_DIRECTIVES,
VULNERABILITY_DIRECTIVES
],
exports: [
LOG_DIRECTIVES,
@ -152,7 +159,8 @@ export function initConfig(translateService: TranslateService, config: IServiceC
REPLICATION_DIRECTIVES,
LIST_REPLICATION_RULE_DIRECTIVES,
CREATE_EDIT_RULE_DIRECTIVES,
DATETIME_PICKER_DIRECTIVES
DATETIME_PICKER_DIRECTIVES,
VULNERABILITY_DIRECTIVES
],
providers: []
})
@ -169,6 +177,7 @@ export class HarborLibraryModule {
config.replicationService || { provide: ReplicationService, useClass: ReplicationDefaultService },
config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService },
config.tagService || { provide: TagService, useClass: TagDefaultService },
config.scanningService || { provide: ScanningResultService, useClass: ScanningResultDefaultService },
//Do initializing
TranslateService,
{
@ -191,7 +200,8 @@ export class HarborLibraryModule {
config.endpointService || { provide: EndpointService, useClass: EndpointDefaultService },
config.replicationService || { provide: ReplicationService, useClass: ReplicationDefaultService },
config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService },
config.tagService || { provide: TagService, useClass: TagDefaultService }
config.tagService || { provide: TagService, useClass: TagDefaultService },
config.scanningService || { provide: ScanningResultService, useClass: ScanningResultDefaultService },
]
};
}

View File

@ -435,6 +435,34 @@ export const EN_US_LANG: any = {
"IN_PROGRESS": "Search...",
"BACK": "Back"
},
"VULNERABILITY": {
"STATE": {
"PENDING": "SCAN NOW",
"QUEUED": "Queued",
"ERROR": "Error",
"SCANNING": "Scanning",
"UNKNOWN": "Unknown"
},
"GRID": {
"PLACEHOLDER": "We couldn't find any scanning results!",
"COLUMN_ID": "Vulnerability",
"COLUMN_SEVERITY": "Severity",
"COLUMN_PACKAGE": "Package",
"COLUMN_VERSION": "Current version",
"COLUMN_FIXED": "Fixed in version",
"COLUMN_LAYER": "Introduced in layer",
"FOOT_ITEMS": "Items",
"FOOT_OF": "of"
},
"CHART": {
"SCANNING_TIME": "Scan completed",
"SEVERITY_HIGH": "High severity",
"SEVERITY_MEDIUM": "Medium severity",
"SEVERITY_LOW": "Low severity",
"SEVERITY_UNKNOWN": "Unknown",
"SEVERITY_NONE": "No Vulnerabilities"
},
},
"UNKNOWN_ERROR": "Unknown errors have occurred. Please try again later.",
"UNAUTHORIZED_ERROR": "Your session is invalid or has expired. You need to sign in to continue your action.",
"FORBIDDEN_ERROR": "You do not have the proper privileges to perform the action.",

View File

@ -433,6 +433,34 @@ export const ES_ES_LANG: any = {
"IN_PROGRESS": "Buscar...",
"BACK": "Volver"
},
"VULNERABILITY": {
"STATE": {
"PENDING": "SCAN NOW",
"QUEUED": "Queued",
"ERROR": "Error",
"SCANNING": "Scanning",
"UNKNOWN": "Unknown"
},
"GRID": {
"PLACEHOLDER": "We couldn't find any scanning results!",
"COLUMN_ID": "Vulnerability",
"COLUMN_SEVERITY": "Severity",
"COLUMN_PACKAGE": "Package",
"COLUMN_VERSION": "Current version",
"COLUMN_FIXED": "Fixed in version",
"COLUMN_LAYER": "Introduced in layer",
"FOOT_ITEMS": "Items",
"FOOT_OF": "of"
},
"CHART": {
"SCANNING_TIME": "Scan completed",
"SEVERITY_HIGH": "High severity",
"SEVERITY_MEDIUM": "Medium severity",
"SEVERITY_LOW": "Low severity",
"SEVERITY_UNKNOWN": "Unknown",
"SEVERITY_NONE": "No Vulnerabilities"
},
},
"UNKNOWN_ERROR": "Ha ocurrido un error desconocido. Por favor, inténtelo de nuevo más tarde.",
"UNAUTHORIZED_ERROR": "La sesión no es válida o ha caducado. Necesita identificarse de nuevo para llevar a cabo esa acción.",
"FORBIDDEN_ERROR": "No tienes permisos para llevar a cabo esa acción.",

View File

@ -435,6 +435,34 @@ export const ZH_CN_LANG: any = {
"IN_PROGRESS": "搜索中...",
"BACK": "返回"
},
"VULNERABILITY": {
"STATE": {
"PENDING": "开始扫描",
"QUEUED": "已入队列",
"ERROR": "错误",
"SCANNING": "扫描中",
"UNKNOWN": "未知"
},
"GRID": {
"PLACEHOLDER": "没有扫描结果!",
"COLUMN_ID": "缺陷码",
"COLUMN_SEVERITY": "严重度",
"COLUMN_PACKAGE": "组件",
"COLUMN_VERSION": "当前版本",
"COLUMN_FIXED": "修复版本",
"COLUMN_LAYER": "引入层",
"FOOT_ITEMS": "项目",
"FOOT_OF": "总共"
},
"CHART": {
"SCANNING_TIME": "扫描完成",
"SEVERITY_HIGH": "严重",
"SEVERITY_MEDIUM": "中等",
"SEVERITY_LOW": "低",
"SEVERITY_UNKNOWN": "未知",
"SEVERITY_NONE": "无缺陷"
},
},
"UNKNOWN_ERROR": "发生未知错误,请稍后再试。",
"UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续。",
"FORBIDDEN_ERROR": "当前操作被禁止,请确认你有合法的权限。",

View File

@ -8,4 +8,5 @@ export * from './filter/index';
export * from './endpoint/index';
export * from './repository/index';
export * from './tag/index';
export * from './replication/index';
export * from './replication/index';
export * from './vulnerability-scanning/index';

View File

@ -12,7 +12,7 @@ import { ErrorHandler } from '../error-handler/index';
import { SharedModule } from '../shared/shared.module';
import { FilterComponent } from '../filter/filter.component';
describe('RecentLogComponent', () => {
describe('RecentLogComponent (inline template)', () => {
let component: RecentLogComponent;
let fixture: ComponentFixture<RecentLogComponent>;
let serviceConfig: IServiceConfig;

View File

@ -17,6 +17,10 @@ import 'core-js/es6/reflect';
import 'core-js/es7/reflect';
import 'intl';
import 'intl/locale-data/jsonp/en';
import 'intl/locale-data/jsonp/es';
import 'intl/locale-data/jsonp/zh';
import 'zone.js/dist/zone';

View File

@ -84,5 +84,13 @@ export interface IServiceConfig {
* @type {boolean}
* @memberOf IServiceConfig
*/
enablei18Support?: boolean
enablei18Support?: boolean;
/**
* The base endpoint of the service used to handle vulnerability scanning.
*
* @type {string}
* @memberOf IServiceConfig
*/
vulnerabilityScanningBaseEndpoint?: string;
}

View File

@ -4,4 +4,5 @@ export * from './endpoint.service';
export * from './replication.service';
export * from './repository.service';
export * from './tag.service';
export * from './RequestQueryParams';
export * from './RequestQueryParams';
export * from './scanning.service';

View File

@ -73,11 +73,11 @@ export interface Tag extends Base {
* @extends {Base}
*/
export interface Endpoint extends Base {
endpoint: string;
name: string;
username?: string;
password?: string;
type: number;
endpoint: string;
name: string;
username?: string;
password?: string;
type: number;
}
/**
@ -143,4 +143,32 @@ export interface SessionInfo {
hasProjectAdminRole?: boolean;
hasSignedIn?: boolean;
registryUrl?: string;
}
//Not finalized yet
export enum VulnerabilitySeverity {
LOW, MEDIUM, HIGH, UNKNOWN, NONE
}
export interface ScanningBaseResult {
id: string;
severity: VulnerabilitySeverity;
package: string;
version: string;
}
export interface ScanningDetailResult extends ScanningBaseResult {
fixedVersion: string;
layer: string;
description: string;
}
export interface ScanningResultSummary {
totalComponents: number;
noneComponents: number;
completeTimestamp: Date;
high: ScanningBaseResult[];
medium: ScanningBaseResult[];
low: ScanningBaseResult[];
unknown: ScanningBaseResult[];
}

View File

@ -0,0 +1,41 @@
import { TestBed, inject } from '@angular/core/testing';
import { ScanningResultService, ScanningResultDefaultService } from './scanning.service';
import { SharedModule } from '../shared/shared.module';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
describe('ScanningResultService', () => {
const mockConfig: IServiceConfig = {
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
};
let config: IServiceConfig;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
],
providers: [
ScanningResultDefaultService,
{
provide: ScanningResultService,
useClass: ScanningResultDefaultService
}, {
provide: SERVICE_CONFIG,
useValue: mockConfig
}]
});
config = TestBed.get(SERVICE_CONFIG);
});
it('should be initialized', inject([ScanningResultDefaultService], (service: ScanningResultService) => {
expect(service).toBeTruthy();
}));
it('should inject the right config', () => {
expect(config).toBeTruthy();
expect(config.vulnerabilityScanningBaseEndpoint).toEqual("/api/vulnerability/testing");
});
});

View File

@ -0,0 +1,65 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { Injectable, Inject } from "@angular/core";
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';
/**
* Get the vulnerabilities scanning results for the specified tag.
*
* @export
* @abstract
* @class ScanningResultService
*/
export abstract class ScanningResultService {
/**
* Get the summary of vulnerability scanning result.
*
* @abstract
* @param {string} tagId
* @returns {(Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary)}
*
* @memberOf ScanningResultService
*/
abstract getScanningResultSummary(tagId: string): Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary;
/**
* Get the detailed vulnerabilities scanning results.
*
* @abstract
* @param {string} tagId
* @returns {(Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[])}
*
* @memberOf ScanningResultService
*/
abstract getScanningResults(tagId: string): Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[];
}
@Injectable()
export class ScanningResultDefaultService extends ScanningResultService {
constructor(
private http: Http,
@Inject(SERVICE_CONFIG) private config: IServiceConfig) {
super();
}
getScanningResultSummary(tagId: string): Observable<ScanningResultSummary> | Promise<ScanningResultSummary> | ScanningResultSummary {
if (!tagId || tagId.trim() === '') {
return Promise.reject('Bad argument');
}
return Observable.of({});
}
getScanningResults(tagId: string): Observable<ScanningDetailResult[]> | Promise<ScanningDetailResult[]> | ScanningDetailResult[] {
if (!tagId || tagId.trim() === '') {
return Promise.reject('Bad argument');
}
return Observable.of([]);
}
}

View File

@ -0,0 +1,13 @@
import { Type } from "@angular/core";
import { ResultGridComponent } from './result-grid.component';
import { ResultBarChartComponent } from './result-bar-chart.component';
import { ResultTipComponent } from './result-tip.component';
export * from "./result-grid.component";
export * from './result-bar-chart.component';
export const VULNERABILITY_DIRECTIVES: Type<any>[] = [
ResultGridComponent,
ResultTipComponent,
ResultBarChartComponent
];

View File

@ -0,0 +1,124 @@
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
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 { ResultBarChartComponent, ScanState } from './result-bar-chart.component';
import { ResultTipComponent } from './result-tip.component';
import { ScanningResultService, ScanningResultDefaultService } from '../service/scanning.service';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { ErrorHandler } from '../error-handler/index';
import { SharedModule } from '../shared/shared.module';
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"
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
],
declarations: [
ResultBarChartComponent,
ResultTipComponent],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: testConfig },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(ResultBarChartComponent);
component = fixture.componentInstance;
component.tagId = "mockTag";
component.state = ScanState.COMPLETED;
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();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
it('should inject the SERVICE_CONFIG', () => {
expect(serviceConfig).toBeTruthy();
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(() => {
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);
});
}));
});

View File

@ -0,0 +1,151 @@
import {
Component,
Input,
Output,
EventEmitter,
OnInit
} 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 { SCANNING_STYLES } from './scanning.css';
import { BAR_CHART_COMPONENT_HTML } from './scanning.html';
export enum ScanState {
COMPLETED, //Scanning work successfully completed
ERROR, //Error occurred when scanning
QUEUED, //Scanning job is queued
SCANNING, //Scanning in progress
PENDING, //Scanning not start
UNKNOWN //Unknown status
}
@Component({
selector: 'hbr-scan-result-bar',
styles: [SCANNING_STYLES],
template: BAR_CHART_COMPONENT_HTML
})
export class ResultBarChartComponent implements OnInit {
@Input() tagId: string = "";
@Input() state: ScanState = ScanState.UNKNOWN;
@Input() summary: ScanningResultSummary = {
totalComponents: 0,
noneComponents: 0,
completeTimestamp: new Date(),
high: [],
medium: [],
low: [],
unknown: []
};
@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);
})
}
public get completed(): boolean {
return this.state === ScanState.COMPLETED;
}
public get error(): boolean {
return this.state === ScanState.ERROR;
}
public get queued(): boolean {
return this.state === ScanState.QUEUED;
}
public get scanning(): boolean {
return this.state === ScanState.SCANNING;
}
public get pending(): boolean {
return this.state === ScanState.PENDING;
}
public get unknown(): boolean {
return this.state === ScanState.UNKNOWN;
}
scanNow(): void {
if (this.tagId && 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

@ -0,0 +1,103 @@
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
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 { ResultGridComponent } from './result-grid.component';
import { ScanningResultService, ScanningResultDefaultService } from '../service/scanning.service';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { ErrorHandler } from '../error-handler/index';
import { SharedModule } from '../shared/shared.module';
describe('ResultGridComponent (inline template)', () => {
let component: ResultGridComponent;
let fixture: ComponentFixture<ResultGridComponent>;
let serviceConfig: IServiceConfig;
let scanningService: ScanningResultService;
let spy: jasmine.Spy;
let testConfig: IServiceConfig = {
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
],
declarations: [ResultGridComponent],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: testConfig },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(ResultGridComponent);
component = fixture.componentInstance;
component.tagId = "mockTag";
serviceConfig = TestBed.get(SERVICE_CONFIG);
scanningService = fixture.debugElement.injector.get(ScanningResultService);
let mockData: ScanningDetailResult[] = [];
for (let i = 0; i < 30; i++) {
let res: ScanningDetailResult = {
id: "CVE-2016-" + (8859 + i),
severity: i % 2 === 0 ? VulnerabilitySeverity.HIGH : VulnerabilitySeverity.MEDIUM,
package: "package_" + i,
layer: "layer_" + i,
version: '4.' + i + ".0",
fixedVersion: '4.' + i + '.11',
description: "Mock data"
};
mockData.push(res);
}
spy = spyOn(scanningService, 'getScanningResults')
.and.returnValue(Promise.resolve(mockData));
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
it('should inject the SERVICE_CONFIG', () => {
expect(serviceConfig).toBeTruthy();
expect(serviceConfig.vulnerabilityScanningBaseEndpoint).toEqual("/api/vulnerability/testing");
});
it('should inject and call the ScanningResultService', () => {
expect(scanningService).toBeTruthy();
expect(spy.calls.any()).toBe(true, 'getScanningResults called');
});
it('should get data from ScanningResultService', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getRecentLogs
fixture.detectChanges();
expect(component.scanningResults).toBeTruthy();
expect(component.scanningResults.length).toEqual(30);
});
}));
it('should render data to view', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let de: DebugElement = fixture.debugElement.query(del => del.classes['datagrid-cell']);
expect(de).toBeTruthy();
let el: HTMLElement = de.nativeElement;
expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual('CVE-2016-8859');
});
}));
});

View File

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

View File

@ -0,0 +1,38 @@
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
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 { ResultTipComponent } from './result-tip.component';
import { SharedModule } from '../shared/shared.module';
describe('ResultTipComponent (inline template)', () => {
let component: ResultTipComponent;
let fixture: ComponentFixture<ResultTipComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
],
declarations: [ResultTipComponent]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(ResultTipComponent);
component = fixture.componentInstance;
component.percent = 50;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
expect(component.severity).toEqual(VulnerabilitySeverity.UNKNOWN);
});
});

View File

@ -0,0 +1,137 @@
import { Component, Input, OnInit } from '@angular/core';
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';
export const MIN_TIP_WIDTH = 5;
export const MAX_TIP_WIDTH = 100;
@Component({
selector: 'hbr-scan-result-tip',
template: TIP_COMPONENT_HTML,
styles: [SCANNING_STYLES]
})
export class ResultTipComponent implements OnInit {
_percent: number = 5;
_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;
}
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) { }
ngOnInit(): void {
this.translateService.get(this._getSeverityKey())
.subscribe((res: string) => this._tipTitle = res);
}
public get tipTitle(): string {
if (!this.data) {
return '';
}
let dataSize: number = this.data.length;
return this._tipTitle + ' (' + dataSize + ')';
}
public get hasResultsToList(): boolean {
return this.data &&
this.data.length > 0 && (
this.severity !== VulnerabilitySeverity.NONE &&
this.severity !== VulnerabilitySeverity.UNKNOWN
);
}
public get tipWidth(): string {
return this.percent + 'px';
}
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 isHigh(): boolean {
return this.severity === VulnerabilitySeverity.HIGH;
}
public get isMedium(): boolean {
return this.severity === VulnerabilitySeverity.MEDIUM;
}
public get isLow(): boolean {
return this.severity === VulnerabilitySeverity.LOW;
}
public get isNone(): boolean {
return this.severity === VulnerabilitySeverity.NONE;
}
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"
}
}
}

View File

@ -0,0 +1,81 @@
export const SCANNING_STYLES: string = `
.bar-wrapper {
width: 150px;
height: 24px;
display: inline-block;
}
.bar-state {
text-align: center !important;
}
.scanning-button {
height: 24px;
margin-top: 0px;
margin-bottom: 0px;
vertical-align: middle;
top: -6px;
position: relative;
}
.tip-wrapper {
display: inline-block;
height: 16px;
max-height: 16px;
max-width: 150px;
}
.tip-position {
margin-left: -4px;
}
.tip-block {
margin-left: -4px;
}
.bar-block-high {
background-color: red;
}
.bar-block-medium {
background-color: orange;
}
.bar-block-low {
background-color: yellow;
}
.bar-block-none {
background-color: green;
}
.bar-block-unknown {
background-color: grey;
}
.bar-tooltip-font {
font-size: 13px;
color: #565656;
}
.bar-tooltip-font-title {
font-weight: 600;
}
.bar-summary {
margin-top: 5px;
}
.bar-scanning-time {
margin-left: 26px;
}
.bar-summary ul {
margin-left: 24px;
}
.bar-summary ul li {
list-style-type: none;
margin: 2px;
}
`;

View File

@ -0,0 +1,89 @@
export const TIP_COMPONENT_HTML: string = `
<div class="tip-wrapper tip-position" [style.width]='tipWidth'>
<clr-tooltip [clrTooltipDirection]="'top-right'" [clrTooltipSize]="'lg'">
<div class="{{tipClass}}" [style.width]='tipWidth'></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>
</div>
</clr-tooltip-content>
</clr-tooltip>
</div>
`;
export const GRID_COMPONENT_HTML: string = `
<div>
<clr-datagrid>
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'severity'">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}} version</clr-dg-column>
<clr-dg-column [clrDgField]="'fixedVersion'">{{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'layer'">{{'VULNERABILITY.GRID.COLUMN_LAYER' | translate}}</clr-dg-column>
<clr-dg-column>Description</clr-dg-column>
<clr-dg-placeholder>{{'VULNERABILITY.GRID.PLACEHOLDER' | translate}}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let res of scanningResults">
<clr-dg-action-overflow>
<button class="action-item" (click)="showDetail(res)">Detail</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{res.id}}</clr-dg-cell>
<clr-dg-cell>{{res.severity}}</clr-dg-cell>
<clr-dg-cell>{{res.package}}</clr-dg-cell>
<clr-dg-cell>{{res.version}}</clr-dg-cell>
<clr-dg-cell>{{res.fixedVersion}}</clr-dg-cell>
<clr-dg-cell>{{res.layer}}</clr-dg-cell>
<clr-dg-cell>{{res.description}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'VULNERABILITY.GRID.FOOT_OF' | translate}} {{pagination.totalItems}} {{'VULNERABILITY.GRID.FOOT_ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="25" [clrDgTotalItems]="scanningResults.length"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
`;
export const BAR_CHART_COMPONENT_HTML: string = `
<div class="bar-wrapper">
<div *ngIf="pending" class="bar-state">
<button class="btn btn-link scanning-button" (click)="scanNow()">{{'VULNERABILITY.STATE.PENDING' | translate}}</button>
</div>
<div *ngIf="queued" class="bar-state">
<span>{{'VULNERABILITY.STATE.QUEUED' | translate}}</span>
</div>
<div *ngIf="error" class="bar-state">
<clr-icon shape="info-circle" class="is-error" size="24"></clr-icon>
<span style="margin-left:-5px;">{{'VULNERABILITY.STATE.ERROR' | translate}}</span>
</div>
<div *ngIf="scanning" class="bar-state">
<div>{{'VULNERABILITY.STATE.SCANNING' | translate}}</div>
<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>
</div>
<div *ngIf="unknown" class="bar-state">
<clr-icon shape="warning" class="is-warning" size="24"></clr-icon>
<span style="margin-left:-5px;">{{'VULNERABILITY.STATE.UNKNOWN' | translate}}</span>
</div>
</div>
`;