Commit c2e30b4b authored by AllForNothing's avatar AllForNothing Committed by sshijun
Browse files

Add scanner UI


Signed-off-by: default avatarAllForNothing <sshijun@vmware.com>
parent 0076f231
......@@ -15,6 +15,7 @@ src/common/dao/dao.test
jobservice/test
src/portal/coverage/
src/portal/lib/coverage/
src/portal/dist/
src/portal/html-report/
src/portal/node_modules/
......
......@@ -4,6 +4,7 @@ export * from "./service/index";
export * from "./error-handler/index";
export * from "./shared/shared.const";
export * from "./shared/shared.utils";
export * from "./shared/shared.module";
export * from "./utils";
export * from "./log/index";
export * from "./filter/index";
......
......@@ -30,6 +30,7 @@ import { UserPermissionDefaultService, UserPermissionService } from "../service/
import { USERSTATICPERMISSION } from "../service/permission-static";
import { of } from "rxjs";
import { delay } from 'rxjs/operators';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
class RouterStub {
......@@ -161,7 +162,8 @@ describe('RepositoryComponent (inline template)', () => {
TestBed.configureTestingModule({
imports: [
SharedModule,
RouterTestingModule
RouterTestingModule,
BrowserAnimationsModule
],
declarations: [
RepositoryComponent,
......
......@@ -64,7 +64,7 @@ export interface Tag extends Base {
author: string;
created: Date;
signature?: string;
scan_overview?: VulnerabilitySummary;
scan_overview?: ScanOverview;
labels: Label[];
push_time?: string;
pull_time?: string;
......@@ -290,25 +290,43 @@ export enum VulnerabilitySeverity {
export interface VulnerabilityBase {
id: string;
severity: VulnerabilitySeverity;
severity: string;
package: string;
version: string;
}
export interface VulnerabilityItem extends VulnerabilityBase {
link: string;
fixedVersion: string;
links: string[];
fix_version: string;
layer?: string;
description: string;
}
export interface VulnerabilitySummary {
image_digest?: string;
scan_status: string;
job_id?: number;
severity: VulnerabilitySeverity;
components: VulnerabilityComponents;
update_time: Date; // Use as complete timestamp
report_id?: string;
mime_type?: string;
scan_status?: string;
severity?: string;
duration?: number;
summary?: SeveritySummary;
start_time?: Date;
end_time?: Date;
}
export interface SeveritySummary {
total: number;
summary: {[key: string]: number};
}
export interface VulnerabilityDetail {
"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"?: VulnerabilityReport;
}
export interface VulnerabilityReport {
vulnerabilities?: VulnerabilityItem[];
}
export interface ScanOverview {
"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"?: VulnerabilitySummary;
}
export interface VulnerabilityComponents {
......
import { HttpClient } from "@angular/common/http";
import { HttpClient, HttpHeaders } from "@angular/common/http";
import { Injectable, Inject } from "@angular/core";
import { SERVICE_CONFIG, IServiceConfig } from "../service.config";
import { buildHttpRequestOptions, HTTP_JSON_OPTIONS } from "../utils";
import { buildHttpRequestOptions, DEFAULT_SUPPORTED_MIME_TYPE, HTTP_JSON_OPTIONS } from "../utils";
import { RequestQueryParams } from "./RequestQueryParams";
import { VulnerabilityItem, VulnerabilitySummary } from "./interface";
import { VulnerabilityDetail, VulnerabilitySummary } from "./interface";
import { map, catchError } from "rxjs/operators";
import { Observable, of, throwError as observableThrowError } from "rxjs";
/**
* Get the vulnerabilities scanning results for the specified tag.
*
......@@ -46,7 +47,7 @@ export abstract class ScanningResultService {
tagId: string,
queryParams?: RequestQueryParams
):
| Observable<VulnerabilityItem[]>;
| Observable<any>;
/**
* Start a new vulnerability scanning
......@@ -106,17 +107,22 @@ export class ScanningResultDefaultService extends ScanningResultService {
tagId: string,
queryParams?: RequestQueryParams
):
| Observable<VulnerabilityItem[]> {
| Observable<any> {
if (!repoName || repoName.trim() === "" || !tagId || tagId.trim() === "") {
return observableThrowError("Bad argument");
}
let httpOptions = buildHttpRequestOptions(queryParams);
let requestHeaders = httpOptions.headers as HttpHeaders;
// Change the accept header to the supported report mime types
httpOptions.headers = requestHeaders.set("Accept", DEFAULT_SUPPORTED_MIME_TYPE);
return this.http
.get(
`${this._baseUrl}/${repoName}/tags/${tagId}/vulnerability/details`,
buildHttpRequestOptions(queryParams)
`${this._baseUrl}/${repoName}/tags/${tagId}/scan`,
httpOptions
)
.pipe(map(response => response as VulnerabilityItem[])
.pipe(map(response => response as VulnerabilityDetail)
, catchError(error => observableThrowError(error)));
}
......
......@@ -32,45 +32,18 @@
<label class="detail-label">{{'TAG.DOCKER_VERSION' | translate }}</label>
<div class="image-details" [title]="tagDetails.docker_version">{{tagDetails.docker_version}}</div>
</section>
<section class="detail-row">
<section class="detail-row" *ngIf="hasCve">
<label class="detail-label">{{'TAG.SCAN_COMPLETION_TIME' | translate }}</label>
<div class="image-details" [title]="scanCompletedDatetime | date">{{scanCompletedDatetime | date}}</div>
</section>
</section>
</div>
</div>
<div *ngIf="withClair" class="col-md-4 col-sm-6">
<div class="vulnerability">
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="tagDetails.scan_overview"></hbr-vulnerability-bar>
</div>
<div class="flex-block vulnerabilities-info">
<div class="second-column">
<div class="row-flex">
<div class="icon-position">
<clr-icon shape="error" size="24" class="is-error"></clr-icon>
</div>
<span class="detail-count">{{highCount}}</span> {{packageText(highCount) | translate}} {{haveText(highCount) | translate}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{suffixForHigh | translate }}
</div>
<div class="second-row row-flex">
<div class="icon-position">
<clr-icon shape="exclamation-triangle" size="24" class="tip-icon-medium"></clr-icon>
</div>
<span class="detail-count">{{mediumCount}}</span> {{packageText(mediumCount) | translate}} {{haveText(mediumCount) | translate}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{suffixForMedium | translate }}
</div>
<div class="second-row row-flex">
<div class="icon-position">
<clr-icon shape="play" size="22" class="tip-icon-low rotate-90"></clr-icon>
</div>
<span class="detail-count">{{lowCount}}</span> {{packageText(lowCount) | translate}} {{haveText(lowCount) | translate}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{suffixForLow | translate }}
</div>
<div class="second-row row-flex">
<div class="icon-position">
<clr-icon shape="help" size="20"></clr-icon>
</div>
<span class="detail-count">{{unknownCount}}</span> {{packageText(unknownCount) | translate}} {{haveText(unknownCount) | translate}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{suffixForUnknown | translate }}
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="vulnerability" [hidden]="hasCve">
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="vulnerabilitySummary"></hbr-vulnerability-bar>
</div>
<histogram-chart *ngIf="hasCve" class="margin-top-5px" [metadata]="passMetadataToChart()" [isWhiteBackground]="true"></histogram-chart>
</div>
<div *ngIf="!withAdmiral && tagDetails?.labels?.length">
<div class="third-column detail-title">{{'TAG.LABELS' | translate }}</div>
......@@ -83,7 +56,7 @@
</div>
</section>
<clr-tabs>
<clr-tab *ngIf="hasVulnerabilitiesListPermission && withClair">
<clr-tab *ngIf="hasVulnerabilitiesListPermission">
<button clrTabLink [clrTabLinkInOverflow]="false" class="btn btn-link nav-link" id="tag-vulnerability" [class.active]='isCurrentTabLink("tag-vulnerability")' type="button" (click)='tabLinkClick("tag-vulnerability")'>{{'REPOSITORY.VULNERABILITY' | translate}}</button>
<clr-tab-content id="content1" *clrIfActive="true">
<hbr-vulnerabilities-grid [repositoryId]="repositoryId" [projectId]="projectId" [tagId]="tagId"></hbr-vulnerabilities-grid>
......@@ -96,4 +69,4 @@
</clr-tab-content>
</clr-tab>
</clr-tabs>
</div>
\ No newline at end of file
</div>
......@@ -75,8 +75,6 @@ $size24:24px;
margin-left: 36px;
}
.vulnerability{
margin-left: 50px;
margin-top: -12px;
margin-bottom: 20px;}
.vulnerabilities-info {
......@@ -151,6 +149,8 @@ $size24:24px;
.tip-icon-low{
color:yellow;
}
.margin-top-5px {
margin-top: 5px;
}
......@@ -21,7 +21,7 @@ import {
ScanningResultDefaultService
} from "../service/index";
import { FilterComponent } from "../filter/index";
import { VULNERABILITY_SCAN_STATUS } from "../utils";
import { VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../utils";
import { VULNERABILITY_DIRECTIVES } from "../vulnerability-scanning/index";
import { LabelPieceComponent } from "../label-piece/label-piece.component";
import { ChannelService } from "../channel/channel.service";
......@@ -43,29 +43,15 @@ describe("TagDetailComponent (inline template)", () => {
let vulSpy: jasmine.Spy;
let manifestSpy: jasmine.Spy;
let mockVulnerability: VulnerabilitySummary = {
scan_status: VULNERABILITY_SCAN_STATUS.finished,
severity: 5,
update_time: new Date(),
components: {
scan_status: VULNERABILITY_SCAN_STATUS.SUCCESS,
severity: "High",
end_time: new Date(),
summary: {
total: 124,
summary: [
{
severity: 1,
count: 90
},
{
severity: 3,
count: 10
},
{
severity: 4,
count: 10
},
{
severity: 5,
count: 13
}
]
summary: {
"High": 5,
"Low": 5
}
}
};
let mockTag: Tag = {
......@@ -80,7 +66,9 @@ describe("TagDetailComponent (inline template)", () => {
author: "steven",
created: new Date("2016-11-08T22:41:15.912313785Z"),
signature: null,
scan_overview: mockVulnerability,
scan_overview: {
"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0": mockVulnerability
},
labels: []
};
......@@ -141,13 +129,13 @@ describe("TagDetailComponent (inline template)", () => {
id: "CVE-2016-" + (8859 + i),
severity:
i % 2 === 0
? VulnerabilitySeverity.HIGH
: VulnerabilitySeverity.MEDIUM,
? VULNERABILITY_SEVERITY.HIGH
: VULNERABILITY_SEVERITY.MEDIUM,
package: "package_" + i,
link: "https://security-tracker.debian.org/tracker/CVE-2016-4484",
links: ["https://security-tracker.debian.org/tracker/CVE-2016-4484"],
layer: "layer_" + i,
version: "4." + i + ".0",
fixedVersion: "4." + i + ".11",
fix_version: "4." + i + ".11",
description: "Mock data"
};
mockData.push(res);
......
import { Component, Input, Output, EventEmitter, OnInit } from "@angular/core";
import { TagService, Tag, VulnerabilitySeverity } from "../service/index";
import { TagService, Tag, VulnerabilitySeverity, VulnerabilitySummary } from "../service/index";
import { ErrorHandler } from "../error-handler/index";
import { Label } from "../service/interface";
import { forkJoin } from "rxjs";
import { UserPermissionService } from "../service/permission.service";
import { USERSTATICPERMISSION } from "../service/permission-static";
import { ChannelService } from "../channel/channel.service";
import { DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../utils";
const TabLinkContentMap: { [index: string]: string } = {
"tag-history": "history",
......@@ -26,7 +27,7 @@ export class TagDetailComponent implements OnInit {
_lowCount: number = 0;
_unknownCount: number = 0;
labels: Label;
vulnerabilitySummary: VulnerabilitySummary;
@Input()
tagId: string;
@Input()
......@@ -73,35 +74,15 @@ export class TagDetailComponent implements OnInit {
}
this.getTagPermissions(this.projectId);
this.channel.tagDetail$.subscribe(tag => {
this.getTagDetails(tag);
this.getTagDetails(tag);
});
}
getTagDetails(tagDetails): void {
getTagDetails(tagDetails: Tag): void {
this.tagDetails = tagDetails;
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;
}
});
if (tagDetails
&& tagDetails.scan_overview
&& tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]) {
this.vulnerabilitySummary = tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE];
}
}
onBack(): void {
......@@ -127,26 +108,58 @@ export class TagDetailComponent implements OnInit {
? this.tagDetails.author
: "TAG.ANONYMITY";
}
public get highCount(): number {
return this._highCount;
private getCountByLevel(level: string): number {
if (this.vulnerabilitySummary && this.vulnerabilitySummary.summary
&& this.vulnerabilitySummary.summary.summary) {
return this.vulnerabilitySummary.summary.summary[level];
}
return 0;
}
public get mediumCount(): number {
return this._mediumCount;
/**
* count of critical level vulnerabilities
*/
get criticalCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.CRITICAL);
}
public get lowCount(): number {
return this._lowCount;
/**
* count of high level vulnerabilities
*/
get highCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.HIGH);
}
public get unknownCount(): number {
return this._unknownCount;
/**
* count of medium level vulnerabilities
*/
get mediumCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.MEDIUM);
}
/**
* count of low level vulnerabilities
*/
get lowCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.LOW);
}
/**
* count of unknown vulnerabilities
*/
get unknownCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.UNKNOWN);
}
/**
* count of negligible vulnerabilities
*/
get negligibleCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.NEGLIGIBLE);
}
get hasCve(): boolean {
return this.vulnerabilitySummary
&& this.vulnerabilitySummary.scan_status === VULNERABILITY_SCAN_STATUS.SUCCESS;
}
public get scanCompletedDatetime(): Date {
return this.tagDetails && this.tagDetails.scan_overview
? this.tagDetails.scan_overview.update_time
&& this.tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]
? this.tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE].end_time
: null;
}
......@@ -208,4 +221,38 @@ export class TagDetailComponent implements OnInit {
error => this.errorHandler.error(error)
);
}
passMetadataToChart() {
return [
{
text: 'VULNERABILITY.SEVERITY.CRITICAL',
value: this.criticalCount ? this.criticalCount : 0,
color: 'red'
},
{
text: 'VULNERABILITY.SEVERITY.HIGH',
value: this.highCount ? this.highCount : 0,
color: '#e64524'
},
{
text: 'VULNERABILITY.SEVERITY.MEDIUM',
value: this.mediumCount ? this.mediumCount : 0,
color: 'orange'
},
{
text: 'VULNERABILITY.SEVERITY.LOW',
value: this.lowCount ? this.lowCount : 0,
color: '#007CBB'
},
{
text: 'VULNERABILITY.SEVERITY.NEGLIGIBLE',
value: this.negligibleCount ? this.negligibleCount : 0,
color: 'green'
},
{
text: 'VULNERABILITY.SEVERITY.UNKNOWN',
value: this.unknownCount ? this.unknownCount : 0,
color: 'grey'
},
];
}
}
......@@ -55,23 +55,23 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" class="datagrid-top" [class.embeded-datagrid]="isEmbedded" [(clrDgSelected)]="selectedRow">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(canScanNow(selectedRow) && selectedRow.length==1)" (click)="scanNow(selectedRow)"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button [clrLoading]="scanBtnState" type="button" class="btn btn-sm btn-secondary" [disabled]="!(canScanNow(selectedRow) && selectedRow.length==1 && hasEnabledScanner)" (click)="scanNow(selectedRow)"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="showDigestId(selectedRow)"><clr-icon shape="copy" size="16"></clr-icon>&nbsp;{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<clr-dropdown *ngIf="!withAdmiral">
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger [disabled]="!(selectedRow.length==1)||!hasAddLabelImagePermission" (click)="addLabels(selectedRow)"><clr-icon shape="plus" size="16"></clr-icon>{{'REPOSITORY.ADD_LABELS' | translate}}</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<clr-dropdown>
<div class="filter-grid">
<label class="dropdown-header">{{'REPOSITORY.ADD_LABEL_TO_IMAGE' | translate}}</label>
<div class="form-group"><input clrInput type="text" placeholder="Filter labels" [(ngModel)]="stickName" (keyup)="handleStickInputFilter()"></div>
<div [hidden]='imageStickLabels.length' class="no-labels">{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' class="has-label">
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' [hidden]='!label.show' (click)="stickLabel(label)">
<div class="filter-grid">
<label class="dropdown-header">{{'REPOSITORY.ADD_LABEL_TO_IMAGE' | translate}}</label>
<div class="form-group"><input clrInput type="text" placeholder="Filter labels" [(ngModel)]="stickName" (keyup)="handleStickInputFilter()"></div>
<div [hidden]='imageStickLabels.length' class="no-labels">{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' class="has-label">
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' [hidden]='!label.show' (click)="stickLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label" [labelWidth]="130"></hbr-label-piece></div>
</button>
</div>
</div>
</div>
</clr-dropdown>
</clr-dropdown-menu>
</clr-dropdown>
......@@ -81,7 +81,7 @@
<clr-dg-column class="flex-max-width" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withClair">{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
......@@ -98,8 +98,8 @@
<clr-dg-cell class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
<hbr-copy-input #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"></hbr-copy-input>
</clr-dg-cell>
<clr-dg-cell *ngIf="withClair">
<hbr-vulnerability-bar [tagStatus]="t.scan_overview?.scan_status" [repoName]="repoName" [tagId]="t.name" [summary]="t.scan_overview"></hbr-vulnerability-bar>
<clr-dg-cell>
<hbr-vulnerability-bar [repoName]="repoName" [tagId]="t.name" [summary]="handleScanOverview(t.scan_overview)"></hbr-vulnerability-bar>
</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signature !== null">
<clr-icon shape="check-circle" *ngSwitchCase="true" size="20" class="color-green"></clr-icon>
......
......@@ -21,7 +21,6 @@
.embeded-datagrid {
width: 98%;
float: right;
/*add for issue #2688*/
}
.hidden-tag {
......@@ -249,4 +248,4 @@ clr-datagrid {
::ng-deep .clr-form-control {
margin-top: 0;
}
\ No newline at end of file
}
import { ComponentFixture, TestBed, async } from "@angular/core/testing";
import { DebugElement } from "@angular/core";
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from "@angular/core";
import { SharedModule } from "../shared/shared.module";
import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component";
......@@ -23,8 +23,11 @@ import { LabelDefaultService, LabelService } from "../service/label.service";
import { UserPermissionService, UserPermissionDefaultService } from "../service/permission.service";
import { USERSTATICPERMISSION } from "../service/permission-static";
import { OperationService } from "../operation/operation.service";
import { Observable, of } from "rxjs";
import { of } from "rxjs";
import { delay } from "rxjs/operators";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { HttpClientTestingModule } from "@angular/common/http/testing";
import { HttpClient } from "@angular/common/http";
describe("TagComponent (inline template)", () => {
......@@ -35,7 +38,11 @@ describe("TagComponent (inline template)", () => {
let spy: jasmine.Spy;
let spyLabels: jasmine.Spy;
let spyLabels1: jasmine.Spy;
let spyScanner: jasmine.Spy;
let scannerMock = {
disabled: false,
name: "Clair"
};
let mockTags: Tag[] = [
{
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
......@@ -108,7 +115,12 @@ describe("TagComponent (inline template)", () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
SharedModule,