diff --git a/src/ui_ng/lib/package.json b/src/ui_ng/lib/package.json index 7ca28f2c2..f3fed7588 100644 --- a/src/ui_ng/lib/package.json +++ b/src/ui_ng/lib/package.json @@ -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", diff --git a/src/ui_ng/lib/src/harbor-library.module.ts b/src/ui_ng/lib/src/harbor-library.module.ts index 98d37ca11..af21a2aa4 100644 --- a/src/ui_ng/lib/src/harbor-library.module.ts +++ b/src/ui_ng/lib/src/harbor-library.module.ts @@ -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 }, ] }; } diff --git a/src/ui_ng/lib/src/i18n/lang/en-us-lang.ts b/src/ui_ng/lib/src/i18n/lang/en-us-lang.ts index f214b3ec9..0771a03d3 100644 --- a/src/ui_ng/lib/src/i18n/lang/en-us-lang.ts +++ b/src/ui_ng/lib/src/i18n/lang/en-us-lang.ts @@ -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.", diff --git a/src/ui_ng/lib/src/i18n/lang/es-es-lang.ts b/src/ui_ng/lib/src/i18n/lang/es-es-lang.ts index bc58a1846..c77c4a6b0 100644 --- a/src/ui_ng/lib/src/i18n/lang/es-es-lang.ts +++ b/src/ui_ng/lib/src/i18n/lang/es-es-lang.ts @@ -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.", diff --git a/src/ui_ng/lib/src/i18n/lang/zh-cn-lang.ts b/src/ui_ng/lib/src/i18n/lang/zh-cn-lang.ts index 8a3f57931..da51d3165 100644 --- a/src/ui_ng/lib/src/i18n/lang/zh-cn-lang.ts +++ b/src/ui_ng/lib/src/i18n/lang/zh-cn-lang.ts @@ -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": "当前操作被禁止,请确认你有合法的权限。", diff --git a/src/ui_ng/lib/src/index.ts b/src/ui_ng/lib/src/index.ts index 38f33349a..2a6a85c13 100644 --- a/src/ui_ng/lib/src/index.ts +++ b/src/ui_ng/lib/src/index.ts @@ -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 diff --git a/src/ui_ng/lib/src/log/recent-log.component.spec.ts b/src/ui_ng/lib/src/log/recent-log.component.spec.ts index 914a1214e..ca77d2270 100644 --- a/src/ui_ng/lib/src/log/recent-log.component.spec.ts +++ b/src/ui_ng/lib/src/log/recent-log.component.spec.ts @@ -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; let serviceConfig: IServiceConfig; diff --git a/src/ui_ng/lib/src/polyfills.ts b/src/ui_ng/lib/src/polyfills.ts index f26b6d8b6..de1f77cf2 100644 --- a/src/ui_ng/lib/src/polyfills.ts +++ b/src/ui_ng/lib/src/polyfills.ts @@ -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'; diff --git a/src/ui_ng/lib/src/service.config.ts b/src/ui_ng/lib/src/service.config.ts index 24e5cdb30..37badc95b 100644 --- a/src/ui_ng/lib/src/service.config.ts +++ b/src/ui_ng/lib/src/service.config.ts @@ -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 diff --git a/src/ui_ng/lib/src/service/index.ts b/src/ui_ng/lib/src/service/index.ts index e57632c6a..291831ad7 100644 --- a/src/ui_ng/lib/src/service/index.ts +++ b/src/ui_ng/lib/src/service/index.ts @@ -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 diff --git a/src/ui_ng/lib/src/service/interface.ts b/src/ui_ng/lib/src/service/interface.ts index f814ed57c..4997006f7 100644 --- a/src/ui_ng/lib/src/service/interface.ts +++ b/src/ui_ng/lib/src/service/interface.ts @@ -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 diff --git a/src/ui_ng/lib/src/service/scanning.service.spec.ts b/src/ui_ng/lib/src/service/scanning.service.spec.ts new file mode 100644 index 000000000..1bf549a20 --- /dev/null +++ b/src/ui_ng/lib/src/service/scanning.service.spec.ts @@ -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"); + }); +}); diff --git a/src/ui_ng/lib/src/service/scanning.service.ts b/src/ui_ng/lib/src/service/scanning.service.ts new file mode 100644 index 000000000..38c053dd0 --- /dev/null +++ b/src/ui_ng/lib/src/service/scanning.service.ts @@ -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 | Promise | ScanningResultSummary)} + * + * @memberOf ScanningResultService + */ + abstract getScanningResultSummary(tagId: string): Observable | Promise | ScanningResultSummary; + + /** + * Get the detailed vulnerabilities scanning results. + * + * @abstract + * @param {string} tagId + * @returns {(Observable | Promise | ScanningDetailResult[])} + * + * @memberOf ScanningResultService + */ + abstract getScanningResults(tagId: string): Observable | Promise | ScanningDetailResult[]; +} + +@Injectable() +export class ScanningResultDefaultService extends ScanningResultService { + constructor( + private http: Http, + @Inject(SERVICE_CONFIG) private config: IServiceConfig) { + super(); + } + + getScanningResultSummary(tagId: string): Observable | Promise | ScanningResultSummary { + if (!tagId || tagId.trim() === '') { + return Promise.reject('Bad argument'); + } + + return Observable.of({}); + } + + getScanningResults(tagId: string): Observable | Promise | ScanningDetailResult[] { + if (!tagId || tagId.trim() === '') { + return Promise.reject('Bad argument'); + } + + return Observable.of([]); + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/vulnerability-scanning/index.ts b/src/ui_ng/lib/src/vulnerability-scanning/index.ts new file mode 100644 index 000000000..4b500ad7d --- /dev/null +++ b/src/ui_ng/lib/src/vulnerability-scanning/index.ts @@ -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[] = [ + ResultGridComponent, + ResultTipComponent, + ResultBarChartComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts new file mode 100644 index 000000000..37d947623 --- /dev/null +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts @@ -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; + 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); + }); + })); + +}); diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.ts new file mode 100644 index 000000000..34b67c3bc --- /dev/null +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-bar-chart.component.ts @@ -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 = new EventEmitter(); + + constructor( + private scanningService: ScanningResultService, + private errorHandler: ErrorHandler) { } + + ngOnInit(): void { + toPromise(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); + } + + +} diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts new file mode 100644 index 000000000..d0578f70d --- /dev/null +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.spec.ts @@ -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; + 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'); + }); + })); + +}); diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts new file mode 100644 index 000000000..8356d7eb0 --- /dev/null +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-grid.component.ts @@ -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(this.scanningService.getScanningResults(tagId)) + .then((results: ScanningDetailResult[]) => { + this.scanningResults = results; + }) + .catch(error => { this.errorHandler.error(error) }) + } +} diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.spec.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.spec.ts new file mode 100644 index 000000000..b3f2332dc --- /dev/null +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.spec.ts @@ -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; + + 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); + }); + +}); diff --git a/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts new file mode 100644 index 000000000..cf35ae836 --- /dev/null +++ b/src/ui_ng/lib/src/vulnerability-scanning/result-tip.component.ts @@ -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" + } + } +} diff --git a/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts b/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts new file mode 100644 index 000000000..a16c9ba93 --- /dev/null +++ b/src/ui_ng/lib/src/vulnerability-scanning/scanning.css.ts @@ -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; +} +`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts b/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts new file mode 100644 index 000000000..95fdbfc24 --- /dev/null +++ b/src/ui_ng/lib/src/vulnerability-scanning/scanning.html.ts @@ -0,0 +1,89 @@ +export const TIP_COMPONENT_HTML: string = ` +
+ +
+ +
+ + + + + + {{tipTitle}} +
+
+ {{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} + {{completeDateTime | date}} +
+
    +
  • {{item.id}} {{item.version}} {{item.package}}
  • +
+
+
+
+
+
+`; + +export const GRID_COMPONENT_HTML: string = ` +
+ + {{'VULNERABILITY.GRID.COLUMN_ID' | translate}} + {{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}} + {{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}} + {{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}} version + {{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}} + {{'VULNERABILITY.GRID.COLUMN_LAYER' | translate}} + Description + + {{'VULNERABILITY.GRID.PLACEHOLDER' | translate}} + + + + + {{res.id}} + {{res.severity}} + {{res.package}} + {{res.version}} + {{res.fixedVersion}} + {{res.layer}} + {{res.description}} + + + + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'VULNERABILITY.GRID.FOOT_OF' | translate}} {{pagination.totalItems}} {{'VULNERABILITY.GRID.FOOT_ITEMS' | translate}} + + + +
+`; + +export const BAR_CHART_COMPONENT_HTML: string = ` +
+
+ +
+
+ {{'VULNERABILITY.STATE.QUEUED' | translate}} +
+
+ + {{'VULNERABILITY.STATE.ERROR' | translate}} +
+
+
{{'VULNERABILITY.STATE.SCANNING' | translate}}
+
+
+
+ + + + + +
+
+ + {{'VULNERABILITY.STATE.UNKNOWN' | translate}} +
+
+`; \ No newline at end of file