mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-26 12:15:20 +01:00
integrate with vulnerability API
This commit is contained in:
parent
4d9eeac434
commit
e60e4c12a6
@ -76,7 +76,7 @@ If **projectId** is set to the id of specified project, then only show the repli
|
|||||||
<hbr-endpoint></hbr-endpoint>
|
<hbr-endpoint></hbr-endpoint>
|
||||||
```
|
```
|
||||||
|
|
||||||
* **Repository and Tag Management View[updating]**
|
* **Repository and Tag Management View**
|
||||||
|
|
||||||
**projectId** is used to specify which projects the repositories are from.
|
**projectId** is used to specify which projects the repositories are from.
|
||||||
|
|
||||||
@ -98,6 +98,19 @@ watchTagClickEvent(tag: Tag): void {
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
* **Tag detail view**
|
||||||
|
|
||||||
|
This view is linked by the repository stack view only when the Clair is enabled in Harbor.
|
||||||
|
|
||||||
|
**tagId** is an @Input property and used to specify the tag of which details are displayed.
|
||||||
|
|
||||||
|
**repositoryId** is an @Input property and used to specified the repository to which the tag is belonged.
|
||||||
|
|
||||||
|
**backEvt** is an @Output event emitter and used to distribute the click event of the back arrow in the detail page.
|
||||||
|
|
||||||
|
```
|
||||||
|
<hbr-tag-detail (backEvt)="goBack($event)" [tagId]="..." [repositoryId]="..."></hbr-tag-detail>
|
||||||
|
```
|
||||||
## Configurations
|
## Configurations
|
||||||
All the related configurations are defined in the **HarborModuleConfig** interface.
|
All the related configurations are defined in the **HarborModuleConfig** interface.
|
||||||
|
|
||||||
@ -111,6 +124,7 @@ export const DefaultServiceConfig: IServiceConfig = {
|
|||||||
targetBaseEndpoint: "/api/targets",
|
targetBaseEndpoint: "/api/targets",
|
||||||
replicationRuleEndpoint: "/api/policies/replication",
|
replicationRuleEndpoint: "/api/policies/replication",
|
||||||
replicationJobEndpoint: "/api/jobs/replication",
|
replicationJobEndpoint: "/api/jobs/replication",
|
||||||
|
vulnerabilityScanningBaseEndpoint: "/api/repositories",
|
||||||
enablei18Support: false,
|
enablei18Support: false,
|
||||||
defaultLang: DEFAULT_LANG, //'en-us'
|
defaultLang: DEFAULT_LANG, //'en-us'
|
||||||
langCookieKey: DEFAULT_LANG_COOKIE_KEY, //'harbor-lang'
|
langCookieKey: DEFAULT_LANG_COOKIE_KEY, //'harbor-lang'
|
||||||
@ -147,6 +161,8 @@ It supports partially overriding. For the items not overridden, default values w
|
|||||||
|
|
||||||
* **replicationJobEndpoint:** The base endpoint of the service used to handle the replication jobs. Default is "/api/jobs/replication".
|
* **replicationJobEndpoint:** The base endpoint of the service used to handle the replication jobs. Default is "/api/jobs/replication".
|
||||||
|
|
||||||
|
* **vulnerabilityScanningBaseEndpoint:** The base endpoint of the service used to handle the vulnerability scanning results.Default value is "/api/repositories".
|
||||||
|
|
||||||
* **langCookieKey:** The cookie key used to store the current used language preference. Default is "harbor-lang".
|
* **langCookieKey:** The cookie key used to store the current used language preference. Default is "harbor-lang".
|
||||||
|
|
||||||
* **supportedLangs:** Declare what languages are supported. Default is ['en-us', 'zh-cn', 'es-es'].
|
* **supportedLangs:** Declare what languages are supported. Default is ['en-us', 'zh-cn', 'es-es'].
|
||||||
@ -215,11 +231,14 @@ HarborLibraryModule.forRoot({
|
|||||||
...
|
...
|
||||||
|
|
||||||
```
|
```
|
||||||
**3. user session(Ongoing/Discussing)**
|
**3. user session**
|
||||||
Some components may need the user authorization and authentication information to display different views. There might be two alternatives to select:
|
Some components may need the user authorization and authentication information to display different views. The following way of handing user session is supported by the library.
|
||||||
* Use @Input properties or interface to let top component or page to pass the required user session information in.
|
* Use @Input properties or interface to let top component or page to pass the required user session information in.
|
||||||
* Component retrieves the required information from some API provided by top component or page when necessary.
|
|
||||||
|
|
||||||
|
```
|
||||||
|
//In the above repository stack view, the user session informations are passed via @input properties.
|
||||||
|
[hasSignedIn]="..." [hasProjectAdminRole]="..."
|
||||||
|
```
|
||||||
**4. services**
|
**4. services**
|
||||||
The library has its own service implementations to communicate with backend APIs and transfer data. If you want to use your own data handling logic, you can implement your own services based on the defined interfaces.
|
The library has its own service implementations to communicate with backend APIs and transfer data. If you want to use your own data handling logic, you can implement your own services based on the defined interfaces.
|
||||||
|
|
||||||
@ -606,7 +625,7 @@ export class MyScanningResultService extends ScanningResultService {
|
|||||||
*
|
*
|
||||||
* @memberOf ScanningResultService
|
* @memberOf ScanningResultService
|
||||||
*/
|
*/
|
||||||
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary{
|
getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary{
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -619,7 +638,22 @@ export class MyScanningResultService extends ScanningResultService {
|
|||||||
*
|
*
|
||||||
* @memberOf ScanningResultService
|
* @memberOf ScanningResultService
|
||||||
*/
|
*/
|
||||||
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[]{
|
getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[]{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new vulnerability scanning
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {string} repoName
|
||||||
|
* @param {string} tagId
|
||||||
|
* @returns {(Observable<any> | Promise<any> | any)}
|
||||||
|
*
|
||||||
|
* @memberOf ScanningResultService
|
||||||
|
*/
|
||||||
|
startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any {
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harbor-ui",
|
"name": "harbor-ui",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harbor-ui",
|
"name": "harbor-ui",
|
||||||
"version": "0.1.42",
|
"version": "0.2.0",
|
||||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||||
"author": "VMware",
|
"author": "VMware",
|
||||||
"module": "index.js",
|
"module": "index.js",
|
||||||
|
@ -60,6 +60,7 @@ export const DefaultServiceConfig: IServiceConfig = {
|
|||||||
targetBaseEndpoint: "/api/targets",
|
targetBaseEndpoint: "/api/targets",
|
||||||
replicationRuleEndpoint: "/api/policies/replication",
|
replicationRuleEndpoint: "/api/policies/replication",
|
||||||
replicationJobEndpoint: "/api/jobs/replication",
|
replicationJobEndpoint: "/api/jobs/replication",
|
||||||
|
vulnerabilityScanningBaseEndpoint: "/api/repositories",
|
||||||
enablei18Support: false,
|
enablei18Support: false,
|
||||||
defaultLang: DEFAULT_LANG,
|
defaultLang: DEFAULT_LANG,
|
||||||
langCookieKey: DEFAULT_LANG_COOKIE_KEY,
|
langCookieKey: DEFAULT_LANG_COOKIE_KEY,
|
||||||
|
@ -21,7 +21,7 @@ export const REPOSITORY_STACKVIEW_TEMPLATE: string = `
|
|||||||
<clr-dg-cell>{{r.name}}</clr-dg-cell>
|
<clr-dg-cell>{{r.name}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
|
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
|
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
|
||||||
<hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" class="sub-grid-custom" [repoName]="r.name" [registryUrl]="registryUrl" [withNotary]="withNotary" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="true" (refreshRepo)="refresh($event)"></hbr-tag>
|
<hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" class="sub-grid-custom" [repoName]="r.name" [registryUrl]="registryUrl" [withNotary]="withNotary" [withClair]="withClair" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="true" (refreshRepo)="refresh($event)"></hbr-tag>
|
||||||
</clr-dg-row>
|
</clr-dg-row>
|
||||||
<clr-dg-footer>
|
<clr-dg-footer>
|
||||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
|
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
|
||||||
|
@ -14,6 +14,7 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
|||||||
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
|
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
|
||||||
import { TagService, TagDefaultService } from '../service/tag.service';
|
import { TagService, TagDefaultService } from '../service/tag.service';
|
||||||
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
|
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
|
||||||
|
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
|
||||||
|
|
||||||
import { click } from '../utils';
|
import { click } from '../utils';
|
||||||
|
|
||||||
@ -90,7 +91,8 @@ describe('RepositoryComponentStackview (inline template)', () => {
|
|||||||
RepositoryStackviewComponent,
|
RepositoryStackviewComponent,
|
||||||
TagComponent,
|
TagComponent,
|
||||||
ConfirmationDialogComponent,
|
ConfirmationDialogComponent,
|
||||||
FilterComponent
|
FilterComponent,
|
||||||
|
VULNERABILITY_DIRECTIVES
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ErrorHandler,
|
ErrorHandler,
|
||||||
|
@ -72,6 +72,10 @@ export class RepositoryStackviewComponent implements OnInit {
|
|||||||
return this.systemInfo ? this.systemInfo.with_notary : false;
|
return this.systemInfo ? this.systemInfo.with_notary : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get withClair(): boolean {
|
||||||
|
return this.systemInfo ? this.systemInfo.with_clair : false;
|
||||||
|
}
|
||||||
|
|
||||||
confirmDeletion(message: ConfirmationAcknowledgement) {
|
confirmDeletion(message: ConfirmationAcknowledgement) {
|
||||||
if (message &&
|
if (message &&
|
||||||
message.source === ConfirmationTargets.REPOSITORY &&
|
message.source === ConfirmationTargets.REPOSITORY &&
|
||||||
|
@ -45,7 +45,7 @@ export interface Tag extends Base {
|
|||||||
author: string;
|
author: string;
|
||||||
created: Date;
|
created: Date;
|
||||||
signature?: string;
|
signature?: string;
|
||||||
vulnerability?: VulnerabilitySummary;
|
scan_overview?: VulnerabilitySummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -145,6 +145,7 @@ export interface AccessLogItem {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export interface SystemInfo {
|
export interface SystemInfo {
|
||||||
|
with_clair?: boolean;
|
||||||
with_notary?: boolean;
|
with_notary?: boolean;
|
||||||
with_admiral?: boolean;
|
with_admiral?: boolean;
|
||||||
admiral_endpoint?: string;
|
admiral_endpoint?: string;
|
||||||
@ -156,9 +157,8 @@ export interface SystemInfo {
|
|||||||
harbor_version?: string;
|
harbor_version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Not finalized yet
|
|
||||||
export enum VulnerabilitySeverity {
|
export enum VulnerabilitySeverity {
|
||||||
NONE, UNKNOWN, LOW, MEDIUM, HIGH
|
_SEVERITY, NONE, UNKNOWN, LOW, MEDIUM, HIGH
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VulnerabilityBase {
|
export interface VulnerabilityBase {
|
||||||
@ -170,18 +170,27 @@ export interface VulnerabilityBase {
|
|||||||
|
|
||||||
export interface VulnerabilityItem extends VulnerabilityBase {
|
export interface VulnerabilityItem extends VulnerabilityBase {
|
||||||
fixedVersion: string;
|
fixedVersion: string;
|
||||||
layer: string;
|
layer?: string;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VulnerabilitySummary {
|
export interface VulnerabilitySummary {
|
||||||
total_package: number;
|
image_digest?: string;
|
||||||
package_with_none: number;
|
scan_status: string;
|
||||||
package_with_high?: number;
|
job_id?: number;
|
||||||
package_with_medium?: number;
|
severity: VulnerabilitySeverity;
|
||||||
package_With_low?: number;
|
components: VulnerabilityComponents;
|
||||||
package_with_unknown?: number;
|
update_time: Date; //Use as complete timestamp
|
||||||
complete_timestamp: Date;
|
}
|
||||||
|
|
||||||
|
export interface VulnerabilityComponents {
|
||||||
|
total: number;
|
||||||
|
summary: VulnerabilitySeverityMetrics[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VulnerabilitySeverityMetrics {
|
||||||
|
severity: VulnerabilitySeverity;
|
||||||
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TagClickEvent {
|
export interface TagClickEvent {
|
||||||
|
@ -3,7 +3,8 @@ import 'rxjs/add/observable/of';
|
|||||||
import { Injectable, Inject } from "@angular/core";
|
import { Injectable, Inject } from "@angular/core";
|
||||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||||
import { Http, URLSearchParams } from '@angular/http';
|
import { Http, URLSearchParams } from '@angular/http';
|
||||||
import { HTTP_JSON_OPTIONS } from '../utils';
|
import { buildHttpRequestOptions, HTTP_JSON_OPTIONS } from '../utils';
|
||||||
|
import { RequestQueryParams } from './RequestQueryParams';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VulnerabilityItem,
|
VulnerabilityItem,
|
||||||
@ -27,7 +28,7 @@ export abstract class ScanningResultService {
|
|||||||
*
|
*
|
||||||
* @memberOf ScanningResultService
|
* @memberOf ScanningResultService
|
||||||
*/
|
*/
|
||||||
abstract getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary;
|
abstract getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the detailed vulnerabilities scanning results.
|
* Get the detailed vulnerabilities scanning results.
|
||||||
@ -38,30 +39,60 @@ export abstract class ScanningResultService {
|
|||||||
*
|
*
|
||||||
* @memberOf ScanningResultService
|
* @memberOf ScanningResultService
|
||||||
*/
|
*/
|
||||||
abstract getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[];
|
abstract getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new vulnerability scanning
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {string} repoName
|
||||||
|
* @param {string} tagId
|
||||||
|
* @returns {(Observable<any> | Promise<any> | any)}
|
||||||
|
*
|
||||||
|
* @memberOf ScanningResultService
|
||||||
|
*/
|
||||||
|
abstract startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ScanningResultDefaultService extends ScanningResultService {
|
export class ScanningResultDefaultService extends ScanningResultService {
|
||||||
|
_baseUrl: string = '/api/repositories';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: Http,
|
private http: Http,
|
||||||
@Inject(SERVICE_CONFIG) private config: IServiceConfig) {
|
@Inject(SERVICE_CONFIG) private config: IServiceConfig) {
|
||||||
super();
|
super();
|
||||||
|
if (this.config && this.config.vulnerabilityScanningBaseEndpoint) {
|
||||||
|
this._baseUrl = this.config.vulnerabilityScanningBaseEndpoint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary {
|
getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary {
|
||||||
if (!tagId || tagId.trim() === '') {
|
if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') {
|
||||||
return Promise.reject('Bad argument');
|
return Promise.reject('Bad argument');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Observable.of({});
|
return Observable.of({});
|
||||||
}
|
}
|
||||||
|
|
||||||
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[] {
|
getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[] {
|
||||||
if (!tagId || tagId.trim() === '') {
|
if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') {
|
||||||
return Promise.reject('Bad argument');
|
return Promise.reject('Bad argument');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Observable.of([]);
|
return this.http.get(`${this._baseUrl}/${repoName}/tags/${tagId}/vulnerability/details`, buildHttpRequestOptions(queryParams)).toPromise()
|
||||||
|
.then(response => response.json() as VulnerabilityItem[])
|
||||||
|
.catch(error => Promise.reject(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any {
|
||||||
|
if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') {
|
||||||
|
return Promise.reject('Bad argument');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.post(`${this._baseUrl}/${repoName}/tags/${tagId}/scan`, null).toPromise()
|
||||||
|
.then(() => { return true })
|
||||||
|
.catch(error => Promise.reject(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,10 +7,10 @@ export const TAG_DETAIL_HTML: string = `
|
|||||||
</div>
|
</div>
|
||||||
<div class="title-block">
|
<div class="title-block">
|
||||||
<div class="tag-name">
|
<div class="tag-name">
|
||||||
{{tagDetails.name}}:v{{tagDetails.docker_version}}
|
{{tagDetails.name}}
|
||||||
</div>
|
</div>
|
||||||
<div class="tag-timestamp">
|
<div class="tag-timestamp">
|
||||||
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{tagDetails.author}}
|
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{author | translate}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -23,11 +23,13 @@ export const TAG_DETAIL_HTML: string = `
|
|||||||
<div class="image-detail-label">
|
<div class="image-detail-label">
|
||||||
<div>{{'TAG.ARCHITECTURE' | translate }}</div>
|
<div>{{'TAG.ARCHITECTURE' | translate }}</div>
|
||||||
<div>{{'TAG.OS' | translate }}</div>
|
<div>{{'TAG.OS' | translate }}</div>
|
||||||
|
<div>{{'TAG.DOCKER_VERSION' | translate }}</div>
|
||||||
<div>{{'TAG.SCAN_COMPLETION_TIME' | translate }}</div>
|
<div>{{'TAG.SCAN_COMPLETION_TIME' | translate }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="image-detail-value">
|
<div class="image-detail-value">
|
||||||
<div>{{tagDetails.architecture}}</div>
|
<div>{{tagDetails.architecture}}</div>
|
||||||
<div>{{tagDetails.os}}</div>
|
<div>{{tagDetails.os}}</div>
|
||||||
|
<div>{{tagDetails.docker_version}}</div>
|
||||||
<div>{{scanCompletedDatetime | date}}</div>
|
<div>{{scanCompletedDatetime | date}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -67,7 +69,7 @@ export const TAG_DETAIL_HTML: string = `
|
|||||||
</section>
|
</section>
|
||||||
<section class="detail-section">
|
<section class="detail-section">
|
||||||
<div class="vulnerability-block">
|
<div class="vulnerability-block">
|
||||||
<hbr-vulnerabilities-grid tagId="tagId"></hbr-vulnerabilities-grid>
|
<hbr-vulnerabilities-grid [repositoryId]="repositoryId" [tagId]="tagId"></hbr-vulnerabilities-grid>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
@ -5,25 +5,40 @@ import { ResultGridComponent } from '../vulnerability-scanning/result-grid.compo
|
|||||||
import { TagDetailComponent } from './tag-detail.component';
|
import { TagDetailComponent } from './tag-detail.component';
|
||||||
|
|
||||||
import { ErrorHandler } from '../error-handler/error-handler';
|
import { ErrorHandler } from '../error-handler/error-handler';
|
||||||
import { Tag, VulnerabilitySummary } from '../service/interface';
|
import { Tag, VulnerabilitySummary, VulnerabilityItem, VulnerabilitySeverity } from '../service/interface';
|
||||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||||
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
|
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
|
||||||
import { FilterComponent } from '../filter/index';
|
import { FilterComponent } from '../filter/index';
|
||||||
|
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||||
|
|
||||||
describe('TagDetailComponent (inline template)', () => {
|
describe('TagDetailComponent (inline template)', () => {
|
||||||
|
|
||||||
let comp: TagDetailComponent;
|
let comp: TagDetailComponent;
|
||||||
let fixture: ComponentFixture<TagDetailComponent>;
|
let fixture: ComponentFixture<TagDetailComponent>;
|
||||||
let tagService: TagService;
|
let tagService: TagService;
|
||||||
|
let scanningService: ScanningResultService;
|
||||||
let spy: jasmine.Spy;
|
let spy: jasmine.Spy;
|
||||||
|
let vulSpy: jasmine.Spy;
|
||||||
let mockVulnerability: VulnerabilitySummary = {
|
let mockVulnerability: VulnerabilitySummary = {
|
||||||
total_package: 124,
|
scan_status: VULNERABILITY_SCAN_STATUS.finished,
|
||||||
package_with_none: 92,
|
severity: 5,
|
||||||
package_with_high: 10,
|
update_time: new Date(),
|
||||||
package_with_medium: 6,
|
components: {
|
||||||
package_With_low: 13,
|
total: 124,
|
||||||
package_with_unknown: 3,
|
summary: [{
|
||||||
complete_timestamp: new Date()
|
severity: 1,
|
||||||
|
count: 90
|
||||||
|
}, {
|
||||||
|
severity: 3,
|
||||||
|
count: 10
|
||||||
|
}, {
|
||||||
|
severity: 4,
|
||||||
|
count: 10
|
||||||
|
}, {
|
||||||
|
severity: 5,
|
||||||
|
count: 13
|
||||||
|
}]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let mockTag: Tag = {
|
let mockTag: Tag = {
|
||||||
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
|
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
|
||||||
@ -34,7 +49,7 @@ describe('TagDetailComponent (inline template)', () => {
|
|||||||
"author": "steven",
|
"author": "steven",
|
||||||
"created": new Date("2016-11-08T22:41:15.912313785Z"),
|
"created": new Date("2016-11-08T22:41:15.912313785Z"),
|
||||||
"signature": null,
|
"signature": null,
|
||||||
vulnerability: mockVulnerability
|
scan_overview: mockVulnerability
|
||||||
};
|
};
|
||||||
|
|
||||||
let config: IServiceConfig = {
|
let config: IServiceConfig = {
|
||||||
@ -70,6 +85,22 @@ describe('TagDetailComponent (inline template)', () => {
|
|||||||
tagService = fixture.debugElement.injector.get(TagService);
|
tagService = fixture.debugElement.injector.get(TagService);
|
||||||
spy = spyOn(tagService, 'getTag').and.returnValues(Promise.resolve(mockTag));
|
spy = spyOn(tagService, 'getTag').and.returnValues(Promise.resolve(mockTag));
|
||||||
|
|
||||||
|
let mockData: VulnerabilityItem[] = [];
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
let res: VulnerabilityItem = {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
scanningService = fixture.debugElement.injector.get(ScanningResultService);
|
||||||
|
vulSpy = spyOn(scanningService, 'getVulnerabilityScanningResults').and.returnValue(Promise.resolve(mockData));
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -85,7 +116,7 @@ describe('TagDetailComponent (inline template)', () => {
|
|||||||
|
|
||||||
let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name');
|
let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name');
|
||||||
expect(el).toBeTruthy();
|
expect(el).toBeTruthy();
|
||||||
expect(el.textContent.trim()).toEqual('nginx:v1.12.3');
|
expect(el.textContent.trim()).toEqual('nginx');
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -113,7 +144,7 @@ describe('TagDetailComponent (inline template)', () => {
|
|||||||
expect(el).toBeTruthy();
|
expect(el).toBeTruthy();
|
||||||
let el2: HTMLElement = el.querySelector('div');
|
let el2: HTMLElement = el.querySelector('div');
|
||||||
expect(el2).toBeTruthy();
|
expect(el2).toBeTruthy();
|
||||||
expect(el2.textContent.trim()).toEqual("10 VULNERABILITY.SEVERITY.HIGH VULNERABILITY.PLURAL");
|
expect(el2.textContent.trim()).toEqual("13 VULNERABILITY.SEVERITY.HIGH VULNERABILITY.PLURAL");
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
|||||||
import { TAG_DETAIL_STYLES } from './tag-detail.component.css';
|
import { TAG_DETAIL_STYLES } from './tag-detail.component.css';
|
||||||
import { TAG_DETAIL_HTML } from './tag-detail.component.html';
|
import { TAG_DETAIL_HTML } from './tag-detail.component.html';
|
||||||
|
|
||||||
import { TagService, Tag } from '../service/index';
|
import { TagService, Tag, VulnerabilitySeverity } from '../service/index';
|
||||||
import { toPromise } from '../utils';
|
import { toPromise } from '../utils';
|
||||||
import { ErrorHandler } from '../error-handler/index';
|
import { ErrorHandler } from '../error-handler/index';
|
||||||
|
|
||||||
@ -15,6 +15,11 @@ import { ErrorHandler } from '../error-handler/index';
|
|||||||
providers: []
|
providers: []
|
||||||
})
|
})
|
||||||
export class TagDetailComponent implements OnInit {
|
export class TagDetailComponent implements OnInit {
|
||||||
|
_highCount: number = 0;
|
||||||
|
_mediumCount: number = 0;
|
||||||
|
_lowCount: number = 0;
|
||||||
|
_unknownCount: number = 0;
|
||||||
|
|
||||||
@Input() tagId: string;
|
@Input() tagId: string;
|
||||||
@Input() repositoryId: string;
|
@Input() repositoryId: string;
|
||||||
tagDetails: Tag = {
|
tagDetails: Tag = {
|
||||||
@ -36,7 +41,32 @@ export class TagDetailComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.repositoryId && this.tagId) {
|
if (this.repositoryId && this.tagId) {
|
||||||
toPromise<Tag>(this.tagService.getTag(this.repositoryId, this.tagId))
|
toPromise<Tag>(this.tagService.getTag(this.repositoryId, this.tagId))
|
||||||
.then(response => this.tagDetails = response)
|
.then(response => {
|
||||||
|
this.tagDetails = response;
|
||||||
|
if (this.tagDetails &&
|
||||||
|
this.tagDetails.scan_overview &&
|
||||||
|
this.tagDetails.scan_overview.components &&
|
||||||
|
this.tagDetails.scan_overview.components.summary) {
|
||||||
|
this.tagDetails.scan_overview.components.summary.forEach(item => {
|
||||||
|
switch (item.severity) {
|
||||||
|
case VulnerabilitySeverity.UNKNOWN:
|
||||||
|
this._unknownCount += item.count;
|
||||||
|
break;
|
||||||
|
case VulnerabilitySeverity.LOW:
|
||||||
|
this._lowCount += item.count;
|
||||||
|
break;
|
||||||
|
case VulnerabilitySeverity.MEDIUM:
|
||||||
|
this._mediumCount += item.count;
|
||||||
|
break;
|
||||||
|
case VulnerabilitySeverity.HIGH:
|
||||||
|
this._highCount += item.count;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(error => this.errorHandler.error(error))
|
.catch(error => this.errorHandler.error(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,29 +75,29 @@ export class TagDetailComponent implements OnInit {
|
|||||||
this.backEvt.emit(this.tagId);
|
this.backEvt.emit(this.tagId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get author(): string {
|
||||||
|
return this.tagDetails && this.tagDetails.author? this.tagDetails.author: 'TAG.ANONYMITY';
|
||||||
|
}
|
||||||
|
|
||||||
public get highCount(): number {
|
public get highCount(): number {
|
||||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
return this._highCount;
|
||||||
this.tagDetails.vulnerability.package_with_high : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get mediumCount(): number {
|
public get mediumCount(): number {
|
||||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
return this._mediumCount;
|
||||||
this.tagDetails.vulnerability.package_with_medium : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get lowCount(): number {
|
public get lowCount(): number {
|
||||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
return this._lowCount;
|
||||||
this.tagDetails.vulnerability.package_With_low : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get unknownCount(): number {
|
public get unknownCount(): number {
|
||||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
return this._unknownCount;
|
||||||
this.tagDetails.vulnerability.package_with_unknown : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get scanCompletedDatetime(): Date {
|
public get scanCompletedDatetime(): Date {
|
||||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
return this.tagDetails && this.tagDetails.scan_overview ?
|
||||||
this.tagDetails.vulnerability.complete_timestamp : new Date();
|
this.tagDetails.scan_overview.update_time : new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get suffixForHigh(): string {
|
public get suffixForHigh(): string {
|
||||||
|
@ -30,4 +30,11 @@ export const TAG_STYLE = `
|
|||||||
:host >>> .datagrid .datagrid-body .datagrid-row-master {
|
:host >>> .datagrid .datagrid-body .datagrid-row-master {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.truncated {
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow:ellipsis;
|
||||||
|
}
|
||||||
`;
|
`;
|
@ -14,23 +14,27 @@ export const TAG_TEMPLATE = `
|
|||||||
|
|
||||||
<h2 *ngIf="!isEmbedded" class="sub-header-title">{{repoName}}</h2>
|
<h2 *ngIf="!isEmbedded" class="sub-header-title">{{repoName}}</h2>
|
||||||
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded">
|
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded">
|
||||||
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
|
<clr-dg-column style="width: 80px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
|
<clr-dg-column style="min-width: 180px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
|
<clr-dg-column style="width: 80px;" *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
|
<clr-dg-column style="width: 150px;" *ngIf="withClair">{{'VULNERABILITY.SINGULAR' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column [clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
|
<clr-dg-column style="width: 100px;">{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'docker_version'">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
|
<clr-dg-column style="width: 160px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'architecture'">{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
|
<clr-dg-column style="width: 80px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'os'">{{'REPOSITORY.OS' | translate}}</clr-dg-column>
|
<clr-dg-column style="width: 80px;" [clrDgField]="'architecture'" *ngIf="!withClair">{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
|
||||||
|
<clr-dg-column style="width: 80px;" [clrDgField]="'os'" *ngIf="!withClair">{{'REPOSITORY.OS' | translate}}</clr-dg-column>
|
||||||
<clr-dg-placeholder>{{'TGA.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
<clr-dg-placeholder>{{'TGA.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||||
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
||||||
<clr-dg-action-overflow>
|
<clr-dg-action-overflow>
|
||||||
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
|
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
|
||||||
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
|
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
|
||||||
</clr-dg-action-overflow>
|
</clr-dg-action-overflow>
|
||||||
<clr-dg-cell><a href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a></clr-dg-cell>
|
<clr-dg-cell style="width: 80px;" [ngSwitch]="withClair">
|
||||||
<clr-dg-cell>docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
|
<a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a>
|
||||||
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signature !== null">
|
<span *ngSwitchDefault>{{t.name}}</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell style="min-width: 180px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell style="width: 80px;" *ngIf="withNotary" [ngSwitch]="t.signature !== null">
|
||||||
<clr-icon shape="check" *ngSwitchCase="true" style="color: #1D5100;"></clr-icon>
|
<clr-icon shape="check" *ngSwitchCase="true" style="color: #1D5100;"></clr-icon>
|
||||||
<clr-icon shape="close" *ngSwitchCase="false" style="color: #C92100;"></clr-icon>
|
<clr-icon shape="close" *ngSwitchCase="false" style="color: #C92100;"></clr-icon>
|
||||||
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
||||||
@ -38,11 +42,14 @@ export const TAG_TEMPLATE = `
|
|||||||
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
|
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
|
||||||
</a>
|
</a>
|
||||||
</clr-dg-cell>
|
</clr-dg-cell>
|
||||||
<clr-dg-cell>{{t.author}}</clr-dg-cell>
|
<clr-dg-cell style="width: 150px;" *ngIf="withClair">
|
||||||
<clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell>
|
<hbr-vulnerability-bar [tagId]="t.name" [summary]="t.scan_overview" (startScanning)="scanTag($event)"></hbr-vulnerability-bar>
|
||||||
<clr-dg-cell>{{t.docker_version}}</clr-dg-cell>
|
</clr-dg-cell>
|
||||||
<clr-dg-cell>{{t.architecture}}</clr-dg-cell>
|
<clr-dg-cell style="width: 100px;">{{t.author}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{t.os}}</clr-dg-cell>
|
<clr-dg-cell style="width: 160px;">{{t.created | date: 'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.docker_version}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.architecture}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.os}}</clr-dg-cell>
|
||||||
</clr-dg-row>
|
</clr-dg-row>
|
||||||
<clr-dg-footer>
|
<clr-dg-footer>
|
||||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
|
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
|
||||||
|
@ -10,9 +10,13 @@ import { TagComponent } from './tag.component';
|
|||||||
import { ErrorHandler } from '../error-handler/error-handler';
|
import { ErrorHandler } from '../error-handler/error-handler';
|
||||||
import { Tag } from '../service/interface';
|
import { Tag } from '../service/interface';
|
||||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||||
import { TagService, TagDefaultService } from '../service/tag.service';
|
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
|
||||||
|
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
|
||||||
|
import { FILTER_DIRECTIVES } from '../filter/index'
|
||||||
|
|
||||||
describe('TagComponent (inline template)', ()=> {
|
import { Observable, Subscription } from 'rxjs/Rx';
|
||||||
|
|
||||||
|
describe('TagComponent (inline template)', () => {
|
||||||
|
|
||||||
let comp: TagComponent;
|
let comp: TagComponent;
|
||||||
let fixture: ComponentFixture<TagComponent>;
|
let fixture: ComponentFixture<TagComponent>;
|
||||||
@ -35,24 +39,27 @@ describe('TagComponent (inline template)', ()=> {
|
|||||||
repositoryBaseEndpoint: '/api/repositories/testing'
|
repositoryBaseEndpoint: '/api/repositories/testing'
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async(()=>{
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
SharedModule
|
SharedModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
TagComponent,
|
TagComponent,
|
||||||
ConfirmationDialogComponent
|
ConfirmationDialogComponent,
|
||||||
|
VULNERABILITY_DIRECTIVES,
|
||||||
|
FILTER_DIRECTIVES
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ErrorHandler,
|
ErrorHandler,
|
||||||
{ provide: SERVICE_CONFIG, useValue: config },
|
{ provide: SERVICE_CONFIG, useValue: config },
|
||||||
{ provide: TagService, useClass: TagDefaultService }
|
{ provide: TagService, useClass: TagDefaultService },
|
||||||
|
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(()=>{
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(TagComponent);
|
fixture = TestBed.createComponent(TagComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
@ -68,15 +75,15 @@ describe('TagComponent (inline template)', ()=> {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load data', async(()=>{
|
it('should load data', async(() => {
|
||||||
expect(spy.calls.any).toBeTruthy();
|
expect(spy.calls.any).toBeTruthy();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should load and render data', async(()=>{
|
it('should load and render data', async(() => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
fixture.whenStable().then(()=>{
|
fixture.whenStable().then(() => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
let de: DebugElement = fixture.debugElement.query(del=>del.classes['datagrid-cell']);
|
let de: DebugElement = fixture.debugElement.query(del => del.classes['datagrid-cell']);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(de).toBeTruthy();
|
expect(de).toBeTruthy();
|
||||||
let el: HTMLElement = de.nativeElement;
|
let el: HTMLElement = de.nativeElement;
|
||||||
|
@ -11,7 +11,17 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
import { Component, OnInit, ViewChild, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
ViewChild,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
EventEmitter,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
OnDestroy
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
import { TagService } from '../service/tag.service';
|
import { TagService } from '../service/tag.service';
|
||||||
|
|
||||||
@ -27,19 +37,25 @@ import { Tag, TagClickEvent } from '../service/interface';
|
|||||||
import { TAG_TEMPLATE } from './tag.component.html';
|
import { TAG_TEMPLATE } from './tag.component.html';
|
||||||
import { TAG_STYLE } from './tag.component.css';
|
import { TAG_STYLE } from './tag.component.css';
|
||||||
|
|
||||||
import { toPromise, CustomComparator } from '../utils';
|
import { toPromise, CustomComparator, VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { State, Comparator } from 'clarity-angular';
|
import { State, Comparator } from 'clarity-angular';
|
||||||
|
|
||||||
|
import { ScanningResultService } from '../service/index';
|
||||||
|
|
||||||
|
import { Observable, Subscription } from 'rxjs/Rx';
|
||||||
|
|
||||||
|
const STATE_CHECK_INTERVAL: number = 2000;//2s
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hbr-tag',
|
selector: 'hbr-tag',
|
||||||
template: TAG_TEMPLATE,
|
template: TAG_TEMPLATE,
|
||||||
styles: [TAG_STYLE],
|
styles: [TAG_STYLE],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class TagComponent implements OnInit {
|
export class TagComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@Input() projectId: number;
|
@Input() projectId: number;
|
||||||
@Input() repoName: string;
|
@Input() repoName: string;
|
||||||
@ -49,6 +65,7 @@ export class TagComponent implements OnInit {
|
|||||||
@Input() hasProjectAdminRole: boolean;
|
@Input() hasProjectAdminRole: boolean;
|
||||||
@Input() registryUrl: string;
|
@Input() registryUrl: string;
|
||||||
@Input() withNotary: boolean;
|
@Input() withNotary: boolean;
|
||||||
|
@Input() withClair: boolean;
|
||||||
|
|
||||||
@Output() refreshRepo = new EventEmitter<boolean>();
|
@Output() refreshRepo = new EventEmitter<boolean>();
|
||||||
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
||||||
@ -66,6 +83,10 @@ export class TagComponent implements OnInit {
|
|||||||
|
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
|
|
||||||
|
stateCheckTimer: Subscription;
|
||||||
|
tagsInScanning: { [key: string]: any } = {};
|
||||||
|
scanningTagCount: number = 0;
|
||||||
|
|
||||||
@ViewChild('confirmationDialog')
|
@ViewChild('confirmationDialog')
|
||||||
confirmationDialog: ConfirmationDialogComponent;
|
confirmationDialog: ConfirmationDialogComponent;
|
||||||
|
|
||||||
@ -73,6 +94,7 @@ export class TagComponent implements OnInit {
|
|||||||
private errorHandler: ErrorHandler,
|
private errorHandler: ErrorHandler,
|
||||||
private tagService: TagService,
|
private tagService: TagService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
|
private scanningService: ScanningResultService,
|
||||||
private ref: ChangeDetectorRef) { }
|
private ref: ChangeDetectorRef) { }
|
||||||
|
|
||||||
confirmDeletion(message: ConfirmationAcknowledgement) {
|
confirmDeletion(message: ConfirmationAcknowledgement) {
|
||||||
@ -108,11 +130,24 @@ export class TagComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.retrieve();
|
this.retrieve();
|
||||||
|
|
||||||
|
this.stateCheckTimer = Observable.timer(STATE_CHECK_INTERVAL, STATE_CHECK_INTERVAL).subscribe(() => {
|
||||||
|
if (this.scanningTagCount > 0) {
|
||||||
|
this.updateScanningStates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.stateCheckTimer) {
|
||||||
|
this.stateCheckTimer.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
retrieve() {
|
retrieve() {
|
||||||
this.tags = [];
|
this.tags = [];
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
toPromise<Tag[]>(this.tagService
|
toPromise<Tag[]>(this.tagService
|
||||||
.getTags(this.repoName))
|
.getTags(this.repoName))
|
||||||
.then(items => {
|
.then(items => {
|
||||||
@ -177,4 +212,47 @@ export class TagComponent implements OnInit {
|
|||||||
this.tagClickEvent.emit(evt);
|
this.tagClickEvent.emit(evt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scanTag(tagId: string): void {
|
||||||
|
//Double check
|
||||||
|
if (this.tagsInScanning[tagId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toPromise<any>(this.scanningService.startVulnerabilityScanning(this.repoName, tagId))
|
||||||
|
.then(() => {
|
||||||
|
//Add to scanning map
|
||||||
|
this.tagsInScanning[tagId] = tagId;
|
||||||
|
//Counting
|
||||||
|
this.scanningTagCount += 1;
|
||||||
|
})
|
||||||
|
.catch(error => this.errorHandler.error(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScanningStates(): void {
|
||||||
|
toPromise<Tag[]>(this.tagService
|
||||||
|
.getTags(this.repoName))
|
||||||
|
.then(items => {
|
||||||
|
console.debug("updateScanningStates called!");
|
||||||
|
//Reset the scanning states
|
||||||
|
this.tagsInScanning = {};
|
||||||
|
this.scanningTagCount = 0;
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.scan_overview) {
|
||||||
|
if (item.scan_overview.scan_status === VULNERABILITY_SCAN_STATUS.pending ||
|
||||||
|
item.scan_overview.scan_status === VULNERABILITY_SCAN_STATUS.running) {
|
||||||
|
this.tagsInScanning[item.name] = item.name;
|
||||||
|
this.scanningTagCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tags = items;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.errorHandler.error(error);
|
||||||
|
});
|
||||||
|
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||||
|
setTimeout(() => clearInterval(hnd), 1000);
|
||||||
|
}
|
||||||
}
|
}
|
@ -102,12 +102,12 @@ export class CustomComparator<T> implements Comparator<T> {
|
|||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
compare(a: {[key: string]: any| any[]}, b: {[key: string]: any| any[]}) {
|
compare(a: { [key: string]: any | any[] }, b: { [key: string]: any | any[] }) {
|
||||||
let comp = 0;
|
let comp = 0;
|
||||||
if(a && b) {
|
if (a && b) {
|
||||||
let fieldA = a[this.fieldName];
|
let fieldA = a[this.fieldName];
|
||||||
let fieldB = b[this.fieldName];
|
let fieldB = b[this.fieldName];
|
||||||
switch(this.type) {
|
switch (this.type) {
|
||||||
case "number":
|
case "number":
|
||||||
comp = fieldB - fieldA;
|
comp = fieldB - fieldA;
|
||||||
break;
|
break;
|
||||||
@ -124,3 +124,15 @@ export class CustomComparator<T> implements Comparator<T> {
|
|||||||
* The default page size
|
* The default page size
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_PAGE_SIZE: number = 15;
|
export const DEFAULT_PAGE_SIZE: number = 15;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state of vulnerability scanning
|
||||||
|
*/
|
||||||
|
export const VULNERABILITY_SCAN_STATUS = {
|
||||||
|
unknown: "n/a",
|
||||||
|
pending: "pending",
|
||||||
|
running: "running",
|
||||||
|
error: "error",
|
||||||
|
stopped: "stopped",
|
||||||
|
finished: "finished"
|
||||||
|
};
|
@ -3,6 +3,7 @@ import { ResultGridComponent } from './result-grid.component';
|
|||||||
import { ResultBarChartComponent } from './result-bar-chart.component';
|
import { ResultBarChartComponent } from './result-bar-chart.component';
|
||||||
import { ResultTipComponent } from './result-tip.component';
|
import { ResultTipComponent } from './result-tip.component';
|
||||||
|
|
||||||
|
export * from './result-tip.component';
|
||||||
export * from "./result-grid.component";
|
export * from "./result-grid.component";
|
||||||
export * from './result-bar-chart.component';
|
export * from './result-bar-chart.component';
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import { ScanningResultService, ScanningResultDefaultService } from '../service/
|
|||||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||||
import { ErrorHandler } from '../error-handler/index';
|
import { ErrorHandler } from '../error-handler/index';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||||
|
|
||||||
describe('ResultBarChartComponent (inline template)', () => {
|
describe('ResultBarChartComponent (inline template)', () => {
|
||||||
let component: ResultBarChartComponent;
|
let component: ResultBarChartComponent;
|
||||||
@ -20,13 +21,25 @@ describe('ResultBarChartComponent (inline template)', () => {
|
|||||||
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
||||||
};
|
};
|
||||||
let mockData: VulnerabilitySummary = {
|
let mockData: VulnerabilitySummary = {
|
||||||
total_package: 124,
|
scan_status: VULNERABILITY_SCAN_STATUS.finished,
|
||||||
package_with_none: 92,
|
severity: 5,
|
||||||
package_with_high: 10,
|
update_time: new Date(),
|
||||||
package_with_medium: 6,
|
components: {
|
||||||
package_With_low: 13,
|
total: 124,
|
||||||
package_with_unknown: 3,
|
summary: [{
|
||||||
complete_timestamp: new Date()
|
severity: 1,
|
||||||
|
count: 90
|
||||||
|
}, {
|
||||||
|
severity: 3,
|
||||||
|
count: 10
|
||||||
|
}, {
|
||||||
|
severity: 4,
|
||||||
|
count: 10
|
||||||
|
}, {
|
||||||
|
severity: 5,
|
||||||
|
count: 13
|
||||||
|
}]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
@ -115,7 +128,7 @@ describe('ResultBarChartComponent (inline template)', () => {
|
|||||||
|
|
||||||
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');
|
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');
|
||||||
expect(el).not.toBeNull();
|
expect(el).not.toBeNull();
|
||||||
expect(el.style.width).toEqual("74px");
|
expect(el.style.width).toEqual("73px");
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -2,11 +2,14 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
EventEmitter
|
EventEmitter,
|
||||||
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { VulnerabilitySummary } from '../service/index';
|
import { VulnerabilitySummary } from '../service/index';
|
||||||
import { SCANNING_STYLES } from './scanning.css';
|
import { SCANNING_STYLES } from './scanning.css';
|
||||||
import { BAR_CHART_COMPONENT_HTML } from './scanning.html';
|
import { BAR_CHART_COMPONENT_HTML } from './scanning.html';
|
||||||
|
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||||
|
import { VulnerabilitySeverity } from '../service/index';
|
||||||
|
|
||||||
export enum ScanState {
|
export enum ScanState {
|
||||||
COMPLETED, //Scanning work successfully completed
|
COMPLETED, //Scanning work successfully completed
|
||||||
@ -22,18 +25,47 @@ export enum ScanState {
|
|||||||
styles: [SCANNING_STYLES],
|
styles: [SCANNING_STYLES],
|
||||||
template: BAR_CHART_COMPONENT_HTML
|
template: BAR_CHART_COMPONENT_HTML
|
||||||
})
|
})
|
||||||
export class ResultBarChartComponent {
|
export class ResultBarChartComponent implements OnInit {
|
||||||
@Input() tagId: string = "";
|
@Input() tagId: string = "";
|
||||||
@Input() state: ScanState = ScanState.UNKNOWN;
|
@Input() state: ScanState = ScanState.PENDING;
|
||||||
@Input() summary: VulnerabilitySummary = {
|
@Input() summary: VulnerabilitySummary = {
|
||||||
total_package: 0,
|
scan_status: VULNERABILITY_SCAN_STATUS.unknown,
|
||||||
package_with_none: 0,
|
severity: VulnerabilitySeverity.UNKNOWN,
|
||||||
complete_timestamp: new Date()
|
update_time: new Date(),
|
||||||
|
components: {
|
||||||
|
total: 0,
|
||||||
|
summary: []
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@Output() startScanning: EventEmitter<string> = new EventEmitter<string>();
|
@Output() startScanning: EventEmitter<string> = new EventEmitter<string>();
|
||||||
|
scanningInProgress: boolean = false;
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.summary && this.summary.scan_status) {
|
||||||
|
switch (this.summary.scan_status) {
|
||||||
|
case VULNERABILITY_SCAN_STATUS.unknown:
|
||||||
|
this.state = ScanState.UNKNOWN;
|
||||||
|
break;
|
||||||
|
case VULNERABILITY_SCAN_STATUS.error:
|
||||||
|
this.state = ScanState.ERROR;
|
||||||
|
break;
|
||||||
|
case VULNERABILITY_SCAN_STATUS.pending:
|
||||||
|
this.state = ScanState.QUEUED;
|
||||||
|
break;
|
||||||
|
case VULNERABILITY_SCAN_STATUS.stopped:
|
||||||
|
this.state = ScanState.PENDING;
|
||||||
|
break;
|
||||||
|
case VULNERABILITY_SCAN_STATUS.finished:
|
||||||
|
this.state = ScanState.COMPLETED;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public get completed(): boolean {
|
public get completed(): boolean {
|
||||||
return this.state === ScanState.COMPLETED;
|
return this.state === ScanState.COMPLETED;
|
||||||
}
|
}
|
||||||
@ -60,6 +92,7 @@ export class ResultBarChartComponent {
|
|||||||
|
|
||||||
scanNow(): void {
|
scanNow(): void {
|
||||||
if (this.tagId && this.tagId !== '') {
|
if (this.tagId && this.tagId !== '') {
|
||||||
|
this.scanningInProgress = true;
|
||||||
this.startScanning.emit(this.tagId);
|
this.startScanning.emit(this.tagId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Component, OnInit, Input } from '@angular/core';
|
import { Component, OnInit, Input } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ScanningResultService,
|
ScanningResultService,
|
||||||
VulnerabilityItem
|
VulnerabilityItem,
|
||||||
|
VulnerabilitySeverity
|
||||||
} from '../service/index';
|
} from '../service/index';
|
||||||
import { ErrorHandler } from '../error-handler/index';
|
import { ErrorHandler } from '../error-handler/index';
|
||||||
|
|
||||||
@ -16,7 +17,10 @@ import { SCANNING_STYLES } from './scanning.css';
|
|||||||
})
|
})
|
||||||
export class ResultGridComponent implements OnInit {
|
export class ResultGridComponent implements OnInit {
|
||||||
scanningResults: VulnerabilityItem[] = [];
|
scanningResults: VulnerabilityItem[] = [];
|
||||||
|
dataCache: VulnerabilityItem[] = [];
|
||||||
|
|
||||||
@Input() tagId: string;
|
@Input() tagId: string;
|
||||||
|
@Input() repositoryId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private scanningService: ScanningResultService,
|
private scanningService: ScanningResultService,
|
||||||
@ -24,26 +28,48 @@ export class ResultGridComponent implements OnInit {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadResults(this.tagId);
|
this.loadResults(this.repositoryId, this.tagId);
|
||||||
}
|
}
|
||||||
|
|
||||||
showDetail(result: VulnerabilityItem): void {
|
loadResults(repositoryId: string, tagId: string): void {
|
||||||
console.log(result.id);
|
toPromise<VulnerabilityItem[]>(this.scanningService.getVulnerabilityScanningResults(repositoryId, tagId))
|
||||||
}
|
|
||||||
|
|
||||||
loadResults(tagId: string): void {
|
|
||||||
toPromise<VulnerabilityItem[]>(this.scanningService.getVulnerabilityScanningResults(tagId))
|
|
||||||
.then((results: VulnerabilityItem[]) => {
|
.then((results: VulnerabilityItem[]) => {
|
||||||
this.scanningResults = results;
|
this.dataCache = results;
|
||||||
|
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== '');
|
||||||
})
|
})
|
||||||
.catch(error => { this.errorHandler.error(error) })
|
.catch(error => { this.errorHandler.error(error) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Should query from back-end service
|
||||||
filterVulnerabilities(terms: string): void {
|
filterVulnerabilities(terms: string): void {
|
||||||
console.log(terms);
|
if (terms.trim() === '') {
|
||||||
|
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== '');
|
||||||
|
} else {
|
||||||
|
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => this._regexpFilter(terms, item.package));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh(): void {
|
refresh(): void {
|
||||||
this.loadResults(this.tagId);
|
this.loadResults(this.repositoryId, this.tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
severityText(severity: VulnerabilitySeverity): string {
|
||||||
|
switch (severity) {
|
||||||
|
case VulnerabilitySeverity.HIGH:
|
||||||
|
return 'VULNERABILITY.SEVERITY.HIGH';
|
||||||
|
case VulnerabilitySeverity.MEDIUM:
|
||||||
|
return 'VULNERABILITY.SEVERITY.MEDIUM';
|
||||||
|
case VulnerabilitySeverity.LOW:
|
||||||
|
return 'VULNERABILITY.SEVERITY.LOW';
|
||||||
|
case VulnerabilitySeverity.UNKNOWN:
|
||||||
|
return 'VULNERABILITY.SEVERITY.UNKNOWN';
|
||||||
|
default:
|
||||||
|
return 'UNKNOWN';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_regexpFilter(terms: string, testedValue: any): boolean {
|
||||||
|
let reg = new RegExp('.*' + terms + '.*', 'i');
|
||||||
|
return reg.test(testedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import { ResultTipComponent } from './result-tip.component';
|
|||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
|
||||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||||
|
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||||
|
|
||||||
describe('ResultTipComponent (inline template)', () => {
|
describe('ResultTipComponent (inline template)', () => {
|
||||||
let component: ResultTipComponent;
|
let component: ResultTipComponent;
|
||||||
@ -16,14 +17,26 @@ describe('ResultTipComponent (inline template)', () => {
|
|||||||
let testConfig: IServiceConfig = {
|
let testConfig: IServiceConfig = {
|
||||||
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
||||||
};
|
};
|
||||||
let mockData:VulnerabilitySummary = {
|
let mockData: VulnerabilitySummary = {
|
||||||
total_package: 124,
|
scan_status: VULNERABILITY_SCAN_STATUS.finished,
|
||||||
package_with_none: 90,
|
severity: 5,
|
||||||
package_with_high: 13,
|
update_time: new Date(),
|
||||||
package_with_medium: 10,
|
components: {
|
||||||
package_With_low: 10,
|
total: 124,
|
||||||
package_with_unknown: 1,
|
summary: [{
|
||||||
complete_timestamp: new Date()
|
severity: 1,
|
||||||
|
count: 90
|
||||||
|
}, {
|
||||||
|
severity: 3,
|
||||||
|
count: 10
|
||||||
|
}, {
|
||||||
|
severity: 4,
|
||||||
|
count: 10
|
||||||
|
}, {
|
||||||
|
severity: 5,
|
||||||
|
count: 13
|
||||||
|
}]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
@ -4,6 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
|
|
||||||
import { SCANNING_STYLES } from './scanning.css';
|
import { SCANNING_STYLES } from './scanning.css';
|
||||||
import { TIP_COMPONENT_HTML } from './scanning.html';
|
import { TIP_COMPONENT_HTML } from './scanning.html';
|
||||||
|
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||||
|
|
||||||
export const MIN_TIP_WIDTH = 5;
|
export const MIN_TIP_WIDTH = 5;
|
||||||
export const MAX_TIP_WIDTH = 100;
|
export const MAX_TIP_WIDTH = 100;
|
||||||
@ -15,24 +16,62 @@ export const MAX_TIP_WIDTH = 100;
|
|||||||
})
|
})
|
||||||
export class ResultTipComponent implements OnInit {
|
export class ResultTipComponent implements OnInit {
|
||||||
_tipTitle: string = "";
|
_tipTitle: string = "";
|
||||||
|
_highCount: number = 0;
|
||||||
|
_mediumCount: number = 0;
|
||||||
|
_lowCount: number = 0;
|
||||||
|
_unknownCount: number = 0;
|
||||||
|
_noneCount: number = 0;
|
||||||
|
|
||||||
|
totalPackages: number = 0;
|
||||||
|
packagesWithVul: number = 0;
|
||||||
|
|
||||||
@Input() summary: VulnerabilitySummary = {
|
@Input() summary: VulnerabilitySummary = {
|
||||||
total_package: 0,
|
scan_status: VULNERABILITY_SCAN_STATUS.unknown,
|
||||||
package_with_none: 0,
|
severity: VulnerabilitySeverity.UNKNOWN,
|
||||||
complete_timestamp: new Date()
|
update_time: new Date(),
|
||||||
|
components: {
|
||||||
|
total: 0,
|
||||||
|
summary: []
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private translate: TranslateService) { }
|
constructor(private translate: TranslateService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.totalPackages = this.summary && this.summary.components ? this.summary.components.total : 0;
|
||||||
|
if (this.summary && this.summary.components && this.summary.components.summary) {
|
||||||
|
this.summary.components.summary.forEach(item => {
|
||||||
|
this.packagesWithVul += item.count
|
||||||
|
|
||||||
|
switch (item.severity) {
|
||||||
|
case VulnerabilitySeverity.UNKNOWN:
|
||||||
|
this._unknownCount += item.count;
|
||||||
|
break;
|
||||||
|
case VulnerabilitySeverity.NONE:
|
||||||
|
this._noneCount += item.count;
|
||||||
|
break;
|
||||||
|
case VulnerabilitySeverity.LOW:
|
||||||
|
this._lowCount += item.count;
|
||||||
|
break;
|
||||||
|
case VulnerabilitySeverity.MEDIUM:
|
||||||
|
this._mediumCount += item.count;
|
||||||
|
break;
|
||||||
|
case VulnerabilitySeverity.HIGH:
|
||||||
|
this._highCount += item.count;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
this.translate.get('VULNERABILITY.CHART.TOOLTIPS_TITLE',
|
this.translate.get('VULNERABILITY.CHART.TOOLTIPS_TITLE',
|
||||||
{ totalVulnerability: this.totalVulnerabilities, totalPackages: this.summary.total_package })
|
{ totalVulnerability: this.packagesWithVul, totalPackages: this.totalPackages })
|
||||||
.subscribe((res: string) => this._tipTitle = res);
|
.subscribe((res: string) => this._tipTitle = res);
|
||||||
}
|
}
|
||||||
|
|
||||||
tipWidth(severity: VulnerabilitySeverity): string {
|
tipWidth(severity: VulnerabilitySeverity): string {
|
||||||
let n: number = 0;
|
let n: number = 0;
|
||||||
let m: number = this.summary ? this.summary.total_package : 0;
|
let m: number = this.totalPackages;
|
||||||
|
|
||||||
if (m === 0) {
|
if (m === 0) {
|
||||||
return 0 + 'px';
|
return 0 + 'px';
|
||||||
@ -59,8 +98,8 @@ export class ResultTipComponent implements OnInit {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let width: number = Math.round((n/m)*MAX_TIP_WIDTH);
|
let width: number = Math.round((n / m) * MAX_TIP_WIDTH);
|
||||||
if(width < MIN_TIP_WIDTH){
|
if (width < MIN_TIP_WIDTH) {
|
||||||
width = MIN_TIP_WIDTH;
|
width = MIN_TIP_WIDTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,8 +115,8 @@ export class ResultTipComponent implements OnInit {
|
|||||||
return "VULNERABILITY.SINGULAR";
|
return "VULNERABILITY.SINGULAR";
|
||||||
}
|
}
|
||||||
|
|
||||||
public get totalVulnerabilities(): number {
|
public get completeTimestamp(): Date {
|
||||||
return this.summary.total_package - this.summary.package_with_none;
|
return this.summary && this.summary.update_time ? this.summary.update_time : new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get hasHigh(): boolean {
|
public get hasHigh(): boolean {
|
||||||
@ -105,22 +144,22 @@ export class ResultTipComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get highCount(): number {
|
public get highCount(): number {
|
||||||
return this.summary && this.summary.package_with_high ? this.summary.package_with_high : 0;
|
return this._highCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get mediumCount(): number {
|
public get mediumCount(): number {
|
||||||
return this.summary && this.summary.package_with_medium ? this.summary.package_with_medium : 0;
|
return this._mediumCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get lowCount(): number {
|
public get lowCount(): number {
|
||||||
return this.summary && this.summary.package_With_low ? this.summary.package_With_low : 0;
|
return this._lowCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get unknownCount(): number {
|
public get unknownCount(): number {
|
||||||
return this.summary && this.summary.package_with_unknown ? this.summary.package_with_unknown : 0;
|
return this._unknownCount;
|
||||||
}
|
}
|
||||||
public get noneCount(): number {
|
public get noneCount(): number {
|
||||||
return this.summary && this.summary.package_with_none ? this.summary.package_with_none : 0;
|
return this._noneCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get highSuffix(): string {
|
public get highSuffix(): string {
|
||||||
@ -144,6 +183,6 @@ export class ResultTipComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get maxWidth(): string {
|
public get maxWidth(): string {
|
||||||
return MAX_TIP_WIDTH+"px";
|
return (MAX_TIP_WIDTH + 20) + "px";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export const SCANNING_STYLES: string = `
|
export const SCANNING_STYLES: string = `
|
||||||
.bar-wrapper {
|
.bar-wrapper {
|
||||||
width: 150px;
|
width: 120px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@ -17,9 +17,9 @@ export const SCANNING_STYLES: string = `
|
|||||||
}
|
}
|
||||||
.tip-wrapper {
|
.tip-wrapper {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 16px;
|
height: 10px;
|
||||||
max-height: 16px;
|
max-height: 10px;
|
||||||
max-width: 150px;
|
max-width: 120px;
|
||||||
}
|
}
|
||||||
.tip-position {
|
.tip-position {
|
||||||
margin-left: -4px;
|
margin-left: -4px;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
export const TIP_COMPONENT_HTML: string = `
|
export const TIP_COMPONENT_HTML: string = `
|
||||||
<div class="tip-wrapper tip-position" [style.width]='maxWidth'>
|
<div class="tip-wrapper tip-position" [style.width]='maxWidth'>
|
||||||
<clr-tooltip [clrTooltipDirection]="'top-right'" [clrTooltipSize]="'lg'">
|
<clr-tooltip [clrTooltipDirection]="'top-right'" [clrTooltipSize]="'lg'">
|
||||||
<div class="tip-wrapper tip-block bar-block-high" [style.width]='tipWidth(4)'></div>
|
<div class="tip-wrapper tip-block bar-block-high" [style.width]='tipWidth(5)'></div>
|
||||||
<div class="tip-wrapper tip-block bar-block-medium" [style.width]='tipWidth(3)'></div>
|
<div class="tip-wrapper tip-block bar-block-medium" [style.width]='tipWidth(4)'></div>
|
||||||
<div class="tip-wrapper tip-block bar-block-low" [style.width]='tipWidth(2)'></div>
|
<div class="tip-wrapper tip-block bar-block-low" [style.width]='tipWidth(3)'></div>
|
||||||
<div class="tip-wrapper tip-block bar-block-unknown" [style.width]='tipWidth(1)'></div>
|
<div class="tip-wrapper tip-block bar-block-unknown" [style.width]='tipWidth(2)'></div>
|
||||||
<div class="tip-wrapper tip-block bar-block-none" [style.width]='tipWidth(0)'></div>
|
<div class="tip-wrapper tip-block bar-block-none" [style.width]='tipWidth(1)'></div>
|
||||||
<clr-tooltip-content>
|
<clr-tooltip-content>
|
||||||
<div>
|
<div>
|
||||||
<span class="bar-tooltip-font bar-tooltip-font-title">{{tipTitle}}</span>
|
<span class="bar-tooltip-font bar-tooltip-font-title">{{tipTitle}}</span>
|
||||||
@ -34,7 +34,7 @@ export const TIP_COMPONENT_HTML: string = `
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
|
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
|
||||||
<span>{{summary.complete_timestamp | date}}</span>
|
<span>{{completeTimestamp | date}}</span>
|
||||||
</div>
|
</div>
|
||||||
</clr-tooltip-content>
|
</clr-tooltip-content>
|
||||||
</clr-tooltip>
|
</clr-tooltip>
|
||||||
@ -58,21 +58,17 @@ export const GRID_COMPONENT_HTML: string = `
|
|||||||
<clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | 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]="'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]="'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-placeholder>{{'VULNERABILITY.GRID.PLACEHOLDER' | translate}}</clr-dg-placeholder>
|
||||||
<clr-dg-row *clrDgItems="let res of scanningResults">
|
<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.id}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{res.severity}}</clr-dg-cell>
|
<clr-dg-cell>{{severityText(res.severity) | translate}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{res.package}}</clr-dg-cell>
|
<clr-dg-cell>{{res.package}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{res.version}}</clr-dg-cell>
|
<clr-dg-cell>{{res.version}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{res.fixedVersion}}</clr-dg-cell>
|
<clr-dg-cell>{{res.fixedVersion}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{res.layer}}</clr-dg-cell>
|
<clr-dg-row-detail *clrIfExpanded>
|
||||||
<clr-dg-cell>{{res.description}}</clr-dg-cell>
|
{{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}}
|
||||||
|
</clr-dg-row-detail>
|
||||||
</clr-dg-row>
|
</clr-dg-row>
|
||||||
|
|
||||||
<clr-dg-footer>
|
<clr-dg-footer>
|
||||||
@ -87,7 +83,7 @@ export const GRID_COMPONENT_HTML: string = `
|
|||||||
export const BAR_CHART_COMPONENT_HTML: string = `
|
export const BAR_CHART_COMPONENT_HTML: string = `
|
||||||
<div class="bar-wrapper">
|
<div class="bar-wrapper">
|
||||||
<div *ngIf="pending" class="bar-state">
|
<div *ngIf="pending" class="bar-state">
|
||||||
<button class="btn btn-link scanning-button" (click)="scanNow()">{{'VULNERABILITY.STATE.PENDING' | translate}}</button>
|
<button class="btn btn-link scanning-button" (click)="scanNow()" [disabled]="scanningInProgress">{{'VULNERABILITY.STATE.PENDING' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="queued" class="bar-state">
|
<div *ngIf="queued" class="bar-state">
|
||||||
<span>{{'VULNERABILITY.STATE.QUEUED' | translate}}</span>
|
<span>{{'VULNERABILITY.STATE.QUEUED' | translate}}</span>
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
"clarity-icons": "^0.9.7",
|
"clarity-icons": "^0.9.7",
|
||||||
"clarity-ui": "^0.9.7",
|
"clarity-ui": "^0.9.7",
|
||||||
"core-js": "^2.4.1",
|
"core-js": "^2.4.1",
|
||||||
"harbor-ui": "^0.1.85",
|
"harbor-ui": "^0.1.99",
|
||||||
"intl": "^1.2.5",
|
"intl": "^1.2.5",
|
||||||
"mutationobserver-shim": "^0.3.2",
|
"mutationobserver-shim": "^0.3.2",
|
||||||
"ngx-clipboard": "^8.0.2",
|
"ngx-clipboard": "^8.0.2",
|
||||||
|
@ -459,7 +459,7 @@
|
|||||||
"COLUMN_PACKAGE": "Package",
|
"COLUMN_PACKAGE": "Package",
|
||||||
"COLUMN_VERSION": "Current version",
|
"COLUMN_VERSION": "Current version",
|
||||||
"COLUMN_FIXED": "Fixed in version",
|
"COLUMN_FIXED": "Fixed in version",
|
||||||
"COLUMN_LAYER": "Introduced in layer",
|
"COLUMN_DESCRIPTION": "Description",
|
||||||
"FOOT_ITEMS": "Items",
|
"FOOT_ITEMS": "Items",
|
||||||
"FOOT_OF": "of"
|
"FOOT_OF": "of"
|
||||||
},
|
},
|
||||||
@ -488,7 +488,9 @@
|
|||||||
"TAG": {
|
"TAG": {
|
||||||
"CREATION_TIME_PREFIX": "Create on",
|
"CREATION_TIME_PREFIX": "Create on",
|
||||||
"CREATOR_PREFIX": "by",
|
"CREATOR_PREFIX": "by",
|
||||||
|
"ANONYMITY": "anonymity",
|
||||||
"IMAGE_DETAILS": "Image Details",
|
"IMAGE_DETAILS": "Image Details",
|
||||||
|
"DOCKER_VERSION": "Docker Version",
|
||||||
"ARCHITECTURE": "Architecture",
|
"ARCHITECTURE": "Architecture",
|
||||||
"OS": "OS",
|
"OS": "OS",
|
||||||
"SCAN_COMPLETION_TIME": "Scan Completed",
|
"SCAN_COMPLETION_TIME": "Scan Completed",
|
||||||
|
@ -458,7 +458,7 @@
|
|||||||
"COLUMN_PACKAGE": "Package",
|
"COLUMN_PACKAGE": "Package",
|
||||||
"COLUMN_VERSION": "Current version",
|
"COLUMN_VERSION": "Current version",
|
||||||
"COLUMN_FIXED": "Fixed in version",
|
"COLUMN_FIXED": "Fixed in version",
|
||||||
"COLUMN_LAYER": "Introduced in layer",
|
"COLUMN_DESCRIPTION": "Description",
|
||||||
"FOOT_ITEMS": "Items",
|
"FOOT_ITEMS": "Items",
|
||||||
"FOOT_OF": "of"
|
"FOOT_OF": "of"
|
||||||
},
|
},
|
||||||
@ -487,7 +487,9 @@
|
|||||||
"TAG": {
|
"TAG": {
|
||||||
"CREATION_TIME_PREFIX": "Create on",
|
"CREATION_TIME_PREFIX": "Create on",
|
||||||
"CREATOR_PREFIX": "by",
|
"CREATOR_PREFIX": "by",
|
||||||
|
"ANONYMITY": "anonymity",
|
||||||
"IMAGE_DETAILS": "Image Details",
|
"IMAGE_DETAILS": "Image Details",
|
||||||
|
"DOCKER_VERSION": "Docker Version",
|
||||||
"ARCHITECTURE": "Architecture",
|
"ARCHITECTURE": "Architecture",
|
||||||
"OS": "OS",
|
"OS": "OS",
|
||||||
"SCAN_COMPLETION_TIME": "Scan Completed",
|
"SCAN_COMPLETION_TIME": "Scan Completed",
|
||||||
|
@ -459,7 +459,7 @@
|
|||||||
"COLUMN_PACKAGE": "组件",
|
"COLUMN_PACKAGE": "组件",
|
||||||
"COLUMN_VERSION": "当前版本",
|
"COLUMN_VERSION": "当前版本",
|
||||||
"COLUMN_FIXED": "修复版本",
|
"COLUMN_FIXED": "修复版本",
|
||||||
"COLUMN_LAYER": "引入层",
|
"COLUMN_DESCRIPTION": "简介",
|
||||||
"FOOT_ITEMS": "项目",
|
"FOOT_ITEMS": "项目",
|
||||||
"FOOT_OF": "总共"
|
"FOOT_OF": "总共"
|
||||||
},
|
},
|
||||||
@ -488,7 +488,9 @@
|
|||||||
"TAG": {
|
"TAG": {
|
||||||
"CREATION_TIME_PREFIX": "创建时间:",
|
"CREATION_TIME_PREFIX": "创建时间:",
|
||||||
"CREATOR_PREFIX": "创建者:",
|
"CREATOR_PREFIX": "创建者:",
|
||||||
|
"ANONYMITY": "匿名用户",
|
||||||
"IMAGE_DETAILS": "镜像详情",
|
"IMAGE_DETAILS": "镜像详情",
|
||||||
|
"DOCKER_VERSION": "Docker版本",
|
||||||
"ARCHITECTURE": "架构",
|
"ARCHITECTURE": "架构",
|
||||||
"OS": "操作系统",
|
"OS": "操作系统",
|
||||||
"SCAN_COMPLETION_TIME": "扫描完成时间",
|
"SCAN_COMPLETION_TIME": "扫描完成时间",
|
||||||
|
Loading…
Reference in New Issue
Block a user