Commit 7a0a423c authored by Steven Zou's avatar Steven Zou
Browse files

Implement vulnerability scanning components

parent 12660e0e
......@@ -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",
......
......@@ -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 },
]
};
}
......
......@@ -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.",
......
......@@ -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.",
......
......@@ -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": "当前操作被禁止,请确认你有合法的权限。",
......
......@@ -8,4 +8,5 @@ export * from './filter/index';
export * from './endpoint/index';
export * from './repository/index';
export * from './tag/index';
export * from './replication/index';
\ No newline at end of file
export * from './replication/index';
export * from './vulnerability-scanning/index';
\ No newline at end of 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;
......
......@@ -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';
......@@ -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;
}
\ No newline at end of file
......@@ -4,4 +4,5 @@ export * from './endpoint.service';
export * from './replication.service';
export * from './repository.service';
export * from './tag.service';
export * from './RequestQueryParams';
\ No newline at end of file
export * from './RequestQueryParams';
export * from './scanning.service';
\ No newline at end of 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[];
}
\ No newline at end of file
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");
});
});
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([]);
}
}
\ No newline at end of file
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
];
\ No newline at end of file
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);
});
}));
});
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,