mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-21 23:21:26 +01:00
Add scanner UI
Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
parent
0076f23195
commit
c2e30b4bad
1
.gitignore
vendored
1
.gitignore
vendored
@ -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 {
|
||||
|
@ -1,13 +1,14 @@
|
||||
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>
|
||||
|
@ -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);
|
||||
|
@ -1,12 +1,13 @@
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* count of critical level vulnerabilities
|
||||
*/
|
||||
get criticalCount(): number {
|
||||
return this.getCountByLevel(VULNERABILITY_SEVERITY.CRITICAL);
|
||||
}
|
||||
|
||||
public get mediumCount(): number {
|
||||
return this._mediumCount;
|
||||
/**
|
||||
* count of high level vulnerabilities
|
||||
*/
|
||||
get highCount(): number {
|
||||
return this.getCountByLevel(VULNERABILITY_SEVERITY.HIGH);
|
||||
}
|
||||
|
||||
public get lowCount(): number {
|
||||
return this._lowCount;
|
||||
/**
|
||||
* count of medium level vulnerabilities
|
||||
*/
|
||||
get mediumCount(): number {
|
||||
return this.getCountByLevel(VULNERABILITY_SEVERITY.MEDIUM);
|
||||
}
|
||||
|
||||
public get unknownCount(): number {
|
||||
return this._unknownCount;
|
||||
/**
|
||||
* 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> {{'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> {{'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> {{'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 {
|
||||
|
@ -1,5 +1,5 @@
|
||||
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,
|
||||
BrowserAnimationsModule,
|
||||
HttpClientTestingModule
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
],
|
||||
declarations: [
|
||||
TagComponent,
|
||||
@ -129,9 +141,9 @@ describe("TagComponent (inline template)", () => {
|
||||
{ provide: ScanningResultService, useClass: ScanningResultDefaultService },
|
||||
{ provide: LabelService, useClass: LabelDefaultService },
|
||||
{ provide: UserPermissionService, useClass: UserPermissionDefaultService },
|
||||
{ provide: OperationService }
|
||||
{ provide: OperationService },
|
||||
]
|
||||
});
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
@ -154,7 +166,9 @@ describe("TagComponent (inline template)", () => {
|
||||
tagService = fixture.debugElement.injector.get(TagService);
|
||||
spy = spyOn(tagService, "getTags").and.returnValues(of(mockTags).pipe(delay(0)));
|
||||
userPermissionService = fixture.debugElement.injector.get(UserPermissionService);
|
||||
|
||||
let http: HttpClient;
|
||||
http = fixture.debugElement.injector.get(HttpClient);
|
||||
spyScanner = spyOn(http, "get").and.returnValue(of(scannerMock));
|
||||
spyOn(userPermissionService, "getPermission")
|
||||
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE )
|
||||
.and.returnValue(of(mockHasAddLabelImagePermission))
|
||||
@ -176,6 +190,10 @@ describe("TagComponent (inline template)", () => {
|
||||
expect(spy.calls.any).toBeTruthy();
|
||||
}));
|
||||
|
||||
it("should load project scanner", async(() => {
|
||||
expect(spyScanner.calls.count()).toEqual(1);
|
||||
}));
|
||||
|
||||
it("should load and render data", () => {
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
|
@ -12,43 +12,38 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
AfterViewInit,
|
||||
ChangeDetectorRef,
|
||||
ElementRef, AfterViewInit
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild
|
||||
} from "@angular/core";
|
||||
import { Subject, forkJoin } from "rxjs";
|
||||
import { debounceTime, distinctUntilChanged, finalize } from 'rxjs/operators';
|
||||
import { forkJoin, Observable, Subject, throwError as observableThrowError } from "rxjs";
|
||||
import { catchError, debounceTime, distinctUntilChanged, finalize, map } from 'rxjs/operators';
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { State, Comparator } from "../service/interface";
|
||||
import { Comparator, Label, State, Tag, TagClickEvent } from "../service/interface";
|
||||
|
||||
import { TagService, RetagService, VulnerabilitySeverity, RequestQueryParams } from "../service/index";
|
||||
import { RequestQueryParams, RetagService, TagService, VulnerabilitySeverity } from "../service/index";
|
||||
import { ErrorHandler } from "../error-handler/error-handler";
|
||||
import { ChannelService } from "../channel/index";
|
||||
import {
|
||||
ConfirmationTargets,
|
||||
ConfirmationState,
|
||||
ConfirmationButtons
|
||||
} from "../shared/shared.const";
|
||||
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../shared/shared.const";
|
||||
|
||||
import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component";
|
||||
import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message";
|
||||
import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message";
|
||||
|
||||
import { Label, Tag, TagClickEvent, RetagRequest } from "../service/interface";
|
||||
|
||||
import {
|
||||
CustomComparator,
|
||||
calculatePage,
|
||||
clone,
|
||||
CustomComparator,
|
||||
DEFAULT_PAGE_SIZE, DEFAULT_SUPPORTED_MIME_TYPE,
|
||||
doFiltering,
|
||||
doSorting,
|
||||
VULNERABILITY_SCAN_STATUS,
|
||||
DEFAULT_PAGE_SIZE,
|
||||
clone,
|
||||
} from "../utils";
|
||||
|
||||
import { CopyInputComponent } from "../push-image/copy-input.component";
|
||||
@ -58,9 +53,10 @@ import { USERSTATICPERMISSION } from "../service/permission-static";
|
||||
import { operateChanges, OperateInfo, OperationState } from "../operation/operate";
|
||||
import { OperationService } from "../operation/operation.service";
|
||||
import { ImageNameInputComponent } from "../image-name-input/image-name-input.component";
|
||||
import { map, catchError } from "rxjs/operators";
|
||||
import { errorHandler as errorHandFn } from "../shared/shared.utils";
|
||||
import { Observable, throwError as observableThrowError } from "rxjs";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ClrLoadingState } from "@clr/angular";
|
||||
|
||||
export interface LabelState {
|
||||
iconsShow: boolean;
|
||||
label: Label;
|
||||
@ -152,6 +148,8 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
hasRetagImagePermission: boolean;
|
||||
hasDeleteImagePermission: boolean;
|
||||
hasScanImagePermission: boolean;
|
||||
hasEnabledScanner: boolean;
|
||||
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private tagService: TagService,
|
||||
@ -161,7 +159,8 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
private translateService: TranslateService,
|
||||
private ref: ChangeDetectorRef,
|
||||
private operationService: OperationService,
|
||||
private channel: ChannelService
|
||||
private channel: ChannelService,
|
||||
private http: HttpClient
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
@ -169,6 +168,7 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
this.errorHandler.error("Project ID cannot be unset.");
|
||||
return;
|
||||
}
|
||||
this.getProjectScanner();
|
||||
if (!this.repoName) {
|
||||
this.errorHandler.error("Repo name cannot be unset.");
|
||||
return;
|
||||
@ -529,17 +529,6 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
.subscribe(items => {
|
||||
// To keep easy use for vulnerability bar
|
||||
items.forEach((t: Tag) => {
|
||||
if (!t.scan_overview) {
|
||||
t.scan_overview = {
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.stopped,
|
||||
severity: VulnerabilitySeverity.UNKNOWN,
|
||||
update_time: new Date(),
|
||||
components: {
|
||||
total: 0,
|
||||
summary: []
|
||||
}
|
||||
};
|
||||
}
|
||||
if (t.signature !== null) {
|
||||
signatures.push(t.name);
|
||||
}
|
||||
@ -722,28 +711,21 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
|
||||
// Get vulnerability scanning status
|
||||
scanStatus(t: Tag): string {
|
||||
if (t && t.scan_overview && t.scan_overview.scan_status) {
|
||||
return t.scan_overview.scan_status;
|
||||
if (t) {
|
||||
let so = this.handleScanOverview(t.scan_overview);
|
||||
if (so && so.scan_status) {
|
||||
return so.scan_status;
|
||||
}
|
||||
}
|
||||
|
||||
return VULNERABILITY_SCAN_STATUS.unknown;
|
||||
return VULNERABILITY_SCAN_STATUS.NOT_SCANNED;
|
||||
}
|
||||
|
||||
existObservablePackage(t: Tag): boolean {
|
||||
return t.scan_overview &&
|
||||
t.scan_overview.components &&
|
||||
t.scan_overview.components.total &&
|
||||
t.scan_overview.components.total > 0 ? true : false;
|
||||
}
|
||||
|
||||
// Whether show the 'scan now' menu
|
||||
canScanNow(t: Tag[]): boolean {
|
||||
if (!this.withClair) { return false; }
|
||||
if (!this.hasScanImagePermission) { return false; }
|
||||
let st: string = this.scanStatus(t[0]);
|
||||
|
||||
return st !== VULNERABILITY_SCAN_STATUS.pending &&
|
||||
st !== VULNERABILITY_SCAN_STATUS.running;
|
||||
return st !== VULNERABILITY_SCAN_STATUS.PENDING &&
|
||||
st !== VULNERABILITY_SCAN_STATUS.RUNNING;
|
||||
}
|
||||
getImagePermissionRule(projectId: number): void {
|
||||
let hasAddLabelImagePermission = this.userPermissionService.getPermission(projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY,
|
||||
@ -776,4 +758,26 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
onCpError($event: any): void {
|
||||
this.copyInput.setPullCommendShow();
|
||||
}
|
||||
getProjectScanner(): void {
|
||||
this.hasEnabledScanner = false;
|
||||
this.scanBtnState = ClrLoadingState.LOADING;
|
||||
this.http.get(`/api/projects/${this.projectId}/scanner`)
|
||||
.pipe(map(response => response as any))
|
||||
.pipe(catchError(error => observableThrowError(error)))
|
||||
.subscribe(response => {
|
||||
if (response && "{}" !== JSON.stringify(response) && !response.disable
|
||||
&& response.health) {
|
||||
this.hasEnabledScanner = true;
|
||||
}
|
||||
this.scanBtnState = ClrLoadingState.SUCCESS;
|
||||
}, error => {
|
||||
this.scanBtnState = ClrLoadingState.ERROR;
|
||||
});
|
||||
}
|
||||
handleScanOverview(scanOverview: any) {
|
||||
if (scanOverview) {
|
||||
return scanOverview[DEFAULT_SUPPORTED_MIME_TYPE];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -225,16 +225,35 @@ export class CustomComparator<T> implements Comparator<T> {
|
||||
*/
|
||||
export const DEFAULT_PAGE_SIZE: number = 15;
|
||||
|
||||
/**
|
||||
* The default supported mime type
|
||||
*/
|
||||
export const DEFAULT_SUPPORTED_MIME_TYPE = "application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0";
|
||||
|
||||
/**
|
||||
* The state of vulnerability scanning
|
||||
*/
|
||||
export const VULNERABILITY_SCAN_STATUS = {
|
||||
unknown: "n/a",
|
||||
pending: "pending",
|
||||
running: "running",
|
||||
error: "error",
|
||||
stopped: "stopped",
|
||||
finished: "finished"
|
||||
// front-end status
|
||||
NOT_SCANNED: "Not Scanned",
|
||||
// back-end status
|
||||
PENDING: "Pending",
|
||||
RUNNING: "Running",
|
||||
ERROR: "Error",
|
||||
STOPPED: "Stopped",
|
||||
SUCCESS: "Success",
|
||||
SCHEDULED: "Scheduled"
|
||||
};
|
||||
/**
|
||||
* The severity of vulnerability scanning
|
||||
*/
|
||||
export const VULNERABILITY_SEVERITY = {
|
||||
NEGLIGIBLE: "Negligible",
|
||||
UNKNOWN: "Unknown",
|
||||
LOW: "Low",
|
||||
MEDIUM: "Medium",
|
||||
HIGH: "High",
|
||||
CRITICAL: "Critical"
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1 @@
|
||||
<canvas class="canvas" #barChart> HTML5 canvas not supported </canvas>
|
@ -0,0 +1,28 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HistogramChartComponent } from './histogram-chart.component';
|
||||
import { TranslateModule } from "@ngx-translate/core";
|
||||
|
||||
|
||||
describe('HistogramChartComponent', () => {
|
||||
let component: HistogramChartComponent;
|
||||
let fixture: ComponentFixture<HistogramChartComponent>;
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [ HistogramChartComponent ],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HistogramChartComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,132 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DoCheck,
|
||||
ElementRef,
|
||||
Input,
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { forkJoin } from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'histogram-chart',
|
||||
templateUrl: './histogram-chart.component.html',
|
||||
styleUrls: ['./histogram-chart.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class HistogramChartComponent implements OnInit, AfterViewInit, DoCheck {
|
||||
@Input()
|
||||
metadata: Array<{
|
||||
text: string,
|
||||
value: number,
|
||||
color: string
|
||||
}> = [];
|
||||
translatedTextArr: Array<string> = [];
|
||||
@Input()
|
||||
isWhiteBackground: boolean = false;
|
||||
max: number;
|
||||
scale: number;
|
||||
hasViewInit: boolean = false;
|
||||
@ViewChild('barChart', { static: false }) barChart: ElementRef;
|
||||
public context: CanvasRenderingContext2D;
|
||||
constructor(private translate: TranslateService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.translateText();
|
||||
}
|
||||
ngAfterViewInit(): void {
|
||||
this.hasViewInit = true;
|
||||
this.initChart();
|
||||
}
|
||||
ngDoCheck() {
|
||||
if (this.hasViewInit) {
|
||||
this.initChart();
|
||||
}
|
||||
}
|
||||
translateText() {
|
||||
if (this.metadata && this.metadata.length > 0) {
|
||||
let textArr = [];
|
||||
this.metadata.forEach(item => {
|
||||
textArr.push(this.translate.get(item.text));
|
||||
});
|
||||
forkJoin(textArr).subscribe(
|
||||
(res: string[]) => {
|
||||
this.translatedTextArr = res;
|
||||
if (this.hasViewInit) {
|
||||
this.initChart();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
initChart() {
|
||||
if (this.barChart && this.metadata && this.metadata.length > 0) {
|
||||
this.barChart.nativeElement.width = "240";
|
||||
this.barChart.nativeElement.height = 25 + 20 * this.metadata.length + "";
|
||||
this.context = this.barChart.nativeElement.getContext('2d');
|
||||
this.getMax();
|
||||
if (this.isWhiteBackground) {
|
||||
this.context.fillStyle = "#000";
|
||||
} else {
|
||||
this.context.fillStyle = "#fff";
|
||||
}
|
||||
this.drawLine(50, 0, 50, 5 + this.metadata.length * 20);
|
||||
this.drawLine(50, 5 + this.metadata.length * 20, 250, 5 + this.metadata.length * 20);
|
||||
this.drawLine(90, 5 + this.metadata.length * 20, 90, 2 + this.metadata.length * 20);
|
||||
this.drawLine(130, 5 + this.metadata.length * 20, 130, 2 + this.metadata.length * 20);
|
||||
this.drawLine(170, 5 + this.metadata.length * 20, 170, 2 + this.metadata.length * 20);
|
||||
this.drawLine(210, 5 + this.metadata.length * 20, 210, 2 + this.metadata.length * 20);
|
||||
this.context.font = "12px";
|
||||
this.context.textAlign = "center";
|
||||
this.context.fillText(this.scale.toString(), 90, this.metadata.length * 20 + 18, 50);
|
||||
this.context.fillText((2 * this.scale).toString(), 130, this.metadata.length * 20 + 18, 50);
|
||||
this.context.fillText((3 * this.scale).toString(), 170, this.metadata.length * 20 + 18, 50);
|
||||
this.context.fillText((4 * this.scale).toString(), 210, this.metadata.length * 20 + 18, 50);
|
||||
this.metadata.forEach((item, index) => {
|
||||
this.drawBar(index, item.color, item.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
drawBar(index: number, color: string, value: number) {
|
||||
this.context.textBaseline = "middle";
|
||||
this.context.textAlign = "left";
|
||||
this.context.fillStyle = color;
|
||||
this.context.fillRect(50, 5 + index * 20, value / this.scale * 40, 15);
|
||||
this.context.fillText(value.toString(), (value / this.scale * 40) + 53, 12 + index * 20, 37);
|
||||
this.context.textAlign = "right";
|
||||
let text = "";
|
||||
if (this.translatedTextArr && this.translatedTextArr.length > 0) {
|
||||
text = this.translatedTextArr[index];
|
||||
} else {
|
||||
text = this.metadata[index].text;
|
||||
}
|
||||
this.context.fillText(text, 47, 12 + index * 20, 47);
|
||||
}
|
||||
drawLine(x, y, X, Y) {
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(x, y);
|
||||
this.context.lineTo(X, Y);
|
||||
if (this.isWhiteBackground) {
|
||||
this.context.strokeStyle = "#000";
|
||||
} else {
|
||||
this.context.strokeStyle = "#fff";
|
||||
}
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
getMax() {
|
||||
let count = 1;
|
||||
if (this.metadata && this.metadata.length > 0) {
|
||||
this.metadata.forEach(item => {
|
||||
if (item.value > count) {
|
||||
count = item.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.max = count;
|
||||
this.scale = Math.ceil(count / 4);
|
||||
}
|
||||
}
|
@ -2,13 +2,19 @@ import { Type } from "@angular/core";
|
||||
import { ResultGridComponent } from './result-grid.component';
|
||||
import { ResultBarChartComponent } from './result-bar-chart.component';
|
||||
import { ResultTipComponent } from './result-tip.component';
|
||||
import { HistogramChartComponent } from "./histogram-chart/histogram-chart.component";
|
||||
import { ResultTipHistogramComponent } from "./result-tip-histogram/result-tip-histogram.component";
|
||||
|
||||
export * from './result-tip.component';
|
||||
export * from "./result-grid.component";
|
||||
export * from './result-bar-chart.component';
|
||||
export * from './histogram-chart/histogram-chart.component';
|
||||
export * from './result-tip-histogram/result-tip-histogram.component';
|
||||
|
||||
export const VULNERABILITY_DIRECTIVES: Type<any>[] = [
|
||||
ResultGridComponent,
|
||||
ResultTipComponent,
|
||||
ResultBarChartComponent
|
||||
ResultBarChartComponent,
|
||||
HistogramChartComponent,
|
||||
ResultTipHistogramComponent
|
||||
];
|
||||
|
@ -1,7 +1,4 @@
|
||||
<div class="bar-wrapper">
|
||||
<div *ngIf="stopped" class="bar-state">
|
||||
<span class="label">{{'VULNERABILITY.STATE.STOPPED' | translate}}</span>
|
||||
</div>
|
||||
<div *ngIf="queued" class="bar-state">
|
||||
<span class="label label-orange">{{'VULNERABILITY.STATE.QUEUED' | translate}}</span>
|
||||
</div>
|
||||
@ -16,10 +13,9 @@
|
||||
<div class="progress loop loop-height"><progress></progress></div>
|
||||
</div>
|
||||
<div *ngIf="completed" class="bar-state bar-state-chart">
|
||||
<hbr-vulnerability-summary-chart [summary]="summary"></hbr-vulnerability-summary-chart>
|
||||
<hbr-result-tip-histogram [vulnerabilitySummary]="summary"></hbr-result-tip-histogram>
|
||||
</div>
|
||||
<div *ngIf="unknown" class="bar-state">
|
||||
<clr-icon shape="warning" class="is-warning" size="24"></clr-icon>
|
||||
<span class="unknow-text">{{'VULNERABILITY.STATE.UNKNOWN' | translate}}</span>
|
||||
<div *ngIf="otherStatus" class="bar-state">
|
||||
<span class="label">{{'VULNERABILITY.STATE.OTHER_STATUS' | translate}}</span>
|
||||
</div>
|
||||
</div>
|
@ -16,6 +16,8 @@ import { ErrorHandler } from '../error-handler/index';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||
import { ChannelService } from '../channel/index';
|
||||
import { ResultTipHistogramComponent } from "./result-tip-histogram/result-tip-histogram.component";
|
||||
import { HistogramChartComponent } from "./histogram-chart/histogram-chart.component";
|
||||
|
||||
describe('ResultBarChartComponent (inline template)', () => {
|
||||
let component: ResultBarChartComponent;
|
||||
@ -25,24 +27,15 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
||||
};
|
||||
let mockData: 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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -53,7 +46,9 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
],
|
||||
declarations: [
|
||||
ResultBarChartComponent,
|
||||
ResultTipComponent],
|
||||
ResultTipComponent,
|
||||
ResultTipHistogramComponent,
|
||||
HistogramChartComponent],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
ChannelService,
|
||||
@ -62,7 +57,7 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
{ provide: ScanningResultService, useValue: ScanningResultDefaultService },
|
||||
{ provide: JobLogService, useValue: JobLogDefaultService}
|
||||
]
|
||||
});
|
||||
}).compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
@ -83,21 +78,19 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
expect(serviceConfig.vulnerabilityScanningBaseEndpoint).toEqual("/api/vulnerability/testing");
|
||||
});
|
||||
|
||||
it('should show "not scanned" if status is STOPPED', async(() => {
|
||||
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.stopped;
|
||||
it('should show "not scanned" if status is STOPPED', () => {
|
||||
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.STOPPED;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('span');
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent).toEqual('VULNERABILITY.STATE.STOPPED');
|
||||
expect(el.textContent).toEqual('VULNERABILITY.STATE.OTHER_STATUS');
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('should show progress if status is SCANNING', async(() => {
|
||||
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.running;
|
||||
it('should show progress if status is SCANNING', () => {
|
||||
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.RUNNING;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
@ -106,12 +99,11 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.progress');
|
||||
expect(el).toBeTruthy();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('should show QUEUED if status is QUEUED', async(() => {
|
||||
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.pending;
|
||||
it('should show QUEUED if status is QUEUED', () => {
|
||||
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.PENDING;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
@ -122,19 +114,17 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
expect(el2.textContent).toEqual('VULNERABILITY.STATE.QUEUED');
|
||||
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('should show summary bar chart if status is COMPLETED', async(() => {
|
||||
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.finished;
|
||||
it('should show summary bar chart if status is COMPLETED', () => {
|
||||
component.summary.scan_status = VULNERABILITY_SCAN_STATUS.SUCCESS;
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('hbr-result-tip-histogram');
|
||||
expect(el).not.toBeNull();
|
||||
expect(el.style.width).toEqual("73px");
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -4,11 +4,10 @@ import {
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
ChangeDetectorRef,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { Subscription , timer} from "rxjs";
|
||||
|
||||
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||
import { clone, DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||
import {
|
||||
VulnerabilitySummary,
|
||||
TagService,
|
||||
@ -19,7 +18,7 @@ import { ErrorHandler } from '../error-handler/index';
|
||||
import { ChannelService } from '../channel/index';
|
||||
import { JobLogService } from "../service/index";
|
||||
|
||||
const STATE_CHECK_INTERVAL: number = 2000; // 2s
|
||||
const STATE_CHECK_INTERVAL: number = 3000; // 3s
|
||||
const RETRY_TIMES: number = 3;
|
||||
|
||||
@Component({
|
||||
@ -29,7 +28,6 @@ const RETRY_TIMES: number = 3;
|
||||
})
|
||||
export class ResultBarChartComponent implements OnInit, OnDestroy {
|
||||
@Input() repoName: string = "";
|
||||
@Input() tagStatus: string = "";
|
||||
@Input() tagId: string = "";
|
||||
@Input() summary: VulnerabilitySummary;
|
||||
onSubmitting: boolean = false;
|
||||
@ -48,8 +46,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if ((this.tagStatus === VULNERABILITY_SCAN_STATUS.running || this.tagStatus === VULNERABILITY_SCAN_STATUS.pending)
|
||||
&& !this.stateCheckTimer) {
|
||||
if ((this.status === VULNERABILITY_SCAN_STATUS.RUNNING ||
|
||||
this.status === VULNERABILITY_SCAN_STATUS.PENDING) &&
|
||||
!this.stateCheckTimer) {
|
||||
// Avoid duplicated subscribing
|
||||
this.stateCheckTimer = timer(0, STATE_CHECK_INTERVAL).subscribe(() => {
|
||||
this.getSummary();
|
||||
@ -78,41 +77,37 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
||||
if (this.summary && this.summary.scan_status) {
|
||||
return this.summary.scan_status;
|
||||
}
|
||||
|
||||
return VULNERABILITY_SCAN_STATUS.stopped;
|
||||
return VULNERABILITY_SCAN_STATUS.NOT_SCANNED;
|
||||
}
|
||||
|
||||
public get completed(): boolean {
|
||||
return this.status === VULNERABILITY_SCAN_STATUS.finished;
|
||||
return this.status === VULNERABILITY_SCAN_STATUS.SUCCESS;
|
||||
}
|
||||
|
||||
public get error(): boolean {
|
||||
return this.status === VULNERABILITY_SCAN_STATUS.error;
|
||||
return this.status === VULNERABILITY_SCAN_STATUS.ERROR;
|
||||
}
|
||||
|
||||
public get queued(): boolean {
|
||||
return this.status === VULNERABILITY_SCAN_STATUS.pending;
|
||||
return this.status === VULNERABILITY_SCAN_STATUS.PENDING;
|
||||
}
|
||||
|
||||
public get scanning(): boolean {
|
||||
return this.status === VULNERABILITY_SCAN_STATUS.running;
|
||||
return this.status === VULNERABILITY_SCAN_STATUS.RUNNING;
|
||||
}
|
||||
|
||||
public get stopped(): boolean {
|
||||
return this.status === VULNERABILITY_SCAN_STATUS.stopped;
|
||||
}
|
||||
|
||||
public get unknown(): boolean {
|
||||
return this.status === VULNERABILITY_SCAN_STATUS.unknown;
|
||||
public get otherStatus(): boolean {
|
||||
return !(this.completed || this.error || this.queued || this.scanning);
|
||||
}
|
||||
|
||||
scanNow(): void {
|
||||
if (this.onSubmitting) {
|
||||
// Avoid duplicated submitting
|
||||
console.log("duplicated submit");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.repoName || !this.tagId) {
|
||||
console.log("bad repository or tag");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -124,10 +119,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Forcely change status to queued after successful submitting
|
||||
this.summary = {
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.pending,
|
||||
severity: null,
|
||||
components: null,
|
||||
update_time: null
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.PENDING,
|
||||
};
|
||||
|
||||
// Forcely refresh view
|
||||
@ -154,8 +146,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
||||
this.tagService.getTag(this.repoName, this.tagId)
|
||||
.subscribe((t: Tag) => {
|
||||
// To keep the same summary reference, use value copy.
|
||||
this.copyValue(t.scan_overview);
|
||||
|
||||
if (t.scan_overview) {
|
||||
this.copyValue(t.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]);
|
||||
}
|
||||
// Forcely refresh view
|
||||
this.forceRefreshView(1000);
|
||||
|
||||
@ -183,11 +176,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
||||
|
||||
copyValue(newVal: VulnerabilitySummary): void {
|
||||
if (!this.summary || !newVal || !newVal.scan_status) { return; }
|
||||
this.summary.scan_status = newVal.scan_status;
|
||||
this.summary.job_id = newVal.job_id;
|
||||
this.summary.severity = newVal.severity;
|
||||
this.summary.components = newVal.components;
|
||||
this.summary.update_time = newVal.update_time;
|
||||
this.summary = clone(newVal);
|
||||
}
|
||||
|
||||
forceRefreshView(duration: number): void {
|
||||
@ -203,8 +192,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
|
||||
viewLog(): string {
|
||||
return this.jobLogService.getScanJobBaseUrl() + "/" + this.summary.job_id + "/log";
|
||||
return `/api/repositories/${this.repoName}/tags/${this.tagId}/scan/${this.summary.report_id}/log`;
|
||||
}
|
||||
}
|
||||
|
@ -1,51 +1,65 @@
|
||||
<div class="row result-row">
|
||||
<div>
|
||||
<div class="row flex-items-xs-right rightPos">
|
||||
<div>
|
||||
<div class="row flex-items-xs-right rightPos">
|
||||
<div class="flex-xs-middle option-right">
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder="{{'VULNERABILITY.PLACEHOLDER' | translate}}" (filterEvt)="filterVulnerabilities($event)"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder="{{'VULNERABILITY.PLACEHOLDER' | translate}}" (filterEvt)="filterVulnerabilities($event)"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<clr-datagrid>
|
||||
<clr-dg-action-bar>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<clr-dg-action-bar>
|
||||
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!hasScanImagePermission" (click)="scanNow()"><clr-icon shape="shield-check" size="16"></clr-icon> {{'VULNERABILITY.SCAN_NOW' | translate}}</button>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'severity'">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'fixedVersion'">{{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}}</clr-dg-column>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'severity'">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'fix_version'">{{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}}</clr-dg-column>
|
||||
|
||||
<clr-dg-placeholder>{{'VULNERABILITY.CHART.TOOLTIPS_TITLE_ZERO' | translate}}</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let res of scanningResults">
|
||||
<clr-dg-cell><a href="{{res.link}}" target="_blank">{{res.id}}</a></clr-dg-cell>
|
||||
<clr-dg-cell [ngSwitch]="res.severity">
|
||||
<span *ngSwitchCase="5" class="label label-danger">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchCase="4" class="label label-medium">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchCase="3" class="label label-low">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchCase="1" class="label">{{severityText(res.severity) | translate}}</span>
|
||||
<clr-dg-placeholder>{{'VULNERABILITY.CHART.TOOLTIPS_TITLE_ZERO' | translate}}</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let res of scanningResults">
|
||||
<clr-dg-cell>
|
||||
<span *ngIf="!res.links">{{res.id}}</span>
|
||||
<a *ngIf="res.links && res.links.length === 1" href="{{res.links[0]}}" target="_blank">{{res.id}}</a>
|
||||
<span *ngIf="res.links && res.links.length > 1">
|
||||
{{res.id}}
|
||||
<clr-signpost>
|
||||
<clr-signpost-content *clrIfOpen>
|
||||
<div class="mt-5px" *ngFor="let link of res.links">
|
||||
<a href="{{link}}" target="_blank">{{link}}</a>
|
||||
</div>
|
||||
</clr-signpost-content>
|
||||
</clr-signpost>
|
||||
</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell [ngSwitch]="res.severity">
|
||||
<span *ngSwitchCase="'Critical'" class="label label-critical">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchCase="'High'" class="label label-danger">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchCase="'Medium'" class="label label-medium">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchCase="'Low'" class="label label-low">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchCase="'Negligible'" class="label label-negligible">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchCase="'Unknown'" class="label label-unknown">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchDefault>{{severityText(res.severity) | translate}}</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{res.package}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{res.version}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div *ngIf="res.fixedVersion; else elseBlock">
|
||||
<clr-icon shape="wrench" class="is-success" size="20"></clr-icon> <span class="font-color-green">{{res.fixedVersion}}</span>
|
||||
</div>
|
||||
<ng-template #elseBlock>{{res.fixedVersion}}</ng-template>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-row-detail *clrIfExpanded>
|
||||
{{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}}
|
||||
</clr-dg-row-detail>
|
||||
</clr-dg-row>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{res.package}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{res.version}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div *ngIf="res.fix_version; else elseBlock">
|
||||
<clr-icon shape="wrench" class="is-success" size="20"></clr-icon> <span class="font-color-green">{{res.fix_version}}</span>
|
||||
</div>
|
||||
<ng-template #elseBlock>{{res.fix_version}}</ng-template>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-row-detail *clrIfExpanded>
|
||||
{{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}}
|
||||
</clr-dg-row-detail>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'VULNERABILITY.GRID.FOOT_OF' | translate}}</span>
|
||||
{{pagination.totalItems}} {{'VULNERABILITY.GRID.FOOT_ITEMS' | translate}}
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="25" [clrDgTotalItems]="scanningResults.length"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
<clr-dg-footer>
|
||||
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'VULNERABILITY.GRID.FOOT_OF' | translate}}</span> {{pagination.totalItems}} {{'VULNERABILITY.GRID.FOOT_ITEMS' | translate}}
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="25" [clrDgTotalItems]="scanningResults.length"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { VulnerabilityItem, VulnerabilitySeverity } from '../service/index';
|
||||
import { VulnerabilityItem } from '../service/index';
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { ResultGridComponent } from './result-grid.component';
|
||||
import { ScanningResultService, ScanningResultDefaultService } from '../service/scanning.service';
|
||||
@ -11,6 +11,7 @@ import {ChannelService} from "../channel/channel.service";
|
||||
import { UserPermissionService, UserPermissionDefaultService } from "../service/permission.service";
|
||||
import { USERSTATICPERMISSION } from "../service/permission-static";
|
||||
import { of } from "rxjs";
|
||||
import { DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SEVERITY } from "../utils";
|
||||
describe('ResultGridComponent (inline template)', () => {
|
||||
let component: ResultGridComponent;
|
||||
let fixture: ComponentFixture<ResultGridComponent>;
|
||||
@ -49,19 +50,21 @@ describe('ResultGridComponent (inline template)', () => {
|
||||
|
||||
serviceConfig = TestBed.get(SERVICE_CONFIG);
|
||||
scanningService = fixture.debugElement.injector.get(ScanningResultService);
|
||||
let mockData: VulnerabilityItem[] = [];
|
||||
let mockData: any = {};
|
||||
mockData[DEFAULT_SUPPORTED_MIME_TYPE] = {};
|
||||
mockData[DEFAULT_SUPPORTED_MIME_TYPE].vulnerabilities = [];
|
||||
for (let i = 0; i < 30; i++) {
|
||||
let res: VulnerabilityItem = {
|
||||
id: "CVE-2016-" + (8859 + i),
|
||||
severity: i % 2 === 0 ? VulnerabilitySeverity.HIGH : VulnerabilitySeverity.MEDIUM,
|
||||
severity: i % 2 === 0 ? 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);
|
||||
mockData[DEFAULT_SUPPORTED_MIME_TYPE].vulnerabilities.push(res);
|
||||
}
|
||||
|
||||
spy = spyOn(scanningService, 'getVulnerabilityScanningResults')
|
||||
@ -107,10 +110,6 @@ describe('ResultGridComponent (inline template)', () => {
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// let de: DebugElement = fixture.debugElement.query(del => del.classes['datagrid-cell']);
|
||||
// expect(de).toBeTruthy();
|
||||
// let el: HTMLElement = de.nativeElement;
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.datagrid-cell a');
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent.trim()).toEqual('CVE-2016-8859');
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import {
|
||||
ScanningResultService,
|
||||
VulnerabilityItem,
|
||||
VulnerabilitySeverity
|
||||
VulnerabilityItem
|
||||
} from '../service/index';
|
||||
import { ErrorHandler } from '../error-handler/index';
|
||||
import { forkJoin } from "rxjs";
|
||||
@ -10,6 +9,10 @@ import { forkJoin } from "rxjs";
|
||||
import { ChannelService } from "../channel/channel.service";
|
||||
import { UserPermissionService } from "../service/permission.service";
|
||||
import { USERSTATICPERMISSION } from "../service/permission-static";
|
||||
import { DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SEVERITY } from '../utils';
|
||||
import { finalize } from "rxjs/operators";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-vulnerabilities-grid',
|
||||
templateUrl: './result-grid.component.html',
|
||||
@ -18,7 +21,7 @@ import { USERSTATICPERMISSION } from "../service/permission-static";
|
||||
export class ResultGridComponent implements OnInit {
|
||||
scanningResults: VulnerabilityItem[] = [];
|
||||
dataCache: VulnerabilityItem[] = [];
|
||||
|
||||
loading: boolean = false;
|
||||
@Input() tagId: string;
|
||||
@Input() repositoryId: string;
|
||||
@Input() projectId: number;
|
||||
@ -40,11 +43,17 @@ export class ResultGridComponent implements OnInit {
|
||||
}
|
||||
|
||||
loadResults(repositoryId: string, tagId: string): void {
|
||||
this.loading = true;
|
||||
this.scanningService.getVulnerabilityScanningResults(repositoryId, tagId)
|
||||
.subscribe((results: VulnerabilityItem[]) => {
|
||||
this.dataCache = results;
|
||||
if (results) {
|
||||
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== '');
|
||||
.pipe(finalize(() => this.loading = false))
|
||||
.subscribe((results) => {
|
||||
if (results && results[DEFAULT_SUPPORTED_MIME_TYPE]) {
|
||||
let report = results[DEFAULT_SUPPORTED_MIME_TYPE];
|
||||
if (report.vulnerabilities) {
|
||||
this.dataCache = report.vulnerabilities;
|
||||
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== '');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, error => { this.errorHandler.error(error); });
|
||||
}
|
||||
@ -62,17 +71,19 @@ export class ResultGridComponent implements OnInit {
|
||||
this.loadResults(this.repositoryId, this.tagId);
|
||||
}
|
||||
|
||||
severityText(severity: VulnerabilitySeverity): string {
|
||||
severityText(severity: string): string {
|
||||
switch (severity) {
|
||||
case VulnerabilitySeverity.HIGH:
|
||||
case VULNERABILITY_SEVERITY.CRITICAL:
|
||||
return 'VULNERABILITY.SEVERITY.CRITICAL';
|
||||
case VULNERABILITY_SEVERITY.HIGH:
|
||||
return 'VULNERABILITY.SEVERITY.HIGH';
|
||||
case VulnerabilitySeverity.MEDIUM:
|
||||
case VULNERABILITY_SEVERITY.MEDIUM:
|
||||
return 'VULNERABILITY.SEVERITY.MEDIUM';
|
||||
case VulnerabilitySeverity.LOW:
|
||||
case VULNERABILITY_SEVERITY.LOW:
|
||||
return 'VULNERABILITY.SEVERITY.LOW';
|
||||
case VulnerabilitySeverity.NONE:
|
||||
case VULNERABILITY_SEVERITY.NEGLIGIBLE:
|
||||
return 'VULNERABILITY.SEVERITY.NEGLIGIBLE';
|
||||
case VulnerabilitySeverity.UNKNOWN:
|
||||
case VULNERABILITY_SEVERITY.UNKNOWN:
|
||||
return 'VULNERABILITY.SEVERITY.UNKNOWN';
|
||||
default:
|
||||
return 'UNKNOWN';
|
||||
|
@ -0,0 +1,55 @@
|
||||
<div class="tip-wrapper tip-position width-210">
|
||||
<clr-tooltip>
|
||||
<div clrTooltipTrigger class="tip-block">
|
||||
<ng-container *ngIf="!isNone">
|
||||
<div *ngIf="criticalCount > 0" class="tip-wrapper bar-block-critical shadow-critical width-30">{{criticalCount}}</div>
|
||||
<div *ngIf="highCount > 0" class="margin-left-5 tip-wrapper bar-block-high shadow-high width-30">{{highCount}}</div>
|
||||
<div *ngIf="mediumCount > 0" class="margin-left-5 tip-wrapper bar-block-medium shadow-medium width-30">{{mediumCount}}</div>
|
||||
<div *ngIf="lowCount > 0" class="margin-left-5 tip-wrapper bar-block-low shadow-low width-30">{{lowCount}}</div>
|
||||
<div *ngIf="negligibleCount > 0" class="margin-left-5 tip-wrapper bar-block-none shadow-none width-30">{{negligibleCount}}</div>
|
||||
<div *ngIf="unknownCount > 0" class="margin-left-5 tip-wrapper bar-block-unknown shadow-unknown width-30">{{unknownCount}}</div>
|
||||
</ng-container>
|
||||
<div *ngIf="isNone" class="margin-left-5 tip-wrapper bar-block-none shadow-none width-150">{{'VULNERABILITY.NO_VULNERABILITY' | translate }}</div>
|
||||
</div>
|
||||
<clr-tooltip-content class="w-800" [clrPosition]="'right'" [clrSize]="'lg'" *clrIfOpen>
|
||||
<div class="bar-tooltip-font-larger">
|
||||
<ng-container *ngIf="isCritical">
|
||||
<clr-icon shape="exclamation-circle" class="is-error" size="32"></clr-icon>
|
||||
<span>{{'VULNERABILITY.OVERALL_SEVERITY' | translate }} <span class="font-weight-600">{{'VULNERABILITY.SEVERITY.CRITICAL' | translate | titlecase }}</span></span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isHigh">
|
||||
<clr-icon shape="exclamation-triangle" class="is-error" size="32"></clr-icon>
|
||||
<span>{{'VULNERABILITY.OVERALL_SEVERITY' | translate }} <span class="font-weight-600">{{'VULNERABILITY.SEVERITY.HIGH' | translate | titlecase }}</span></span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isMedium">
|
||||
<clr-icon shape="minus-circle" class="tip-icon-medium" size="30"></clr-icon>
|
||||
<span>{{'VULNERABILITY.OVERALL_SEVERITY' | translate }} <span class="font-weight-600">{{'VULNERABILITY.SEVERITY.MEDIUM' | translate | titlecase}}</span></span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isLow">
|
||||
<clr-icon shape="info-circle" class="tip-icon-low" size="32"></clr-icon>
|
||||
<span>{{'VULNERABILITY.OVERALL_SEVERITY' | translate }} <span class="font-weight-600">{{'VULNERABILITY.SEVERITY.LOW' | translate | titlecase }}</span></span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isUnknown">
|
||||
<clr-icon shape="help" size="24" class="help-icon"></clr-icon>
|
||||
<span>{{'VULNERABILITY.OVERALL_SEVERITY' | translate }} <span class="font-weight-600">{{'VULNERABILITY.SEVERITY.UNKNOWN' | translate | titlecase }}</span></span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isNegligible">
|
||||
<clr-icon shape="circle" class="is-success" size="32"></clr-icon>
|
||||
<span>{{'VULNERABILITY.OVERALL_SEVERITY' | translate }} <span class="font-weight-600">{{'VULNERABILITY.SEVERITY.NEGLIGIBLE' | translate | titlecase }}</span></span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isNone">
|
||||
<clr-icon shape="check-circle" class="is-success" size="32"></clr-icon>
|
||||
<span>{{'VULNERABILITY.NO_VULNERABILITY' | translate }}</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="bar-summary bar-tooltip-fon" *ngIf="!isNone">
|
||||
<histogram-chart [isWhiteBackground]="false" [metadata]="passMetadataToChart()"></histogram-chart>
|
||||
</div>
|
||||
<div>
|
||||
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
|
||||
<span>{{completeTimestamp | date:'short'}}</span>
|
||||
</div>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
</div>
|
@ -0,0 +1,222 @@
|
||||
.bar-wrapper {
|
||||
width: 144px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.bar-state {
|
||||
text-align: center;
|
||||
.unknow-text {
|
||||
margin-left: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
.bar-state-chart {
|
||||
margin-top: 2px;
|
||||
.loop-height {
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.bar-state-error {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-left: -5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scanning-button {
|
||||
height: 24px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
top: -12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tip-wrapper {
|
||||
display: inline-block;
|
||||
height: 15px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.tip-position {
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.tip-block {
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
.bar-block-critical {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.bar-block-high {
|
||||
background-color: #e64524;
|
||||
}
|
||||
|
||||
.bar-block-medium {
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
.bar-block-low {
|
||||
background-color: #007CBB;
|
||||
}
|
||||
|
||||
.bar-block-none {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.bar-block-unknown {
|
||||
background-color: grey;
|
||||
}
|
||||
|
||||
.bar-tooltip-font {
|
||||
font-size: 13px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.bar-tooltip-font-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bar-summary {
|
||||
margin-top: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bar-scanning-time {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.bar-summary-item {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
span {
|
||||
:nth-child(1) {
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
:nth-child(2) {
|
||||
width: 28px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
}
|
||||
|
||||
.label.label-medium {
|
||||
background-color: #ffe4a9;
|
||||
border: 1px solid orange;
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.tip-icon-medium {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.label.label-low {
|
||||
background: rgba(251, 255, 0, 0.38);
|
||||
color: #c5c50b;
|
||||
border: 1px solid #e6e63f;
|
||||
}
|
||||
|
||||
.tip-icon-low {
|
||||
color: #007CBB;
|
||||
}
|
||||
|
||||
.font-color-green {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.bar-tooltip-font-larger {
|
||||
span {
|
||||
font-size: 16px;
|
||||
vertical-align: middle
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
border-bottom: 0;
|
||||
border-color: #aaa;
|
||||
margin: 6px -10px;
|
||||
}
|
||||
|
||||
.font-weight-600 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.ml-3px {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.shadow-critical {
|
||||
box-shadow: 1px -1px 1px red;
|
||||
}
|
||||
|
||||
.shadow-high {
|
||||
box-shadow: 1px -1px 1px #e64524;
|
||||
}
|
||||
|
||||
.shadow-medium {
|
||||
box-shadow: 1px -1px 1px orange;
|
||||
}
|
||||
|
||||
.shadow-low {
|
||||
box-shadow: 1px -1px 1px #007CBB;
|
||||
}
|
||||
|
||||
.shadow-none {
|
||||
box-shadow: 1px -1px 1px green;
|
||||
}
|
||||
|
||||
.shadow-unknown {
|
||||
box-shadow: 1px -1px 1px gray;
|
||||
}
|
||||
|
||||
.w-360 {
|
||||
width: 360px !important;
|
||||
}
|
||||
|
||||
.margin-left-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.width-30 {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.width-210 {
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.width-150 {
|
||||
width: 150px;
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ResultTipHistogramComponent } from './result-tip-histogram.component';
|
||||
import { ClarityModule } from "@clr/angular";
|
||||
import { TranslateModule, TranslateService } from "@ngx-translate/core";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { HistogramChartComponent } from "..";
|
||||
|
||||
describe('ResultTipHistogramComponent', () => {
|
||||
let component: ResultTipHistogramComponent;
|
||||
let fixture: ComponentFixture<ResultTipHistogramComponent>;
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
ClarityModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
TranslateService
|
||||
],
|
||||
declarations: [
|
||||
ResultTipHistogramComponent,
|
||||
HistogramChartComponent
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ResultTipHistogramComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,176 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { VulnerabilitySummary } from "../../service";
|
||||
import { VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../../utils";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-result-tip-histogram',
|
||||
templateUrl: './result-tip-histogram.component.html',
|
||||
styleUrls: ['./result-tip-histogram.component.scss']
|
||||
})
|
||||
export class ResultTipHistogramComponent implements OnInit {
|
||||
_tipTitle: string = "";
|
||||
@Input() vulnerabilitySummary: VulnerabilitySummary = {
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.NOT_SCANNED,
|
||||
severity: "",
|
||||
};
|
||||
constructor(private translate: TranslateService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
let key = "VULNERABILITY.SEVERITY.UNKNOWN";
|
||||
switch (this.vulnerabilitySummary.severity) {
|
||||
case VULNERABILITY_SEVERITY.CRITICAL:
|
||||
key = "VULNERABILITY.SEVERITY.CRITICAL";
|
||||
break;
|
||||
case VULNERABILITY_SEVERITY.HIGH:
|
||||
key = "VULNERABILITY.SEVERITY.HIGH";
|
||||
break;
|
||||
case VULNERABILITY_SEVERITY.MEDIUM:
|
||||
key = "VULNERABILITY.SEVERITY.MEDIUM";
|
||||
break;
|
||||
case VULNERABILITY_SEVERITY.LOW:
|
||||
key = "VULNERABILITY.SEVERITY.LOW";
|
||||
break;
|
||||
case VULNERABILITY_SEVERITY.NEGLIGIBLE:
|
||||
key = "VULNERABILITY.SEVERITY.NEGLIGIBLE";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.translate.get(key).subscribe( (res: string) => {
|
||||
this._tipTitle = res;
|
||||
});
|
||||
}
|
||||
|
||||
get tipTitle(): string {
|
||||
return this._tipTitle;
|
||||
}
|
||||
|
||||
get total(): number {
|
||||
if (this.vulnerabilitySummary &&
|
||||
this.vulnerabilitySummary.summary) {
|
||||
return this.vulnerabilitySummary.summary.total;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
get sevSummary(): {[key: string]: number} {
|
||||
if (this.vulnerabilitySummary &&
|
||||
this.vulnerabilitySummary.summary) {
|
||||
return this.vulnerabilitySummary.summary.summary;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get criticalCount(): number {
|
||||
if (this.sevSummary) {
|
||||
return this.sevSummary[VULNERABILITY_SEVERITY.CRITICAL];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
get highCount(): number {
|
||||
if (this.sevSummary) {
|
||||
return this.sevSummary[VULNERABILITY_SEVERITY.HIGH];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
get mediumCount(): number {
|
||||
if (this.sevSummary) {
|
||||
return this.sevSummary[VULNERABILITY_SEVERITY.MEDIUM];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
get lowCount(): number {
|
||||
if (this.sevSummary) {
|
||||
return this.sevSummary[VULNERABILITY_SEVERITY.LOW];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
get unknownCount(): number {
|
||||
if (this.vulnerabilitySummary && this.vulnerabilitySummary.summary
|
||||
&& this.vulnerabilitySummary.summary.summary) {
|
||||
return this.vulnerabilitySummary.summary.summary[VULNERABILITY_SEVERITY.UNKNOWN];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
get negligibleCount(): number {
|
||||
if (this.sevSummary) {
|
||||
return this.sevSummary[VULNERABILITY_SEVERITY.NEGLIGIBLE];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
get completeTimestamp(): Date {
|
||||
return this.vulnerabilitySummary && this.vulnerabilitySummary.end_time ? this.vulnerabilitySummary.end_time : new Date();
|
||||
}
|
||||
get isCritical(): boolean {
|
||||
return this.vulnerabilitySummary && VULNERABILITY_SEVERITY.CRITICAL === this.vulnerabilitySummary.severity;
|
||||
}
|
||||
get isHigh(): boolean {
|
||||
return this.vulnerabilitySummary && VULNERABILITY_SEVERITY.HIGH === this.vulnerabilitySummary.severity;
|
||||
}
|
||||
get isMedium(): boolean {
|
||||
return this.vulnerabilitySummary && VULNERABILITY_SEVERITY.MEDIUM === this.vulnerabilitySummary.severity;
|
||||
}
|
||||
get isLow(): boolean {
|
||||
return this.vulnerabilitySummary && VULNERABILITY_SEVERITY.LOW === this.vulnerabilitySummary.severity;
|
||||
}
|
||||
get isUnknown(): boolean {
|
||||
return this.vulnerabilitySummary && VULNERABILITY_SEVERITY.UNKNOWN === this.vulnerabilitySummary.severity;
|
||||
}
|
||||
get isNegligible(): boolean {
|
||||
return this.vulnerabilitySummary && VULNERABILITY_SEVERITY.NEGLIGIBLE === this.vulnerabilitySummary.severity;
|
||||
}
|
||||
get isNone(): boolean {
|
||||
return this.total === 0;
|
||||
}
|
||||
|
||||
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'
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
@ -15,24 +15,15 @@ describe('ResultTipComponent (inline template)', () => {
|
||||
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
||||
};
|
||||
let mockData: 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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -66,10 +57,10 @@ describe('ResultTipComponent (inline template)', () => {
|
||||
fixture.detectChanges();
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');
|
||||
expect(el).not.toBeNull();
|
||||
expect(el.style.width).toEqual("73px");
|
||||
expect(el.style.width).toEqual("0px");
|
||||
let el2: HTMLElement = fixture.nativeElement.querySelector('.bar-block-high');
|
||||
expect(el2).not.toBeNull();
|
||||
expect(el2.style.width).toEqual("10px");
|
||||
expect(el2.style.width).toEqual("0px");
|
||||
});
|
||||
}));
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { VulnerabilitySummary, VulnerabilitySeverity } from '../service/index';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||
|
||||
export const MIN_TIP_WIDTH = 5;
|
||||
@ -24,13 +22,9 @@ export class ResultTipComponent implements OnInit {
|
||||
packagesWithVul: number = 0;
|
||||
|
||||
@Input() summary: VulnerabilitySummary = {
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.unknown,
|
||||
severity: VulnerabilitySeverity.UNKNOWN,
|
||||
update_time: new Date(),
|
||||
components: {
|
||||
total: 0,
|
||||
summary: []
|
||||
}
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.NOT_SCANNED,
|
||||
severity: "",
|
||||
end_time: new Date(),
|
||||
};
|
||||
|
||||
get scanLevel() {
|
||||
@ -51,56 +45,9 @@ export class ResultTipComponent implements OnInit {
|
||||
return level;
|
||||
}
|
||||
|
||||
constructor(private translate: TranslateService) { }
|
||||
constructor() { }
|
||||
|
||||
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 => {
|
||||
if (item.severity !== VulnerabilitySeverity.NONE) {
|
||||
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(this.packageText(this.totalPackages)).subscribe((p1: string) => {
|
||||
this.translate.get(this.unitText(this.packagesWithVul)).subscribe((vul: string) => {
|
||||
if (this.totalPackages === 0) {
|
||||
this.translate.get('VULNERABILITY.CHART.TOOLTIPS_TITLE_ZERO').subscribe( (res: string) => {
|
||||
this._tipTitle = res;
|
||||
});
|
||||
} else {
|
||||
let messageKey = 'VULNERABILITY.CHART.TOOLTIPS_TITLE_SINGULAR';
|
||||
if (this.packagesWithVul > 1) {
|
||||
messageKey = 'VULNERABILITY.CHART.TOOLTIPS_TITLE';
|
||||
}
|
||||
this.translate.get(messageKey, {
|
||||
totalVulnerability: this.packagesWithVul,
|
||||
totalPackages: this.totalPackages,
|
||||
package: p1,
|
||||
vulnerability: vul
|
||||
}).subscribe((res: string) => this._tipTitle = res);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tipWidth(severity: VulnerabilitySeverity): string {
|
||||
@ -169,7 +116,7 @@ export class ResultTipComponent implements OnInit {
|
||||
}
|
||||
|
||||
public get completeTimestamp(): Date {
|
||||
return this.summary && this.summary.update_time ? this.summary.update_time : new Date();
|
||||
return this.summary && this.summary.end_time ? this.summary.end_time : new Date();
|
||||
}
|
||||
|
||||
public get hasHigh(): boolean {
|
||||
|
@ -1,6 +1,6 @@
|
||||
.bar-wrapper {
|
||||
width: 120px;
|
||||
height: 12px;
|
||||
width: 210px;
|
||||
height: 15px;
|
||||
}
|
||||
.bar-state {
|
||||
text-align: center !important;
|
||||
@ -48,7 +48,7 @@
|
||||
margin-left: -3px;
|
||||
}
|
||||
.bar-block-high {
|
||||
background-color: #e62700;
|
||||
background-color: #e64524;
|
||||
}
|
||||
.bar-block-medium {
|
||||
background-color: orange;
|
||||
@ -100,19 +100,10 @@
|
||||
color: #007CBB;
|
||||
}
|
||||
|
||||
.label.label-medium{
|
||||
background-color: #ffe4a9;
|
||||
border: 1px solid orange;
|
||||
color: orange;
|
||||
}
|
||||
.tip-icon-medium {
|
||||
color: orange;
|
||||
}
|
||||
.label.label-low{
|
||||
background: rgba(251, 255, 0, 0.38);
|
||||
color: #c5c50b;
|
||||
border: 1px solid #e6e63f;
|
||||
}
|
||||
|
||||
.tip-icon-low {
|
||||
color: yellow;
|
||||
}
|
||||
@ -145,3 +136,39 @@ hr{
|
||||
.help-icon {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.mt-3px {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.label-critical {
|
||||
background:red;
|
||||
color:#621501;
|
||||
border:1px solid #f8b5b4;
|
||||
}
|
||||
|
||||
.label-danger {
|
||||
background:#e64524!important;
|
||||
color:#621501!important;
|
||||
border:1px solid #f8b5b4!important;
|
||||
}
|
||||
.label-medium {
|
||||
background-color: orange;
|
||||
color:#621501;
|
||||
border:1px solid #f8b5b4;
|
||||
}
|
||||
.label-low {
|
||||
background: #007CBB;
|
||||
color:#cab6b1;
|
||||
border:1px solid #f8b5b4;
|
||||
}
|
||||
.label-negligible {
|
||||
background-color: green;
|
||||
color:#bad7ba;
|
||||
border:1px solid #f8b5b4;
|
||||
}
|
||||
.label-unknown {
|
||||
background-color: grey;
|
||||
color:#bad7ba;
|
||||
border:1px solid #f8b5b4;
|
||||
}
|
||||
|
@ -38,6 +38,12 @@
|
||||
<project-quotas [(allConfig)]="allConfig" (refreshAllconfig)="refreshAllconfig()"></project-quotas>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
<clr-tab>
|
||||
<button id="config-scanners" clrTabLink>{{'SCANNER.SCANNERS' | translate}}</button>
|
||||
<clr-tab-content id="scanners" *clrIfActive>
|
||||
<config-scanner></config-scanner>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
</clr-tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -22,20 +22,31 @@ import { ConfirmMessageHandler } from "./config.msg.utils";
|
||||
import { ConfigurationAuthComponent } from "./auth/config-auth.component";
|
||||
import { ConfigurationEmailComponent } from "./email/config-email.component";
|
||||
import { RobotApiRepository } from "../project/robot-account/robot.api.repository";
|
||||
import { ConfigurationScannerComponent } from "./scanner/config-scanner.component";
|
||||
import { NewScannerModalComponent } from "./scanner/new-scanner-modal/new-scanner-modal.component";
|
||||
import { NewScannerFormComponent } from "./scanner/new-scanner-form/new-scanner-form.component";
|
||||
import { ConfigScannerService } from "./scanner/config-scanner.service";
|
||||
import { ScannerMetadataComponent } from "./scanner/scanner-metadata/scanner-metadata.component";
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [CoreModule, SharedModule],
|
||||
declarations: [
|
||||
ConfigurationComponent,
|
||||
ConfigurationAuthComponent,
|
||||
ConfigurationEmailComponent
|
||||
],
|
||||
exports: [ConfigurationComponent],
|
||||
providers: [
|
||||
ConfigurationService,
|
||||
ConfirmMessageHandler,
|
||||
RobotApiRepository
|
||||
]
|
||||
imports: [CoreModule, SharedModule],
|
||||
declarations: [
|
||||
ConfigurationComponent,
|
||||
ConfigurationAuthComponent,
|
||||
ConfigurationEmailComponent,
|
||||
ConfigurationScannerComponent,
|
||||
NewScannerModalComponent,
|
||||
NewScannerFormComponent,
|
||||
ScannerMetadataComponent
|
||||
],
|
||||
exports: [ConfigurationComponent],
|
||||
providers: [
|
||||
ConfigurationService,
|
||||
ConfirmMessageHandler,
|
||||
RobotApiRepository,
|
||||
ConfigScannerService,
|
||||
]
|
||||
})
|
||||
export class ConfigurationModule {}
|
||||
export class ConfigurationModule {
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
<div class="row">
|
||||
<div>
|
||||
<clr-datagrid [clrDgLoading]="onGoing" [(clrDgSingleSelected)]="selectedRow">
|
||||
<clr-dg-action-bar>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-7">
|
||||
<button type="button" class="btn btn-sm btn-secondary" (click)="addNewScanner()">
|
||||
<clr-icon shape="plus" size="16"></clr-icon>
|
||||
{{'SCANNER.NEW_SCANNER' | translate}}
|
||||
</button>
|
||||
<button id="set-default" [disabled]="!(selectedRow && !selectedRow.is_default && !selectedRow.disabled)"
|
||||
class="btn btn-sm btn-secondary"
|
||||
(click)="setAsDefault()">{{'SCANNER.SET_AS_DEFAULT' | translate}}</button>
|
||||
<clr-dropdown [clrCloseMenuOnItemClick]="false" class="btn btn-sm btn-link" clrDropdownTrigger>
|
||||
<span>{{'MEMBER.ACTION' | translate}}<clr-icon class="clr-icon" shape="caret down"></clr-icon></span>
|
||||
<clr-dropdown-menu *clrIfOpen>
|
||||
<button clrDropdownItem
|
||||
(click)="changeStat()"
|
||||
[disabled]="!(selectedRow && !selectedRow.is_default)">
|
||||
<span *ngIf="selectedRow && selectedRow.disabled">{{'BUTTON.ENABLE' | translate}}</span>
|
||||
<span *ngIf="!(selectedRow && selectedRow.disabled)">{{'BUTTON.DISABLE' | translate}}</span>
|
||||
</button>
|
||||
<button clrDropdownItem
|
||||
(click)="editScanner()"
|
||||
class="btn btn-sm btn-secondary" [disabled]="!selectedRow">
|
||||
{{'BUTTON.EDIT' | translate}}
|
||||
</button>
|
||||
<button clrDropdownItem
|
||||
(click)="deleteScanners()"
|
||||
class="btn btn-sm btn-secondary" [disabled]="!selectedRow">
|
||||
{{'BUTTON.DELETE' | translate}}
|
||||
</button>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
<div class="clr-col-5">
|
||||
<div class="action-head-pos">
|
||||
<span (click)="getScanners()" class="refresh-btn">
|
||||
<clr-icon shape="refresh" [hidden]="onGoing"></clr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column class="width-240" [clrDgField]="'name'">{{'SCANNER.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column class="width-240" [clrDgField]="'url'">{{'SCANNER.ENDPOINT' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'SCANNER.HEALTH' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'SCANNER.DISABLED' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'SCANNER.AUTH' | translate}}</clr-dg-column>
|
||||
<clr-dg-placeholder>
|
||||
{{'SCANNER.NO_SCANNER' | translate}}
|
||||
</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let scanner of scanners" [clrDgItem]="scanner">
|
||||
<clr-dg-cell>
|
||||
<span>{{scanner.name}}</span>
|
||||
<span *ngIf="scanner.is_default" class="label label-info ml-1">{{'SCANNER.DEFAULT' | translate}}</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{scanner.url}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span *ngIf="scanner.health;else elseBlock" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
|
||||
<ng-template #elseBlock>
|
||||
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
|
||||
</ng-template>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{scanner.disabled}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{scanner.auth?scanner.auth:'None'}}</clr-dg-cell>
|
||||
<scanner-metadata *clrIfExpanded [uid]="scanner.uuid" ngProjectAs="clr-dg-row-detail"></scanner-metadata>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<span *ngIf="scanners?.length > 0">1 - {{scanners?.length}} {{'WEBHOOK.OF' | translate}} </span> {{scanners?.length}} {{'WEBHOOK.ITEMS' | translate}}
|
||||
<clr-dg-pagination [clrDgPageSize]="10"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
<new-scanner-modal (notify)="addSuccess()"></new-scanner-modal>
|
||||
</div>
|
@ -0,0 +1,24 @@
|
||||
.action-head-pos {
|
||||
padding-right: 18px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
margin-top: 7px;
|
||||
}
|
||||
.clr-icon {
|
||||
color: #0079b8;
|
||||
margin-top: 0;
|
||||
}
|
||||
.color-b {
|
||||
color: #bbb;
|
||||
}
|
||||
.margin-left-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.width-240 {
|
||||
min-width: 240px !important;
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { ClarityModule } from "@clr/angular";
|
||||
import { of } from "rxjs";
|
||||
import { ErrorHandler } from "@harbor/ui";
|
||||
import { ConfigurationScannerComponent } from "./config-scanner.component";
|
||||
import { ConfigScannerService } from "./config-scanner.service";
|
||||
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
|
||||
import { ConfirmationDialogService } from "../../shared/confirmation-dialog/confirmation-dialog.service";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { ScannerMetadataComponent } from "./scanner-metadata/scanner-metadata.component";
|
||||
import { NewScannerModalComponent } from "./new-scanner-modal/new-scanner-modal.component";
|
||||
import { NewScannerFormComponent } from "./new-scanner-form/new-scanner-form.component";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
describe('ConfigurationScannerComponent', () => {
|
||||
let mockScannerMetadata = {
|
||||
scanner: {
|
||||
name: 'test1',
|
||||
vendor: 'clair',
|
||||
version: '1.0.1',
|
||||
},
|
||||
capabilities: [{
|
||||
consumes_mime_types: ['consumes_mime_types'],
|
||||
produces_mime_types: ['consumes_mime_types']
|
||||
}]
|
||||
};
|
||||
let mockScanner1 = {
|
||||
name: 'test1',
|
||||
description: 'just a sample',
|
||||
version: '1.0.0',
|
||||
url: 'http://168.0.0.1'
|
||||
};
|
||||
let component: ConfigurationScannerComponent;
|
||||
let fixture: ComponentFixture<ConfigurationScannerComponent>;
|
||||
let fakedConfigScannerService = {
|
||||
getScannerMetadata() {
|
||||
return of(mockScannerMetadata);
|
||||
},
|
||||
getScanners() {
|
||||
return of([mockScanner1]);
|
||||
}
|
||||
};
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
BrowserAnimationsModule,
|
||||
ClarityModule,
|
||||
],
|
||||
declarations: [
|
||||
ConfigurationScannerComponent,
|
||||
ScannerMetadataComponent,
|
||||
NewScannerModalComponent,
|
||||
NewScannerFormComponent
|
||||
],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
MessageHandlerService,
|
||||
ConfirmationDialogService,
|
||||
TranslateService,
|
||||
{ provide: ConfigScannerService, useValue: fakedConfigScannerService },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ConfigurationScannerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
expect(component.scanners.length).toBe(1);
|
||||
});
|
||||
it('should be clickable', () => {
|
||||
component.selectedRow = mockScanner1;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('#set-default');
|
||||
expect(el.getAttribute('disable')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
149
src/portal/src/app/config/scanner/config-scanner.component.ts
Normal file
149
src/portal/src/app/config/scanner/config-scanner.component.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { Component, ViewChild, OnInit, OnDestroy } from "@angular/core";
|
||||
import { Scanner } from "./scanner";
|
||||
import { NewScannerModalComponent } from "./new-scanner-modal/new-scanner-modal.component";
|
||||
import { ConfigScannerService } from "./config-scanner.service";
|
||||
import { clone, ErrorHandler } from "@harbor/ui";
|
||||
import { finalize } from "rxjs/operators";
|
||||
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
|
||||
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../../shared/shared.const";
|
||||
import { ConfirmationDialogService } from "../../shared/confirmation-dialog/confirmation-dialog.service";
|
||||
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
|
||||
|
||||
@Component({
|
||||
selector: 'config-scanner',
|
||||
templateUrl: "config-scanner.component.html",
|
||||
styleUrls: ['./config-scanner.component.scss', '../config.component.scss']
|
||||
})
|
||||
export class ConfigurationScannerComponent implements OnInit, OnDestroy {
|
||||
scanners: Scanner[] = [];
|
||||
selectedRow: Scanner;
|
||||
onGoing: boolean = false;
|
||||
@ViewChild(NewScannerModalComponent, {static: false})
|
||||
newScannerDialog: NewScannerModalComponent;
|
||||
deletionSubscription: any;
|
||||
constructor(
|
||||
private configScannerService: ConfigScannerService,
|
||||
private errorHandler: ErrorHandler,
|
||||
private msgHandler: MessageHandlerService,
|
||||
private deletionDialogService: ConfirmationDialogService,
|
||||
) {}
|
||||
ngOnInit() {
|
||||
if (!this.deletionSubscription) {
|
||||
this.deletionSubscription = this.deletionDialogService.confirmationConfirm$.subscribe(confirmed => {
|
||||
if (confirmed &&
|
||||
confirmed.source === ConfirmationTargets.SCANNER &&
|
||||
confirmed.state === ConfirmationState.CONFIRMED) {
|
||||
this.configScannerService.deleteScanners(confirmed.data)
|
||||
.subscribe(response => {
|
||||
this.msgHandler.showSuccess("Delete Success");
|
||||
this.getScanners();
|
||||
}, error => {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
this.getScanners();
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
if (this.deletionSubscription) {
|
||||
this.deletionSubscription.unsubscribe();
|
||||
this.deletionSubscription = null;
|
||||
}
|
||||
}
|
||||
getScanners() {
|
||||
this.onGoing = true;
|
||||
this.configScannerService.getScanners()
|
||||
.pipe(finalize(() => this.onGoing = false))
|
||||
.subscribe(response => {
|
||||
this.scanners = response;
|
||||
}, error => {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
addNewScanner(): void {
|
||||
this.newScannerDialog.open();
|
||||
this.newScannerDialog.isEdit = false;
|
||||
this.newScannerDialog.newScannerFormComponent.isEdit = false;
|
||||
}
|
||||
addSuccess() {
|
||||
this.getScanners();
|
||||
}
|
||||
changeStat() {
|
||||
if (this.selectedRow) {
|
||||
let scanner: Scanner = clone(this.selectedRow);
|
||||
scanner.disabled = !scanner.disabled;
|
||||
this.configScannerService.updateScanner(scanner)
|
||||
.subscribe(response => {
|
||||
this.msgHandler.showSuccess("Update Success");
|
||||
this.getScanners();
|
||||
}, error => {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
setAsDefault() {
|
||||
if (this.selectedRow) {
|
||||
this.configScannerService.setAsDefault(this.selectedRow.uuid)
|
||||
.subscribe(response => {
|
||||
this.msgHandler.showSuccess("Update Success");
|
||||
this.getScanners();
|
||||
}, error => {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
deleteScanners() {
|
||||
if (this.selectedRow) {
|
||||
// Confirm deletion
|
||||
let msg: ConfirmationMessage = new ConfirmationMessage(
|
||||
"Confirm Scanner deletion",
|
||||
"SCANNER.DELETION_SUMMARY",
|
||||
this.selectedRow.name,
|
||||
[this.selectedRow],
|
||||
ConfirmationTargets.SCANNER,
|
||||
ConfirmationButtons.DELETE_CANCEL
|
||||
);
|
||||
this.deletionDialogService.openComfirmDialog(msg);
|
||||
}
|
||||
}
|
||||
editScanner() {
|
||||
if (this.selectedRow) {
|
||||
this.newScannerDialog.open();
|
||||
let resetValue: object = {};
|
||||
resetValue['name'] = this.selectedRow.name;
|
||||
resetValue['description'] = this.selectedRow.description;
|
||||
resetValue['url'] = this.selectedRow.url;
|
||||
resetValue['skipCertVerify'] = this.selectedRow.skip_certVerify;
|
||||
if (this.selectedRow.auth === 'Basic') {
|
||||
resetValue['auth'] = 'Basic';
|
||||
let username: string = this.selectedRow.access_credential.split(":")[0];
|
||||
let password: string = this.selectedRow.access_credential.split(":")[1];
|
||||
resetValue['accessCredential'] = {
|
||||
username: username,
|
||||
password: password
|
||||
};
|
||||
} else if (this.selectedRow.auth === 'Bearer') {
|
||||
resetValue['auth'] = 'Bearer';
|
||||
resetValue['accessCredential'] = {
|
||||
token: this.selectedRow.access_credential
|
||||
};
|
||||
} else if (this.selectedRow.auth === 'APIKey') {
|
||||
resetValue['auth'] = 'APIKey';
|
||||
resetValue['accessCredential'] = {
|
||||
apiKey: this.selectedRow.access_credential
|
||||
};
|
||||
} else {
|
||||
resetValue['auth'] = 'None';
|
||||
}
|
||||
this.newScannerDialog.newScannerFormComponent.newScannerForm.reset(resetValue);
|
||||
this.newScannerDialog.isEdit = true;
|
||||
this.newScannerDialog.newScannerFormComponent.isEdit = true;
|
||||
this.newScannerDialog.uid = this.selectedRow.uuid;
|
||||
this.newScannerDialog.originValue = clone(resetValue);
|
||||
this.newScannerDialog.newScannerFormComponent.originValue = clone(resetValue);
|
||||
this.newScannerDialog.editScanner = clone(this.selectedRow);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { ConfigScannerService } from "./config-scanner.service";
|
||||
|
||||
describe('TagService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule
|
||||
],
|
||||
providers: [
|
||||
ConfigScannerService
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be initialized', inject([ConfigScannerService], (service: ConfigScannerService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
70
src/portal/src/app/config/scanner/config-scanner.service.ts
Normal file
70
src/portal/src/app/config/scanner/config-scanner.service.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import {Scanner} from "./scanner";
|
||||
import { forkJoin, Observable, throwError as observableThrowError } from "rxjs";
|
||||
import { catchError, map } from "rxjs/operators";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ScannerMetadata } from "./scanner-metadata";
|
||||
|
||||
@Injectable()
|
||||
export class ConfigScannerService {
|
||||
|
||||
constructor( private http: HttpClient) {}
|
||||
getScannersByName(name: string): Observable<Scanner[]> {
|
||||
return this.http.get(`/api/scanners?ex_name=${name}`)
|
||||
.pipe(catchError(error => observableThrowError(error)))
|
||||
.pipe(map(response => response as Scanner[]));
|
||||
}
|
||||
getScannersByEndpointUrl(endpointUrl: string): Observable<Scanner[]> {
|
||||
return this.http.get(`/api/scanners?ex_url=${endpointUrl}`)
|
||||
.pipe(catchError(error => observableThrowError(error)))
|
||||
.pipe(map(response => response as Scanner[]));
|
||||
}
|
||||
testEndpointUrl(testValue: any): Observable<any> {
|
||||
return this.http.post(`/api/scanners/ping`, testValue)
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
addScanner(scanner: Scanner): Observable<any> {
|
||||
return this.http.post('/api/scanners', scanner )
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
getScanners(): Observable<Scanner[]> {
|
||||
return this.http.get('/api/scanners')
|
||||
.pipe(map(response => response as Scanner[]))
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
updateScanner(scanner: Scanner): Observable<any> {
|
||||
return this.http.put(`/api/scanners/${scanner.uuid}`, scanner )
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
deleteScanner(scanner: Scanner): Observable<any> {
|
||||
return this.http.delete(`/api/scanners/${scanner.uuid}`)
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
deleteScanners(scanners: Scanner[]): Observable<any> {
|
||||
let observableLists: any[] = [];
|
||||
if (scanners && scanners.length > 0) {
|
||||
scanners.forEach(scanner => {
|
||||
observableLists.push(this.deleteScanner(scanner));
|
||||
});
|
||||
return forkJoin(...observableLists);
|
||||
}
|
||||
}
|
||||
getProjectScanner(projectId: number): Observable<Scanner> {
|
||||
return this.http.get(`/api/projects/${projectId}/scanner`)
|
||||
.pipe(map(response => response as Scanner))
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
updateProjectScanner(projectId: number , uid: string): Observable<any> {
|
||||
return this.http.put(`/api/projects/${projectId}/scanner` , {uuid: uid})
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
getScannerMetadata(uid: string): Observable<ScannerMetadata> {
|
||||
return this.http.get(`/api/scanners/${uid}/metadata`)
|
||||
.pipe(map(response => response as ScannerMetadata))
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
setAsDefault(uid: string): Observable<any> {
|
||||
return this.http.patch(`/api/scanners/${uid}`, {is_default: true} )
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
<div>
|
||||
<form [formGroup]="newScannerForm" class="clr-form clr-form-horizontal">
|
||||
<div class="clr-form-control">
|
||||
<label class="required clr-control-label">{{"SCANNER.NAME" | translate}}</label>
|
||||
<div class="clr-control-container" [class.clr-error]="!isNameValid">
|
||||
<div class="clr-input-wrapper">
|
||||
<input autocomplete="off" #name formControlName="name" class="clr-input width-312"
|
||||
type="text"
|
||||
id="scanner-name">
|
||||
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
|
||||
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
|
||||
</div>
|
||||
<clr-control-error *ngIf="!isNameValid">
|
||||
{{nameTooltip | translate}}
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">{{"SCANNER.DESCRIPTION" | translate}}</label>
|
||||
<div class="clr-control-container">
|
||||
<textarea autocomplete="off" formControlName="description" class="clr-textarea width-312" type="text"
|
||||
id="description">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control">
|
||||
<label class="required clr-control-label">{{"SCANNER.ENDPOINT" | translate}}</label>
|
||||
<div class="clr-control-container" [class.clr-error]="!isEndpointValid || showEndpointError">
|
||||
<div class="clr-input-wrapper">
|
||||
<input (focus)="showEndpointError=false" (blur)="checkEndpointUrl()" #endpointUrl placeholder="http(s)://192.168.1.1" autocomplete="off" formControlName="url"
|
||||
class="clr-input width-312" type="text" id="scanner-endpoint">
|
||||
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
|
||||
<span class="spinner spinner-inline" [hidden]="!checkEndpointOnGoing"></span>
|
||||
</div>
|
||||
<clr-control-error *ngIf="!isEndpointValid || showEndpointError">
|
||||
{{endpointTooltip | translate}}
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">{{"SCANNER.AUTH" | translate}}</label>
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-select-wrapper">
|
||||
<select formControlName="auth" class="clr-select width-312" id="scanner-authorization">
|
||||
<option value="None">{{"SCANNER.NONE" | translate}}</option>
|
||||
<option value="Basic">{{"SCANNER.BASIC" | translate}}</option>
|
||||
<option value="Bearer">{{"SCANNER.BEARER" | translate}}</option>
|
||||
<option value="APIKey">{{"SCANNER.API_KEY" | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container formGroupName="accessCredential">
|
||||
<div class="clr-form-control" *ngIf="auth==='Basic'">
|
||||
<label class="required clr-control-label">{{"SCANNER.USERNAME" | translate}}</label>
|
||||
<div class="clr-control-container" [class.clr-error]="!isUserNameValid">
|
||||
<div class="clr-input-wrapper">
|
||||
<input formControlName="username" autocomplete="off"
|
||||
class="clr-input width-312" type="text" id="scanner-username">
|
||||
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
|
||||
</div>
|
||||
<clr-control-error *ngIf="!isUserNameValid">
|
||||
{{"SCANNER.USERNAME_REQUIRED" | translate}}
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control" *ngIf="auth==='Basic'">
|
||||
<label class="required clr-control-label">{{"SCANNER.PASSWORD" | translate}}</label>
|
||||
<div class="clr-control-container" [class.clr-error]="!isPasswordValid">
|
||||
<div class="clr-input-wrapper">
|
||||
<input formControlName="password" autocomplete="off"
|
||||
class="clr-input width-312" type="password" id="scanner-password">
|
||||
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
|
||||
</div>
|
||||
<clr-control-error *ngIf="!isPasswordValid">
|
||||
{{"SCANNER.PASSWORD_REQUIRED" | translate}}
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control" *ngIf="auth==='Bearer'">
|
||||
<label class="required clr-control-label">{{"SCANNER.TOKEN" | translate}}</label>
|
||||
<div class="clr-control-container" [class.clr-error]="!isTokenValid">
|
||||
<div class="clr-input-wrapper">
|
||||
<input formControlName="token" autocomplete="off"
|
||||
class="clr-input width-312" type="text" id="scanner-token">
|
||||
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
|
||||
</div>
|
||||
<clr-control-error *ngIf="!isTokenValid">
|
||||
{{"SCANNER.TOKEN_REQUIRED" | translate}}
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control" *ngIf="auth==='APIKey'">
|
||||
<label class="required clr-control-label">{{"SCANNER.API_KEY" | translate}}</label>
|
||||
<div class="clr-control-container" [class.clr-error]="!isApiKeyValid">
|
||||
<div class="clr-input-wrapper">
|
||||
<input formControlName="apiKey" autocomplete="off"
|
||||
class="clr-input width-312" type="text" id="scanner-apiKey">
|
||||
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
|
||||
</div>
|
||||
<clr-control-error *ngIf="!isApiKeyValid">
|
||||
{{"SCANNER.API_KEY_REQUIRED" | translate}}
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">{{"SCANNER.SKIP" | translate}}
|
||||
<clr-tooltip>
|
||||
<clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon>
|
||||
<clr-tooltip-content clrPosition="top-right" clrSize="lg" *clrIfOpen>
|
||||
{{'SCANNER.SKIP_CERT_VERIFY' | translate}}
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
</label>
|
||||
<div class="clr-control-container padding-top-3">
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input clrCheckbox formControlName="skipCertVerify"
|
||||
type="checkbox" id="scanner-skipCertVerify">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,6 @@
|
||||
.width-312 {
|
||||
width: 312px;
|
||||
}
|
||||
.padding-top-3 {
|
||||
padding-top: 3px;
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
import { async, ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing';
|
||||
import { NewScannerFormComponent } from "./new-scanner-form.component";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { ClarityModule } from "@clr/angular";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { ConfigScannerService } from "../config-scanner.service";
|
||||
import { of } from "rxjs";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
describe('NewScannerFormComponent', () => {
|
||||
let mockScanner1 = {
|
||||
name: 'test1',
|
||||
description: 'just a sample',
|
||||
version: '1.0.0',
|
||||
url: 'http://168.0.0.1'
|
||||
};
|
||||
let component: NewScannerFormComponent;
|
||||
let fixture: ComponentFixture<NewScannerFormComponent>;
|
||||
let fakedConfigScannerService = {
|
||||
getScannersByName() {
|
||||
return of([mockScanner1]);
|
||||
}
|
||||
};
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
BrowserAnimationsModule,
|
||||
ClarityModule,
|
||||
],
|
||||
declarations: [ NewScannerFormComponent ],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
TranslateService,
|
||||
{ provide: ConfigScannerService, useValue: fakedConfigScannerService },
|
||||
// open auto detect
|
||||
{ provide: ComponentFixtureAutoDetect, useValue: true }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(NewScannerFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should creat', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('should show "name is required"', () => {
|
||||
let nameInput = fixture.nativeElement.querySelector('#scanner-name');
|
||||
nameInput.value = "";
|
||||
nameInput.dispatchEvent(new Event('input'));
|
||||
nameInput.blur();
|
||||
nameInput.dispatchEvent(new Event('blur'));
|
||||
let el = fixture.nativeElement.querySelector('clr-control-error');
|
||||
expect(el).toBeTruthy();
|
||||
});
|
||||
|
||||
it('name should be valid', () => {
|
||||
let nameInput = fixture.nativeElement.querySelector('#scanner-name');
|
||||
nameInput.value = "test2";
|
||||
nameInput.dispatchEvent(new Event('input'));
|
||||
nameInput.blur();
|
||||
nameInput.dispatchEvent(new Event('blur'));
|
||||
setTimeout(() => {
|
||||
let el = fixture.nativeElement.querySelector('clr-control-error');
|
||||
expect(el).toBeFalsy();
|
||||
}, 900);
|
||||
});
|
||||
|
||||
it('endpoint url should be valid', () => {
|
||||
let nameInput = fixture.nativeElement.querySelector('#scanner-name');
|
||||
nameInput.value = "test2";
|
||||
let urlInput = fixture.nativeElement.querySelector('#scanner-endpoint');
|
||||
urlInput.value = "http://168.0.0.1";
|
||||
urlInput.dispatchEvent(new Event('input'));
|
||||
urlInput.blur();
|
||||
urlInput.dispatchEvent(new Event('blur'));
|
||||
setTimeout(() => {
|
||||
let el = fixture.nativeElement.querySelector('clr-control-error');
|
||||
expect(el).toBeFalsy();
|
||||
}, 900);
|
||||
});
|
||||
|
||||
it('auth should be valid', () => {
|
||||
let authInput = fixture.nativeElement.querySelector('#scanner-authorization');
|
||||
authInput.value = "Basic";
|
||||
authInput.dispatchEvent(new Event('change'));
|
||||
let usernameInput = fixture.nativeElement.querySelector('#scanner-username');
|
||||
let passwordInput = fixture.nativeElement.querySelector('#scanner-password');
|
||||
expect(usernameInput).toBeTruthy();
|
||||
expect(passwordInput).toBeTruthy();
|
||||
usernameInput.value = "user";
|
||||
passwordInput.value = "12345";
|
||||
usernameInput.dispatchEvent(new Event('input'));
|
||||
passwordInput.dispatchEvent(new Event('input'));
|
||||
let el = fixture.nativeElement.querySelector('clr-control-error');
|
||||
expect(el).toBeFalsy();
|
||||
});
|
||||
});
|
@ -0,0 +1,197 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from "@angular/core";
|
||||
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
|
||||
import { fromEvent } from "rxjs";
|
||||
import { debounceTime, distinctUntilChanged, filter, finalize, map, switchMap } from "rxjs/operators";
|
||||
import { ConfigScannerService } from "../config-scanner.service";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'new-scanner-form',
|
||||
templateUrl: 'new-scanner-form.component.html',
|
||||
styleUrls: ['new-scanner-form.component.scss']
|
||||
})
|
||||
export class NewScannerFormComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
checkOnGoing: boolean = false;
|
||||
newScannerForm: FormGroup = this.fb.group({
|
||||
name: this.fb.control("",
|
||||
[Validators.required, Validators.pattern(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/)]),
|
||||
description: this.fb.control(""),
|
||||
url: this.fb.control("",
|
||||
[Validators.required,
|
||||
Validators.pattern(/^http[s]?:\/\//)]),
|
||||
auth: this.fb.control(""),
|
||||
accessCredential: this.fb.group({
|
||||
username: this.fb.control("", Validators.required),
|
||||
password: this.fb.control("", Validators.required),
|
||||
token: this.fb.control("", Validators.required),
|
||||
apiKey: this.fb.control("", Validators.required)
|
||||
}),
|
||||
skipCertVerify: this.fb.control(false)
|
||||
});
|
||||
checkNameSubscribe: any;
|
||||
checkEndpointUrlSubscribe: any;
|
||||
nameTooltip: string;
|
||||
endpointTooltip: string;
|
||||
isNameExisting: boolean = false;
|
||||
checkEndpointOnGoing: boolean = false;
|
||||
isEndpointUrlExisting: boolean = false;
|
||||
showEndpointError: boolean = false;
|
||||
originValue: any;
|
||||
isEdit: boolean;
|
||||
@ViewChild('name', {static: false}) scannerName: ElementRef;
|
||||
@ViewChild('endpointUrl', {static: false}) scannerEndpointUrl: ElementRef;
|
||||
constructor(private fb: FormBuilder, private scannerService: ConfigScannerService) {
|
||||
}
|
||||
ngAfterViewInit(): void {
|
||||
if (!this.checkNameSubscribe) {
|
||||
this.checkNameSubscribe = fromEvent(this.scannerName.nativeElement, 'input').pipe(
|
||||
map((e: any) => e.target.value),
|
||||
filter(name => {
|
||||
if (this.isEdit && this.originValue && this.originValue.name === name) {
|
||||
return false;
|
||||
}
|
||||
return this.newScannerForm.get('name').valid && name.length > 1;
|
||||
}),
|
||||
debounceTime(500),
|
||||
distinctUntilChanged(),
|
||||
switchMap((name) => {
|
||||
this.isNameExisting = false;
|
||||
this.checkOnGoing = true;
|
||||
return this.scannerService.getScannersByName(name)
|
||||
.pipe(finalize(() => this.checkOnGoing = false));
|
||||
})).subscribe(response => {
|
||||
if (response && response.length > 0) {
|
||||
response.forEach(s => {
|
||||
if (s.name === this.newScannerForm.get('name').value) {
|
||||
this.isNameExisting = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, error => {
|
||||
this.isNameExisting = false;
|
||||
});
|
||||
}
|
||||
if (!this.checkEndpointUrlSubscribe) {
|
||||
this.checkEndpointUrlSubscribe = fromEvent(this.scannerEndpointUrl.nativeElement, 'input').pipe(
|
||||
map((e: any) => e.target.value),
|
||||
filter(endpointUrl => {
|
||||
if (this.isEdit && this.originValue && this.originValue.url === endpointUrl) {
|
||||
return false;
|
||||
}
|
||||
return this.newScannerForm.get('url').valid && endpointUrl.length > 6;
|
||||
}),
|
||||
debounceTime(800),
|
||||
distinctUntilChanged(),
|
||||
switchMap((endpointUrl) => {
|
||||
this.isEndpointUrlExisting = false;
|
||||
this.checkEndpointOnGoing = true;
|
||||
return this.scannerService.getScannersByEndpointUrl(endpointUrl)
|
||||
.pipe(finalize(() => this.checkEndpointOnGoing = false));
|
||||
})).subscribe(response => {
|
||||
if (response && response.length > 0) {
|
||||
response.forEach(s => {
|
||||
if (s.url === this.newScannerForm.get('url').value) {
|
||||
this.isEndpointUrlExisting = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, error => {
|
||||
this.isEndpointUrlExisting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
ngOnDestroy() {
|
||||
if (this.checkNameSubscribe) {
|
||||
this.checkNameSubscribe.unsubscribe();
|
||||
this.checkNameSubscribe = null;
|
||||
}
|
||||
if (this.checkEndpointUrlSubscribe) {
|
||||
this.checkEndpointUrlSubscribe.unsubscribe();
|
||||
this.checkEndpointUrlSubscribe = null;
|
||||
}
|
||||
}
|
||||
get isNameValid(): boolean {
|
||||
if (!(this.newScannerForm.get('name').dirty || this.newScannerForm.get('name').touched)) {
|
||||
return true;
|
||||
}
|
||||
if (this.checkOnGoing) {
|
||||
return true;
|
||||
}
|
||||
if (this.isNameExisting) {
|
||||
this.nameTooltip = 'NAME_EXISTS';
|
||||
return false;
|
||||
}
|
||||
if (this.newScannerForm.get('name').errors && this.newScannerForm.get('name').errors.required) {
|
||||
this.nameTooltip = 'NAME_REQUIRED';
|
||||
return false;
|
||||
}
|
||||
if (this.newScannerForm.get('name').errors && this.newScannerForm.get('name').errors.pattern) {
|
||||
this.nameTooltip = 'NAME_REX';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
get isEndpointValid(): boolean {
|
||||
if (!(this.newScannerForm.get('url').dirty || this.newScannerForm.get('url').touched)) {
|
||||
return true;
|
||||
}
|
||||
if (this.checkEndpointOnGoing) {
|
||||
return true;
|
||||
}
|
||||
if (this.isEndpointUrlExisting) {
|
||||
this.endpointTooltip = 'ENDPOINT_EXISTS';
|
||||
return false;
|
||||
}
|
||||
if (this.newScannerForm.get('url').errors && this.newScannerForm.get('url').errors.required) {
|
||||
this.endpointTooltip = 'ENDPOINT_REQUIRED';
|
||||
return false;
|
||||
}
|
||||
// skip here, validate when onblur
|
||||
if (this.newScannerForm.get('url').errors && this.newScannerForm.get('url').errors.pattern) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// validate endpointUrl when onblur
|
||||
checkEndpointUrl() {
|
||||
if (this.newScannerForm.get('url').errors && this.newScannerForm.get('url').errors.pattern) {
|
||||
this.endpointTooltip = "ILLEGAL_ENDPOINT";
|
||||
this.showEndpointError = true;
|
||||
}
|
||||
}
|
||||
get auth(): string {
|
||||
return this.newScannerForm.get('auth').value;
|
||||
}
|
||||
get isUserNameValid(): boolean {
|
||||
return !(this.newScannerForm.get('accessCredential').get('username').invalid
|
||||
&& (this.newScannerForm.get('accessCredential').get('username').dirty
|
||||
|| this.newScannerForm.get('accessCredential').get('username').touched));
|
||||
}
|
||||
get isPasswordValid(): boolean {
|
||||
return !(this.newScannerForm.get('accessCredential').get('password').invalid
|
||||
&& (this.newScannerForm.get('accessCredential').get('password').dirty
|
||||
|| this.newScannerForm.get('accessCredential').get('password').touched));
|
||||
}
|
||||
get isTokenValid(): boolean {
|
||||
return !(this.newScannerForm.get('accessCredential').get('token').invalid
|
||||
&& (this.newScannerForm.get('accessCredential').get('token').dirty
|
||||
|| this.newScannerForm.get('accessCredential').get('token').touched));
|
||||
}
|
||||
get isApiKeyValid(): boolean {
|
||||
return !(this.newScannerForm.get('accessCredential').get('apiKey').invalid
|
||||
&& (this.newScannerForm.get('accessCredential').get('apiKey').dirty
|
||||
|| this.newScannerForm.get('accessCredential').get('apiKey').touched));
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true" [clrModalClosable]="false">
|
||||
<h3 *ngIf="!isEdit" class="modal-title">{{'SCANNER.ADD_SCANNER' | translate}}</h3>
|
||||
<h3 *ngIf="isEdit" class="modal-title">{{'SCANNER.EDIT_SCANNER' | translate}}</h3>
|
||||
<div class="modal-body body-format">
|
||||
<inline-alert class="modal-title"></inline-alert>
|
||||
<new-scanner-form></new-scanner-form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="button-test" type="button" [clrLoading]="checkBtnState" class="btn btn-outline" (click)="onTestEndpoint()" [disabled]="!canTestEndpoint">{{'SCANNER.TEST_CONNECTION' | translate}}</button>
|
||||
<button id="button-cancel" type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button id="button-add" *ngIf="!isEdit" type="button" [clrLoading]="saveBtnState" class="btn btn-primary" [disabled]="!valid" (click)="create()">{{'BUTTON.ADD' | translate}}</button>
|
||||
<button id="button-save" *ngIf="isEdit" type="button" [clrLoading]="saveBtnState" class="btn btn-primary" [disabled]="!validForSaving" (click)="save()">{{'BUTTON.SAVE' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
@ -0,0 +1,144 @@
|
||||
import { async, ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing';
|
||||
import { ClrLoadingState } from "@clr/angular";
|
||||
import { ConfigScannerService } from "../config-scanner.service";
|
||||
import { NewScannerModalComponent } from "./new-scanner-modal.component";
|
||||
import { MessageHandlerService } from "../../../shared/message-handler/message-handler.service";
|
||||
import { NewScannerFormComponent } from "../new-scanner-form/new-scanner-form.component";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { of, Subscription } from "rxjs";
|
||||
import { delay } from "rxjs/operators";
|
||||
import { SharedModule } from "@harbor/ui";
|
||||
import { SharedModule as AppSharedModule } from "../../../shared/shared.module";
|
||||
|
||||
describe('NewScannerModalComponent', () => {
|
||||
let component: NewScannerModalComponent;
|
||||
let fixture: ComponentFixture<NewScannerModalComponent>;
|
||||
|
||||
let mockScanner1 = {
|
||||
name: 'test1',
|
||||
description: 'just a sample',
|
||||
url: 'http://168.0.0.1',
|
||||
auth: "",
|
||||
};
|
||||
let fakedConfigScannerService = {
|
||||
getScannersByName() {
|
||||
return of([mockScanner1]);
|
||||
},
|
||||
testEndpointUrl() {
|
||||
return of(true).pipe(delay(200));
|
||||
},
|
||||
addScanner() {
|
||||
return of(true).pipe(delay(200));
|
||||
},
|
||||
updateScanner() {
|
||||
return of(true).pipe(delay(200));
|
||||
}
|
||||
};
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
AppSharedModule
|
||||
],
|
||||
declarations: [
|
||||
NewScannerFormComponent,
|
||||
NewScannerModalComponent,
|
||||
],
|
||||
providers: [
|
||||
MessageHandlerService,
|
||||
{ provide: ConfigScannerService, useValue: fakedConfigScannerService },
|
||||
FormBuilder,
|
||||
// open auto detect
|
||||
{ provide: ComponentFixtureAutoDetect, useValue: true }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(NewScannerModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.opened = true;
|
||||
component.newScannerFormComponent.checkNameSubscribe = new Subscription();
|
||||
component.newScannerFormComponent.checkEndpointUrlSubscribe = new Subscription();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should creat', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('should be add mode', () => {
|
||||
component.isEdit = false;
|
||||
fixture.detectChanges();
|
||||
let el = fixture.nativeElement.querySelector('#button-add');
|
||||
expect(el).toBeTruthy();
|
||||
});
|
||||
it('should be edit mode', () => {
|
||||
component.isEdit = true;
|
||||
fixture.detectChanges();
|
||||
let el = fixture.nativeElement.querySelector('#button-save');
|
||||
expect(el).toBeTruthy();
|
||||
// set origin value
|
||||
component.originValue = mockScanner1;
|
||||
component.editScanner = {};
|
||||
// input same value to origin
|
||||
fixture.nativeElement.querySelector('#scanner-name').value = "test2";
|
||||
fixture.nativeElement.querySelector('#description').value = "just a sample";
|
||||
fixture.nativeElement.querySelector('#scanner-endpoint').value = "http://168.0.0.1";
|
||||
fixture.nativeElement.querySelector('#scanner-authorization').value = "";
|
||||
fixture.nativeElement.querySelector('#scanner-name').dispatchEvent(new Event('input'));
|
||||
fixture.nativeElement.querySelector('#description').dispatchEvent(new Event('input'));
|
||||
fixture.nativeElement.querySelector('#scanner-endpoint').dispatchEvent(new Event('input'));
|
||||
fixture.nativeElement.querySelector('#scanner-authorization').dispatchEvent(new Event('input'));
|
||||
// save button should not be disabled
|
||||
expect(component.validForSaving).toBeTruthy();
|
||||
fixture.nativeElement.querySelector('#scanner-name').value = "test3";
|
||||
fixture.nativeElement.querySelector('#scanner-name').dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
expect(component.validForSaving).toBeTruthy();
|
||||
el.click();
|
||||
el.dispatchEvent(new Event('click'));
|
||||
setTimeout(() => {
|
||||
expect(component.opened).toBeFalsy();
|
||||
}, 300);
|
||||
});
|
||||
it('test connection button should not be disabled', () => {
|
||||
let nameInput = fixture.nativeElement.querySelector('#scanner-name');
|
||||
nameInput.value = "test2";
|
||||
nameInput.dispatchEvent(new Event('input'));
|
||||
let urlInput = fixture.nativeElement.querySelector('#scanner-endpoint');
|
||||
urlInput.value = "http://168.0.0.1";
|
||||
urlInput.dispatchEvent(new Event('input'));
|
||||
expect(component.canTestEndpoint).toBeTruthy();
|
||||
let el = fixture.nativeElement.querySelector('#button-test');
|
||||
el.click();
|
||||
el.dispatchEvent(new Event('click'));
|
||||
expect(component.checkBtnState).toBe(ClrLoadingState.LOADING);
|
||||
setTimeout(() => {
|
||||
expect(component.checkBtnState).toBe(ClrLoadingState.SUCCESS);
|
||||
}, 300);
|
||||
});
|
||||
it('add button should not be disabled', () => {
|
||||
fixture.nativeElement.querySelector('#scanner-name').value = "test2";
|
||||
fixture.nativeElement.querySelector('#scanner-endpoint').value = "http://168.0.0.1";
|
||||
let authInput = fixture.nativeElement.querySelector('#scanner-authorization');
|
||||
authInput.value = "Basic";
|
||||
authInput.dispatchEvent(new Event('change'));
|
||||
let usernameInput = fixture.nativeElement.querySelector('#scanner-username');
|
||||
let passwordInput = fixture.nativeElement.querySelector('#scanner-password');
|
||||
expect(usernameInput).toBeTruthy();
|
||||
expect(passwordInput).toBeTruthy();
|
||||
usernameInput.value = "user";
|
||||
passwordInput.value = "12345";
|
||||
usernameInput.dispatchEvent(new Event('input'));
|
||||
passwordInput.dispatchEvent(new Event('input'));
|
||||
let el = fixture.nativeElement.querySelector('#button-add');
|
||||
expect(component.valid).toBeFalsy();
|
||||
el.click();
|
||||
el.dispatchEvent(new Event('click'));
|
||||
setTimeout(() => {
|
||||
expect(component.opened).toBeFalsy();
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
@ -0,0 +1,229 @@
|
||||
import { Component, EventEmitter, Output, ViewChild } from '@angular/core';
|
||||
import { Scanner } from "../scanner";
|
||||
import { NewScannerFormComponent } from "../new-scanner-form/new-scanner-form.component";
|
||||
import { ConfigScannerService } from "../config-scanner.service";
|
||||
import { ClrLoadingState } from "@clr/angular";
|
||||
import { finalize } from "rxjs/operators";
|
||||
import { InlineAlertComponent } from "../../../shared/inline-alert/inline-alert.component";
|
||||
import { MessageHandlerService } from "../../../shared/message-handler/message-handler.service";
|
||||
|
||||
@Component({
|
||||
selector: "new-scanner-modal",
|
||||
templateUrl: "new-scanner-modal.component.html",
|
||||
styleUrls: ['../../../common.scss']
|
||||
})
|
||||
export class NewScannerModalComponent {
|
||||
testMap: any = {};
|
||||
opened: boolean = false;
|
||||
@Output() notify = new EventEmitter<Scanner>();
|
||||
@ViewChild(NewScannerFormComponent, {static: true})
|
||||
newScannerFormComponent: NewScannerFormComponent;
|
||||
checkBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
onTesting: boolean = false;
|
||||
onSaving: boolean = false;
|
||||
isEdit: boolean = false;
|
||||
originValue: any;
|
||||
uid: string;
|
||||
editScanner: Scanner;
|
||||
@ViewChild(InlineAlertComponent, { static: false }) inlineAlert: InlineAlertComponent;
|
||||
constructor(
|
||||
private configScannerService: ConfigScannerService,
|
||||
private msgHandler: MessageHandlerService
|
||||
) {}
|
||||
open(): void {
|
||||
// reset
|
||||
this.opened = true;
|
||||
this.inlineAlert.close();
|
||||
this.testMap = {};
|
||||
this.newScannerFormComponent.showEndpointError = false;
|
||||
this.newScannerFormComponent.newScannerForm.reset({auth: "None"});
|
||||
}
|
||||
close(): void {
|
||||
this.opened = false;
|
||||
}
|
||||
create(): void {
|
||||
this.onSaving = true;
|
||||
this.saveBtnState = ClrLoadingState.LOADING;
|
||||
let scanner: Scanner = new Scanner();
|
||||
let value = this.newScannerFormComponent.newScannerForm.value;
|
||||
scanner.name = value.name;
|
||||
scanner.description = value.description;
|
||||
scanner.url = value.url;
|
||||
if (value.auth === "None") {
|
||||
scanner.auth = "";
|
||||
} else if (value.auth === "Basic") {
|
||||
scanner.auth = value.auth;
|
||||
scanner.access_credential = value.accessCredential.username + ":" + value.accessCredential.password;
|
||||
} else if (value.auth === "APIKey") {
|
||||
scanner.auth = value.auth;
|
||||
scanner.access_credential = value.accessCredential.apiKey;
|
||||
} else {
|
||||
scanner.auth = value.auth;
|
||||
scanner.access_credential = value.accessCredential.token;
|
||||
}
|
||||
scanner.skip_certVerify = !!value.skipCertVerify;
|
||||
this.configScannerService.addScanner(scanner)
|
||||
.pipe(finalize(() => this.onSaving = false))
|
||||
.subscribe(response => {
|
||||
this.close();
|
||||
this.msgHandler.showSuccess("ADD_SUCCESS");
|
||||
this.notify.emit();
|
||||
this.saveBtnState = ClrLoadingState.SUCCESS;
|
||||
}, error => {
|
||||
this.inlineAlert.showInlineError(error);
|
||||
this.saveBtnState = ClrLoadingState.ERROR;
|
||||
});
|
||||
}
|
||||
get hasPassedTest(): boolean {
|
||||
return this.testMap[this.newScannerFormComponent.newScannerForm.get('url').value];
|
||||
}
|
||||
get canTestEndpoint(): boolean {
|
||||
return !this.onTesting
|
||||
&& this.newScannerFormComponent
|
||||
&& !this.newScannerFormComponent.checkOnGoing
|
||||
&& this.newScannerFormComponent.newScannerForm.get('name').valid
|
||||
&& !this.newScannerFormComponent.checkEndpointOnGoing
|
||||
&& this.newScannerFormComponent.newScannerForm.get('url').valid;
|
||||
}
|
||||
get valid(): boolean {
|
||||
if (this.onSaving
|
||||
|| this.newScannerFormComponent.isNameExisting
|
||||
|| this.newScannerFormComponent.isEndpointUrlExisting
|
||||
|| this.onTesting
|
||||
|| !this.newScannerFormComponent
|
||||
|| this.newScannerFormComponent.checkOnGoing
|
||||
|| this.newScannerFormComponent.checkEndpointOnGoing) {
|
||||
return false;
|
||||
}
|
||||
if (this.newScannerFormComponent.newScannerForm.get('name').invalid) {
|
||||
return false;
|
||||
}
|
||||
if (this.newScannerFormComponent.newScannerForm.get('url').invalid) {
|
||||
return false;
|
||||
}
|
||||
if (this.newScannerFormComponent.newScannerForm.get('auth').value === "Basic") {
|
||||
return this.newScannerFormComponent.newScannerForm.get('accessCredential').get('username').valid
|
||||
&& this.newScannerFormComponent.newScannerForm.get('accessCredential').get('password').valid;
|
||||
}
|
||||
if (this.newScannerFormComponent.newScannerForm.get('auth').value === "Bearer") {
|
||||
return this.newScannerFormComponent.newScannerForm.get('accessCredential').get('token').valid;
|
||||
}
|
||||
if (this.newScannerFormComponent.newScannerForm.get('auth').value === "APIKey") {
|
||||
return this.newScannerFormComponent.newScannerForm.get('accessCredential').get('apiKey').valid;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
get validForSaving() {
|
||||
return this.valid && this.hasChange();
|
||||
}
|
||||
hasChange(): boolean {
|
||||
if (this.originValue.name !== this.newScannerFormComponent.newScannerForm.get('name').value) {
|
||||
return true;
|
||||
}
|
||||
if (this.originValue.description !== this.newScannerFormComponent.newScannerForm.get('description').value) {
|
||||
return true;
|
||||
}
|
||||
if (this.originValue.url !== this.newScannerFormComponent.newScannerForm.get('url').value) {
|
||||
return true;
|
||||
}
|
||||
if (this.originValue.auth !== this.newScannerFormComponent.newScannerForm.get('auth').value) {
|
||||
return true;
|
||||
}
|
||||
if (this.originValue.skipCertVerify !== this.newScannerFormComponent.newScannerForm.get('skipCertVerify').value) {
|
||||
return true;
|
||||
}
|
||||
if (this.originValue.auth === "Basic") {
|
||||
if (this.originValue.accessCredential.username !==
|
||||
this.newScannerFormComponent.newScannerForm.get('accessCredential').get('username').value) {
|
||||
return true;
|
||||
}
|
||||
if (this.originValue.accessCredential.password !==
|
||||
this.newScannerFormComponent.newScannerForm.get('accessCredential').get('password').value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.originValue.auth === "Bearer") {
|
||||
if (this.originValue.accessCredential.token !==
|
||||
this.newScannerFormComponent.newScannerForm.get('accessCredential').get('token').value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.originValue.auth === "APIKey") {
|
||||
if (this.originValue.accessCredential.apiKey !==
|
||||
this.newScannerFormComponent.newScannerForm.get('accessCredential').get('apiKey').value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
onTestEndpoint() {
|
||||
this.onTesting = true;
|
||||
this.checkBtnState = ClrLoadingState.LOADING;
|
||||
let scanner: Scanner = new Scanner();
|
||||
let value = this.newScannerFormComponent.newScannerForm.value;
|
||||
scanner.name = value.name;
|
||||
scanner.description = value.description;
|
||||
scanner.url = value.url;
|
||||
if (value.auth === "None") {
|
||||
scanner.auth = "";
|
||||
} else if (value.auth === "Basic") {
|
||||
scanner.auth = value.auth;
|
||||
scanner.access_credential = value.accessCredential.username + ":" + value.accessCredential.password;
|
||||
} else if (value.auth === "APIKey") {
|
||||
scanner.auth = value.auth;
|
||||
scanner.access_credential = value.accessCredential.apiKey;
|
||||
} else {
|
||||
scanner.auth = value.auth;
|
||||
scanner.access_credential = value.accessCredential.token;
|
||||
}
|
||||
scanner.skip_certVerify = !!value.skipCertVerify;
|
||||
this.configScannerService.testEndpointUrl(scanner)
|
||||
.pipe(finalize(() => this.onTesting = false))
|
||||
.subscribe(response => {
|
||||
this.inlineAlert.showInlineSuccess({
|
||||
message: "TEST_PASS"
|
||||
});
|
||||
this.checkBtnState = ClrLoadingState.SUCCESS;
|
||||
this.testMap[this.newScannerFormComponent.newScannerForm.get('url').value] = true;
|
||||
}, error => {
|
||||
this.inlineAlert.showInlineError({
|
||||
message: "TEST_FAILED"
|
||||
});
|
||||
this.checkBtnState = ClrLoadingState.ERROR;
|
||||
});
|
||||
}
|
||||
save() {
|
||||
this.onSaving = true;
|
||||
this.saveBtnState = ClrLoadingState.LOADING;
|
||||
let value = this.newScannerFormComponent.newScannerForm.value;
|
||||
this.editScanner.name = value.name;
|
||||
this.editScanner.description = value.description;
|
||||
this.editScanner.url = value.url;
|
||||
if (value.auth === "None") {
|
||||
this.editScanner.auth = "";
|
||||
} else if (value.auth === "Basic") {
|
||||
this.editScanner.auth = value.auth;
|
||||
this.editScanner.access_credential = value.accessCredential.username + ":" + value.accessCredential.password;
|
||||
} else if (value.auth === "APIKey") {
|
||||
this.editScanner.auth = value.auth;
|
||||
this.editScanner.access_credential = value.accessCredential.apiKey;
|
||||
} else {
|
||||
this.editScanner.auth = value.auth;
|
||||
this.editScanner.access_credential = value.accessCredential.token;
|
||||
}
|
||||
this.editScanner.skip_certVerify = !!value.skipCertVerify;
|
||||
this.editScanner.uuid = this.uid;
|
||||
this.configScannerService.updateScanner(this.editScanner)
|
||||
.pipe(finalize(() => this.onSaving = false))
|
||||
.subscribe(response => {
|
||||
this.close();
|
||||
this.msgHandler.showSuccess("UPDATE_SUCCESS");
|
||||
this.notify.emit();
|
||||
this.saveBtnState = ClrLoadingState.SUCCESS;
|
||||
}, error => {
|
||||
this.inlineAlert.showInlineError(error);
|
||||
this.saveBtnState = ClrLoadingState.ERROR;
|
||||
});
|
||||
}
|
||||
}
|
16
src/portal/src/app/config/scanner/scanner-metadata.ts
Normal file
16
src/portal/src/app/config/scanner/scanner-metadata.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export class ScannerMetadata {
|
||||
scanner?: {
|
||||
name?: string;
|
||||
vendor?: string;
|
||||
version?: string;
|
||||
};
|
||||
capabilities?: [{
|
||||
consumes_mime_types?: Array<string>;
|
||||
produces_mime_types?: Array<string>;
|
||||
}];
|
||||
properties?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
constructor() {
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { ClarityModule } from "@clr/angular";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { ConfigScannerService } from "../config-scanner.service";
|
||||
import { of } from "rxjs";
|
||||
import { ScannerMetadataComponent } from "./scanner-metadata.component";
|
||||
import { ErrorHandler } from "@harbor/ui";
|
||||
|
||||
describe('ScannerMetadataComponent', () => {
|
||||
let mockScannerMetadata = {
|
||||
scanner: {
|
||||
name: 'test1',
|
||||
vendor: 'clair',
|
||||
version: '1.0.1',
|
||||
},
|
||||
capabilities: [{
|
||||
consumes_mime_types: ['consumes_mime_types'],
|
||||
produces_mime_types: ['consumes_mime_types']
|
||||
}]
|
||||
};
|
||||
let component: ScannerMetadataComponent;
|
||||
let fixture: ComponentFixture<ScannerMetadataComponent>;
|
||||
let fakedConfigScannerService = {
|
||||
getScannerMetadata() {
|
||||
return of(mockScannerMetadata);
|
||||
}
|
||||
};
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
BrowserAnimationsModule,
|
||||
ClarityModule,
|
||||
],
|
||||
declarations: [
|
||||
ScannerMetadataComponent
|
||||
],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
{ provide: ConfigScannerService, useValue: fakedConfigScannerService },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ScannerMetadataComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('should get metadata', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('#scannerMetadata-name');
|
||||
expect(el.textContent).toEqual('test1');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,50 @@
|
||||
import {
|
||||
Component, Inject, Input, LOCALE_ID,
|
||||
OnInit
|
||||
} from "@angular/core";
|
||||
import { ConfigScannerService } from "../config-scanner.service";
|
||||
import { finalize } from "rxjs/operators";
|
||||
import { ErrorHandler } from "@harbor/ui";
|
||||
import { ScannerMetadata } from "../scanner-metadata";
|
||||
import { DatePipe } from "@angular/common";
|
||||
|
||||
@Component({
|
||||
selector: 'scanner-metadata',
|
||||
templateUrl: 'scanner-metadata.html',
|
||||
styleUrls: ['./scanner-metadata.scss']
|
||||
})
|
||||
export class ScannerMetadataComponent implements OnInit {
|
||||
@Input() uid: string;
|
||||
loading: boolean = false;
|
||||
scannerMetadata: ScannerMetadata;
|
||||
constructor(private configScannerService: ConfigScannerService,
|
||||
private errorHandler: ErrorHandler,
|
||||
@Inject(LOCALE_ID) private _locale: string) {
|
||||
}
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
this.configScannerService.getScannerMetadata(this.uid)
|
||||
.pipe(finalize(() => this.loading = false))
|
||||
.subscribe(response => {
|
||||
this.scannerMetadata = response;
|
||||
}, error => {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
parseDate(str: string): string {
|
||||
try {
|
||||
if (str === new Date(str).toISOString()) {
|
||||
return new DatePipe(this._locale).transform(str, 'short');
|
||||
}
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
toString(arr: string[]) {
|
||||
if (arr && arr.length > 0) {
|
||||
return "[" + arr.join(" , ") + "]";
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<div [clrLoading]="loading" class="ml-2">
|
||||
<div class="clr-control-label">{{'SCANNER.SCANNER_COLON' | translate}}</div>
|
||||
<div class="ml-1">
|
||||
<span>{{'SCANNER.NAME_COLON' | translate}}</span>
|
||||
<span id="scannerMetadata-name" class="ml-1">{{scannerMetadata?.scanner?.name}}</span>
|
||||
</div>
|
||||
<div class="ml-1">
|
||||
<span>{{'SCANNER.VENDOR_COLON' | translate}}</span>
|
||||
<span class="ml-1">{{scannerMetadata?.scanner?.vendor}}</span>
|
||||
</div>
|
||||
<div class="ml-1">
|
||||
<span>{{'SCANNER.VERSION_COLON' | translate}}</span>
|
||||
<span class="ml-1">{{scannerMetadata?.scanner?.version}}</span>
|
||||
</div>
|
||||
<div class="clr-control-label">{{'SCANNER.CAPABILITIES' | translate}}</div>
|
||||
<div class="ml-1">
|
||||
<span>{{'SCANNER.CONSUMES_MIME_TYPES_COLON' | translate}}</span>
|
||||
<span class="ml-1" [innerHTML]="toString(scannerMetadata?.capabilities[0]?.consumes_mime_types)"></span>
|
||||
</div>
|
||||
<div class="ml-1">
|
||||
<span>{{'SCANNER.PRODUCTS_MIME_TYPES_COLON' | translate}}</span>
|
||||
<span class="ml-1" [innerHTML]="toString(scannerMetadata?.capabilities[0]?.produces_mime_types)"></span>
|
||||
</div>
|
||||
<div class="clr-control-label">{{'SCANNER.PROPERTIES' | translate}}</div>
|
||||
<div class="ml-1" *ngFor="let item of scannerMetadata?.properties | keyvalue">
|
||||
<span>{{item?.key}}:</span>
|
||||
<span class="ml-1">{{parseDate(item?.value)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
19
src/portal/src/app/config/scanner/scanner.ts
Normal file
19
src/portal/src/app/config/scanner/scanner.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export class Scanner {
|
||||
name?: string;
|
||||
description?: string;
|
||||
uuid?: string;
|
||||
url?: string;
|
||||
auth?: string;
|
||||
access_credential?: string;
|
||||
scanner?: string;
|
||||
disabled?: boolean;
|
||||
is_default?: boolean;
|
||||
skip_certVerify?: boolean;
|
||||
create_time?: any;
|
||||
update_time?: any;
|
||||
vendor?: string;
|
||||
version?: string;
|
||||
health?: boolean;
|
||||
constructor() {
|
||||
}
|
||||
}
|
@ -62,6 +62,7 @@ import { LicenseComponent } from './license/license.component';
|
||||
import { SummaryComponent } from './project/summary/summary.component';
|
||||
import { TagRetentionComponent } from './project/tag-retention/tag-retention.component';
|
||||
import { USERSTATICPERMISSION } from '@harbor/ui';
|
||||
import { ScannerComponent } from "./project/scanner/scanner.component";
|
||||
|
||||
const harborRoutes: Routes = [
|
||||
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
|
||||
@ -285,6 +286,16 @@ const harborRoutes: Routes = [
|
||||
}
|
||||
},
|
||||
component: WebhookComponent
|
||||
},
|
||||
{
|
||||
path: 'scanner',
|
||||
data: {
|
||||
permissionParam: {
|
||||
resource: USERSTATICPERMISSION.CONFIGURATION.KEY,
|
||||
action: USERSTATICPERMISSION.CONFIGURATION.VALUE.READ
|
||||
}
|
||||
},
|
||||
component: ScannerComponent
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -31,6 +31,9 @@
|
||||
<li class="nav-item" *ngIf="hasWebhookListPermission">
|
||||
<a class="nav-link" routerLink="webhook" routerLinkActive="active">{{'PROJECT_DETAIL.WEBHOOKS' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="isSessionValid && (hasConfigurationListPermission)">
|
||||
<a class="nav-link" routerLink="scanner" routerLinkActive="active">{{'SCANNER.SCANNER' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="isSessionValid && (hasConfigurationListPermission)">
|
||||
<a class="nav-link" routerLink="configs" routerLinkActive="active">{{'PROJECT_DETAIL.CONFIG' | translate}}</a>
|
||||
</li>
|
||||
|
@ -47,6 +47,8 @@ import { WebhookService } from './webhook/webhook.service';
|
||||
import { WebhookComponent } from './webhook/webhook.component';
|
||||
import { AddWebhookComponent } from './webhook/add-webhook/add-webhook.component';
|
||||
import { AddWebhookFormComponent } from './webhook/add-webhook-form/add-webhook-form.component';
|
||||
import { ScannerComponent } from "./scanner/scanner.component";
|
||||
import { ConfigScannerService } from "../config/scanner/config-scanner.service";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -76,9 +78,10 @@ import { AddWebhookFormComponent } from './webhook/add-webhook-form/add-webhook-
|
||||
WebhookComponent,
|
||||
AddWebhookComponent,
|
||||
AddWebhookFormComponent,
|
||||
ScannerComponent,
|
||||
],
|
||||
exports: [ProjectComponent, ListProjectComponent],
|
||||
providers: [ProjectRoutingResolver, MemberService, RobotService, TagRetentionService, WebhookService]
|
||||
providers: [ProjectRoutingResolver, MemberService, RobotService, TagRetentionService, WebhookService, ConfigScannerService]
|
||||
})
|
||||
export class ProjectModule {
|
||||
|
||||
|
97
src/portal/src/app/project/scanner/scanner.component.html
Normal file
97
src/portal/src/app/project/scanner/scanner.component.html
Normal file
@ -0,0 +1,97 @@
|
||||
<div *ngIf="loading" class="clr-row mt-2 center">
|
||||
<span class="spinner spinner-md"></span>
|
||||
</div>
|
||||
<div *ngIf="!loading" class="clr-form clr-form-horizontal">
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label name">{{'SCANNER.SCANNER' | translate}}</label>
|
||||
<div class="clr-control-container">
|
||||
<button *ngIf="scanners && scanners.length > 0" id="edit-scanner" class="btn btn-link edit" (click)="open()">{{'SCANNER.EDIT' | translate}}</button>
|
||||
<label *ngIf="!(scanners && scanners.length > 0)" class="name">{{'SCANNER.NOT_AVAILABLE' | translate}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="scanner">
|
||||
<div class="clr-form-control">
|
||||
<label for="select-full" class="clr-control-label">{{'SCANNER.NAME' | translate}}</label>
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<div class="clr-input-wrapper">
|
||||
<span id="scanner-name" class="scanner-name">{{scanner?.name}}</span>
|
||||
<span *ngIf="scanner?.disabled" class="label label-warning ml-1">{{'SCANNER.DISABLED' | translate}}</span>
|
||||
<span *ngIf="scanner?.health" class="label label-success ml-1">{{'SCANNER.HEALTHY' | translate}}</span>
|
||||
<span *ngIf="!scanner?.health" class="label label-danger ml-1">{{'SCANNER.Unhealthy' | translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control">
|
||||
<label class="clr-control-label">{{'SCANNER.ENDPOINT' | translate}}</label>
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input [ngModel]="scanner?.url" readonly class="clr-input width-240" type="text" id="scanner-endpoint"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control" *ngIf="scanner?.scanner">
|
||||
<label class="clr-control-label">{{'SCANNER.ADAPTER' | translate}}</label>
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input [ngModel]="scanner?.scanner" readonly class="clr-input width-240" type="text" id="scanner-scanner"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control" *ngIf="scanner?.vendor">
|
||||
<label class="clr-control-label">{{'SCANNER.VENDOR' | translate}}</label>
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input [ngModel]="scanner?.vendor" readonly class="clr-input width-240" type="text" id="scanner-vendor"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control" *ngIf="scanner?.version">
|
||||
<label class="clr-control-label">{{'SCANNER.VERSION' | translate}}</label>
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input [ngModel]="scanner?.version" readonly class="clr-input width-240" type="text" id="scanner-version"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<clr-modal [clrModalSize]="'xl'" [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="true" [clrModalClosable]="false">
|
||||
<h3 class="modal-title">{{'SCANNER.SELECT_SCANNER' | translate}}</h3>
|
||||
<div class="modal-body body-format">
|
||||
<inline-alert class="modal-title"></inline-alert>
|
||||
<clr-datagrid [(clrDgSingleSelected)]="selectedScanner">
|
||||
<clr-dg-column [clrDgField]="'name'">{{'SCANNER.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'url'">{{'SCANNER.ENDPOINT' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'SCANNER.HEALTH' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'SCANNER.DEFAULT' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'SCANNER.AUTH' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let scanner of scanners" [clrDgItem]="scanner">
|
||||
<clr-dg-cell>{{scanner.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{scanner.url}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span *ngIf="scanner.health" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
|
||||
<span *ngIf="!scanner.health" class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span *ngIf="scanner.is_default" class="label label-info">{{scanner.is_default}}</span>
|
||||
<span *ngIf="!scanner.is_default">{{scanner.is_default}}</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{scanner.auth?scanner.auth:('SCANNER.NONE'|translate)}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<span *ngIf="scanners?.length > 0">1 - {{scanners?.length}} {{'WEBHOOK.OF' | translate}} </span> {{scanners?.length}} {{'WEBHOOK.ITEMS' | translate}}
|
||||
<clr-dg-pagination [clrDgPageSize]="10"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="close()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" [clrLoading]="saveBtnState" class="btn btn-primary" [disabled]="!valid" (click)="save()">{{'BUTTON.OK' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
20
src/portal/src/app/project/scanner/scanner.component.scss
Normal file
20
src/portal/src/app/project/scanner/scanner.component.scss
Normal file
@ -0,0 +1,20 @@
|
||||
.scanner-name {
|
||||
height: 1rem;
|
||||
color: #000;
|
||||
display: inline-block;
|
||||
padding: 0 .25rem;
|
||||
max-height: 1rem;
|
||||
font-size: .541667rem;
|
||||
}
|
||||
.edit {
|
||||
margin-top: -5px;
|
||||
margin-left: -8px;
|
||||
font-size: 0.58rem;
|
||||
}
|
||||
.width-240 {
|
||||
width: 240px;
|
||||
}
|
||||
.center {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
77
src/portal/src/app/project/scanner/scanner.component.spec.ts
Normal file
77
src/portal/src/app/project/scanner/scanner.component.spec.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { ClarityModule } from "@clr/angular";
|
||||
import { of } from "rxjs";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
|
||||
import { ErrorHandler } from "@harbor/ui";
|
||||
import { ScannerComponent } from "./scanner.component";
|
||||
import { ConfigScannerService } from "../../config/scanner/config-scanner.service";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
xdescribe('ScannerComponent', () => {
|
||||
let mockScanner1 = {
|
||||
name: 'test1',
|
||||
description: 'just a sample',
|
||||
version: '1.0.0',
|
||||
url: 'http://168.0.0.1'
|
||||
};
|
||||
let component: ScannerComponent;
|
||||
let fixture: ComponentFixture<ScannerComponent>;
|
||||
let fakedConfigScannerService = {
|
||||
getProjectScanner() {
|
||||
return of(mockScanner1);
|
||||
},
|
||||
getScanners() {
|
||||
return of([mockScanner1]);
|
||||
}
|
||||
};
|
||||
let fakedRoute = {
|
||||
snapshot: {
|
||||
parent: {
|
||||
params: {
|
||||
id: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
BrowserAnimationsModule,
|
||||
ClarityModule,
|
||||
],
|
||||
declarations: [ ScannerComponent ],
|
||||
providers: [
|
||||
TranslateService,
|
||||
MessageHandlerService,
|
||||
ErrorHandler,
|
||||
{provide: ActivatedRoute, useValue: fakedRoute},
|
||||
{ provide: ConfigScannerService, useValue: fakedConfigScannerService },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ScannerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should creat', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
it('should get scanner and render', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('#scanner-name');
|
||||
expect(el.textContent.trim).toEqual('test1');
|
||||
});
|
||||
});
|
||||
it('should get scanners and edit button is available', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('#edit-scanner');
|
||||
expect(el).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
108
src/portal/src/app/project/scanner/scanner.component.ts
Normal file
108
src/portal/src/app/project/scanner/scanner.component.ts
Normal file
@ -0,0 +1,108 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||
import { ConfigScannerService } from "../../config/scanner/config-scanner.service";
|
||||
import { Scanner } from "../../config/scanner/scanner";
|
||||
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
|
||||
import { ErrorHandler } from "@harbor/ui";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { ClrLoadingState } from "@clr/angular";
|
||||
import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component";
|
||||
import { finalize } from "rxjs/operators";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'scanner',
|
||||
templateUrl: './scanner.component.html',
|
||||
styleUrls: ['./scanner.component.scss']
|
||||
})
|
||||
export class ScannerComponent implements OnInit {
|
||||
loading: boolean = false;
|
||||
scanners: Scanner[];
|
||||
scanner: Scanner;
|
||||
projectId: number;
|
||||
opened: boolean = false;
|
||||
selectedScanner: Scanner;
|
||||
saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
onSaving: boolean = false;
|
||||
@ViewChild(InlineAlertComponent, { static: false }) inlineAlert: InlineAlertComponent;
|
||||
constructor( private configScannerService: ConfigScannerService,
|
||||
private msgHandler: MessageHandlerService,
|
||||
private errorHandler: ErrorHandler,
|
||||
private route: ActivatedRoute,
|
||||
) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.projectId = +this.route.snapshot.parent.params['id'];
|
||||
this.init();
|
||||
}
|
||||
init() {
|
||||
this.getScanner();
|
||||
this.getScanners();
|
||||
}
|
||||
getScanner() {
|
||||
this.configScannerService.getProjectScanner(this.projectId)
|
||||
.subscribe(response => {
|
||||
if (response && "{}" !== JSON.stringify(response)) {
|
||||
this.scanner = response;
|
||||
}
|
||||
}, error => {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
getScanners() {
|
||||
this.loading = true;
|
||||
this.configScannerService.getScanners()
|
||||
.pipe(finalize(() => this.loading = false))
|
||||
.subscribe(response => {
|
||||
if (response && response.length > 0) {
|
||||
this.scanners = response.filter(scanner => {
|
||||
return !scanner.disabled;
|
||||
});
|
||||
}
|
||||
}, error => {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
close() {
|
||||
this.opened = false;
|
||||
this.selectedScanner = null;
|
||||
}
|
||||
open() {
|
||||
this.opened = true;
|
||||
this.inlineAlert.close();
|
||||
this.scanners.forEach(s => {
|
||||
if (this.scanner && s.uuid === this.scanner.uuid) {
|
||||
this.selectedScanner = s;
|
||||
}
|
||||
});
|
||||
}
|
||||
get valid(): boolean {
|
||||
return this.selectedScanner
|
||||
&& !(this.scanner && this.scanner.uuid === this.selectedScanner.uuid);
|
||||
}
|
||||
save() {
|
||||
this.saveBtnState = ClrLoadingState.LOADING;
|
||||
this.configScannerService.updateProjectScanner(this.projectId, this.selectedScanner.uuid)
|
||||
.subscribe(response => {
|
||||
this.close();
|
||||
this.msgHandler.showSuccess('Update Success');
|
||||
this.getScanner();
|
||||
this.saveBtnState = ClrLoadingState.SUCCESS;
|
||||
}, error => {
|
||||
this.inlineAlert.showInlineError(error);
|
||||
this.saveBtnState = ClrLoadingState.ERROR;
|
||||
});
|
||||
}
|
||||
}
|
@ -41,7 +41,8 @@ export const enum ConfirmationTargets {
|
||||
CONFIG_TAB,
|
||||
HELM_CHART,
|
||||
HELM_CHART_VERSION,
|
||||
WEBHOOK
|
||||
WEBHOOK,
|
||||
SCANNER
|
||||
}
|
||||
|
||||
export const enum ActionType {
|
||||
|
@ -922,11 +922,10 @@
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"STOPPED": "Not Scanned",
|
||||
"OTHER_STATUS": "Not Scanned",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Scanning",
|
||||
"UNKNOWN": "Unknown"
|
||||
"SCANNING": "Scanning"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "We couldn't find any scanning results!",
|
||||
@ -947,6 +946,7 @@
|
||||
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability package found"
|
||||
},
|
||||
"SEVERITY": {
|
||||
"CRITICAL": "Critical",
|
||||
"HIGH": "High",
|
||||
"MEDIUM": "Medium",
|
||||
"LOW": "Low",
|
||||
@ -1224,6 +1224,62 @@
|
||||
"DAYS_LARGE": "Parameter \"DAYS\" is too large",
|
||||
"EXECUTION_TYPE": "Execution Type",
|
||||
"ACTION": "ACTION"
|
||||
},
|
||||
"SCANNER": {
|
||||
"DELETION_SUMMARY": "Do you want to delete scanner {{param}}?",
|
||||
"SKIP_CERT_VERIFY": "Check this box to skip certificate verification when the remote registry uses a self-signed or untrusted certificate.",
|
||||
"NAME": "Name",
|
||||
"NAME_EXISTS": "Name already exists",
|
||||
"NAME_REQUIRED": "Name is required",
|
||||
"NAME_REX": "Name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
|
||||
"DESCRIPTION": "Description",
|
||||
"ENDPOINT": "Endpoint",
|
||||
"ENDPOINT_EXISTS": "EndpointUrl already exists",
|
||||
"ENDPOINT_REQUIRED": "EndpointUrl is required",
|
||||
"ILLEGAL_ENDPOINT": "EndpointUrl is illegal",
|
||||
"AUTH": "Authorization",
|
||||
"NONE": "None",
|
||||
"BASIC": "Basic",
|
||||
"BEARER": "Bearer",
|
||||
"API_KEY": "APIKey",
|
||||
"USERNAME": "Username",
|
||||
"USERNAME_REQUIRED": "Username is required",
|
||||
"PASSWORD": "Password",
|
||||
"PASSWORD_REQUIRED": "Password is required",
|
||||
"TOKEN": "Token",
|
||||
"TOKEN_REQUIRED": "Token is required",
|
||||
"API_KEY_REQUIRED": "APIKey is required",
|
||||
"SKIP": "Skip Certificate Verification",
|
||||
"ADD_SCANNER": "Add Scanner",
|
||||
"EDIT_SCANNER": "Edit Scanner",
|
||||
"TEST_CONNECTION": "TEST CONNECTION",
|
||||
"ADD_SUCCESS": "Added successfully",
|
||||
"TEST_PASS": "Test passed",
|
||||
"TEST_FAILED": "Test failed",
|
||||
"UPDATE_SUCCESS": "Updated successfully",
|
||||
"SCANNER_COLON": "Scanner:",
|
||||
"NAME_COLON": "Name:",
|
||||
"VENDOR_COLON": "Vendor:",
|
||||
"VERSION_COLON": "Version:",
|
||||
"CAPABILITIES": "Capabilities",
|
||||
"CONSUMES_MIME_TYPES_COLON": "Consumes Mime Types:",
|
||||
"PRODUCTS_MIME_TYPES_COLON": "Produces Mime Types:",
|
||||
"PROPERTIES": "Properties",
|
||||
"NEW_SCANNER": "NEW SCANNER",
|
||||
"SET_AS_DEFAULT": "SET AS DEFAULT",
|
||||
"HEALTH": "Health",
|
||||
"DISABLED": "Disabled",
|
||||
"NO_SCANNER": "Can not find any scanner",
|
||||
"DEFAULT": "Default",
|
||||
"HEALTHY": "Healthy",
|
||||
"UNHEALTHY": "Unhealthy",
|
||||
"SCANNERS": "Scanners",
|
||||
"SCANNER": "Scanner",
|
||||
"EDIT": "Edit",
|
||||
"NOT_AVAILABLE": "Not Available",
|
||||
"ADAPTER": "Adapter",
|
||||
"VENDOR": "Vendor",
|
||||
"VERSION": "Version",
|
||||
"SELECT_SCANNER": "Select Scanner"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -921,11 +921,10 @@
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"STOPPED": "Not Scanned",
|
||||
"OTHER_STATUS": "Not Scanned",
|
||||
"QUEUED": "Queued",
|
||||
"ERROR": "View Log",
|
||||
"SCANNING": "Scanning",
|
||||
"UNKNOWN": "Unknown"
|
||||
"SCANNING": "Scanning"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "We couldn't find any scanning results!",
|
||||
@ -946,6 +945,7 @@
|
||||
"TOOLTIPS_TITLE_ZERO": "No se encontró ningún paquete de vulnerabilidad reconocible"
|
||||
},
|
||||
"SEVERITY": {
|
||||
"CRITICAL": "Critical",
|
||||
"HIGH": "High",
|
||||
"MEDIUM": "Medium",
|
||||
"LOW": "Low",
|
||||
@ -1221,6 +1221,62 @@
|
||||
"DAYS_LARGE": "Parameter \"DAYS\" is too large",
|
||||
"EXECUTION_TYPE": "Execution Type",
|
||||
"ACTION": "ACTION"
|
||||
},
|
||||
"SCANNER": {
|
||||
"DELETION_SUMMARY": "Do you want to delete scanner {{param}}?",
|
||||
"SKIP_CERT_VERIFY": "Check this box to skip certificate verification when the remote registry uses a self-signed or untrusted certificate.",
|
||||
"NAME": "Name",
|
||||
"NAME_EXISTS": "Name already exists",
|
||||
"NAME_REQUIRED": "Name is required",
|
||||
"NAME_REX": "Name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
|
||||
"DESCRIPTION": "Description",
|
||||
"ENDPOINT": "Endpoint",
|
||||
"ENDPOINT_EXISTS": "EndpointUrl already exists",
|
||||
"ENDPOINT_REQUIRED": "EndpointUrl is required",
|
||||
"ILLEGAL_ENDPOINT": "EndpointUrl is illegal",
|
||||
"AUTH": "Authorization",
|
||||
"NONE": "None",
|
||||
"BASIC": "Basic",
|
||||
"BEARER": "Bearer",
|
||||
"API_KEY": "APIKey",
|
||||
"USERNAME": "Username",
|
||||
"USERNAME_REQUIRED": "Username is required",
|
||||
"PASSWORD": "Password",
|
||||
"PASSWORD_REQUIRED": "Password is required",
|
||||
"TOKEN": "Token",
|
||||
"TOKEN_REQUIRED": "Token is required",
|
||||
"API_KEY_REQUIRED": "APIKey is required",
|
||||
"SKIP": "Skip Certificate Verification",
|
||||
"ADD_SCANNER": "Add Scanner",
|
||||
"EDIT_SCANNER": "Edit Scanner",
|
||||
"TEST_CONNECTION": "TEST CONNECTION",
|
||||
"ADD_SUCCESS": "Added successfully",
|
||||
"TEST_PASS": "Test passed",
|
||||
"TEST_FAILED": "Test failed",
|
||||
"UPDATE_SUCCESS": "Updated successfully",
|
||||
"SCANNER_COLON": "Scanner:",
|
||||
"NAME_COLON": "Name:",
|
||||
"VENDOR_COLON": "Vendor:",
|
||||
"VERSION_COLON": "Version:",
|
||||
"CAPABILITIES": "Capabilities",
|
||||
"CONSUMES_MIME_TYPES_COLON": "Consumes Mime Types:",
|
||||
"PRODUCTS_MIME_TYPES_COLON": "Produces Mime Types:",
|
||||
"PROPERTIES": "Properties",
|
||||
"NEW_SCANNER": "NEW SCANNER",
|
||||
"SET_AS_DEFAULT": "SET AS DEFAULT",
|
||||
"HEALTH": "Health",
|
||||
"DISABLED": "Disabled",
|
||||
"NO_SCANNER": "Can not find any scanner",
|
||||
"DEFAULT": "Default",
|
||||
"HEALTHY": "Healthy",
|
||||
"UNHEALTHY": "Unhealthy",
|
||||
"SCANNERS": "Scanners",
|
||||
"SCANNER": "Scanner",
|
||||
"EDIT": "Edit",
|
||||
"NOT_AVAILABLE": "Not Available",
|
||||
"ADAPTER": "Adapter",
|
||||
"VENDOR": "Vendor",
|
||||
"VERSION": "Version",
|
||||
"SELECT_SCANNER": "Select Scanner"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -895,11 +895,10 @@
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"STOPPED": "Non Analysé",
|
||||
"OTHER_STATUS": "Non Analysé",
|
||||
"QUEUED": "En fil d'attente",
|
||||
"ERROR": "Voir le Log",
|
||||
"SCANNING": "En cours d'analyse",
|
||||
"UNKNOWN": "Inconnu"
|
||||
"SCANNING": "En cours d'analyse"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "Nous n'avons pas trouvé de résultats d'analyse !",
|
||||
@ -920,6 +919,7 @@
|
||||
"TOOLTIPS_TITLE_ZERO": "Aucun paquet de vulnérabilité connue trouvé"
|
||||
},
|
||||
"SEVERITY": {
|
||||
"CRITICAL": "Critique",
|
||||
"HIGH": "Haut",
|
||||
"MEDIUM": "Moyen",
|
||||
"LOW": "Bas",
|
||||
@ -1193,6 +1193,62 @@
|
||||
"DAYS_LARGE": "Parameter \"DAYS\" is too large",
|
||||
"EXECUTION_TYPE": "Execution Type",
|
||||
"ACTION": "ACTION"
|
||||
},
|
||||
"SCANNER": {
|
||||
"DELETION_SUMMARY": "Do you want to delete scanner {{param}}?",
|
||||
"SKIP_CERT_VERIFY": "Check this box to skip certificate verification when the remote registry uses a self-signed or untrusted certificate.",
|
||||
"NAME": "Name",
|
||||
"NAME_EXISTS": "Name already exists",
|
||||
"NAME_REQUIRED": "Name is required",
|
||||
"NAME_REX": "Name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
|
||||
"DESCRIPTION": "Description",
|
||||
"ENDPOINT": "Endpoint",
|
||||
"ENDPOINT_EXISTS": "EndpointUrl already exists",
|
||||
"ENDPOINT_REQUIRED": "EndpointUrl is required",
|
||||
"ILLEGAL_ENDPOINT": "EndpointUrl is illegal",
|
||||
"AUTH": "Authorization",
|
||||
"NONE": "None",
|
||||
"BASIC": "Basic",
|
||||
"BEARER": "Bearer",
|
||||
"API_KEY": "APIKey",
|
||||
"USERNAME": "Username",
|
||||
"USERNAME_REQUIRED": "Username is required",
|
||||
"PASSWORD": "Password",
|
||||
"PASSWORD_REQUIRED": "Password is required",
|
||||
"TOKEN": "Token",
|
||||
"TOKEN_REQUIRED": "Token is required",
|
||||
"API_KEY_REQUIRED": "APIKey is required",
|
||||
"SKIP": "Skip Certificate Verification",
|
||||
"ADD_SCANNER": "Add Scanner",
|
||||
"EDIT_SCANNER": "Edit Scanner",
|
||||
"TEST_CONNECTION": "TEST CONNECTION",
|
||||
"ADD_SUCCESS": "Added successfully",
|
||||
"TEST_PASS": "Test passed",
|
||||
"TEST_FAILED": "Test failed",
|
||||
"UPDATE_SUCCESS": "Updated successfully",
|
||||
"SCANNER_COLON": "Scanner:",
|
||||
"NAME_COLON": "Name:",
|
||||
"VENDOR_COLON": "Vendor:",
|
||||
"VERSION_COLON": "Version:",
|
||||
"CAPABILITIES": "Capabilities",
|
||||
"CONSUMES_MIME_TYPES_COLON": "Consumes Mime Types:",
|
||||
"PRODUCTS_MIME_TYPES_COLON": "Produces Mime Types:",
|
||||
"PROPERTIES": "Properties",
|
||||
"NEW_SCANNER": "NEW SCANNER",
|
||||
"SET_AS_DEFAULT": "SET AS DEFAULT",
|
||||
"HEALTH": "Health",
|
||||
"DISABLED": "Disabled",
|
||||
"NO_SCANNER": "Can not find any scanner",
|
||||
"DEFAULT": "Default",
|
||||
"HEALTHY": "Healthy",
|
||||
"UNHEALTHY": "Unhealthy",
|
||||
"SCANNERS": "Scanners",
|
||||
"SCANNER": "Scanner",
|
||||
"EDIT": "Edit",
|
||||
"NOT_AVAILABLE": "Not Available",
|
||||
"ADAPTER": "Adapter",
|
||||
"VENDOR": "Vendor",
|
||||
"VERSION": "Version",
|
||||
"SELECT_SCANNER": "Select Scanner"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -916,11 +916,10 @@
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"STOPPED": "Não analisado",
|
||||
"OTHER_STATUS": "Não analisado",
|
||||
"QUEUED": "Solicitado",
|
||||
"ERROR": "Visualizar Log",
|
||||
"SCANNING": "Analisando",
|
||||
"UNKNOWN": "Desconhecido"
|
||||
"SCANNING": "Analisando"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "Não foi possível encontrar nenhum resultado de análise!",
|
||||
@ -941,6 +940,7 @@
|
||||
"TOOLTIPS_TITLE_ZERO": "Nenhum pacote vulnerável reconhecido foi encontrado"
|
||||
},
|
||||
"SEVERITY": {
|
||||
"CRITICAL": "Crítico",
|
||||
"HIGH": "Alta",
|
||||
"MEDIUM": "Média",
|
||||
"LOW": "Baixa",
|
||||
@ -1218,7 +1218,63 @@
|
||||
"DAYS_LARGE": "Parameter \"DAYS\" is too large",
|
||||
"EXECUTION_TYPE": "Execution Type",
|
||||
"ACTION": "ACTION"
|
||||
},
|
||||
"SCANNER": {
|
||||
"DELETION_SUMMARY": "Do you want to delete scanner {{param}}?",
|
||||
"SKIP_CERT_VERIFY": "Check this box to skip certificate verification when the remote registry uses a self-signed or untrusted certificate.",
|
||||
"NAME": "Name",
|
||||
"NAME_EXISTS": "Name already exists",
|
||||
"NAME_REQUIRED": "Name is required",
|
||||
"NAME_REX": "Name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
|
||||
"DESCRIPTION": "Description",
|
||||
"ENDPOINT": "Endpoint",
|
||||
"ENDPOINT_EXISTS": "EndpointUrl already exists",
|
||||
"ENDPOINT_REQUIRED": "EndpointUrl is required",
|
||||
"ILLEGAL_ENDPOINT": "EndpointUrl is illegal",
|
||||
"AUTH": "Authorization",
|
||||
"NONE": "None",
|
||||
"BASIC": "Basic",
|
||||
"BEARER": "Bearer",
|
||||
"API_KEY": "APIKey",
|
||||
"USERNAME": "Username",
|
||||
"USERNAME_REQUIRED": "Username is required",
|
||||
"PASSWORD": "Password",
|
||||
"PASSWORD_REQUIRED": "Password is required",
|
||||
"TOKEN": "Token",
|
||||
"TOKEN_REQUIRED": "Token is required",
|
||||
"API_KEY_REQUIRED": "APIKey is required",
|
||||
"SKIP": "Skip Certificate Verification",
|
||||
"ADD_SCANNER": "Add Scanner",
|
||||
"EDIT_SCANNER": "Edit Scanner",
|
||||
"TEST_CONNECTION": "TEST CONNECTION",
|
||||
"ADD_SUCCESS": "Added successfully",
|
||||
"TEST_PASS": "Test passed",
|
||||
"TEST_FAILED": "Test failed",
|
||||
"UPDATE_SUCCESS": "Updated successfully",
|
||||
"SCANNER_COLON": "Scanner:",
|
||||
"NAME_COLON": "Name:",
|
||||
"VENDOR_COLON": "Vendor:",
|
||||
"VERSION_COLON": "Version:",
|
||||
"CAPABILITIES": "Capabilities",
|
||||
"CONSUMES_MIME_TYPES_COLON": "Consumes Mime Types:",
|
||||
"PRODUCTS_MIME_TYPES_COLON": "Produces Mime Types:",
|
||||
"PROPERTIES": "Properties",
|
||||
"NEW_SCANNER": "NEW SCANNER",
|
||||
"SET_AS_DEFAULT": "SET AS DEFAULT",
|
||||
"HEALTH": "Health",
|
||||
"DISABLED": "Disabled",
|
||||
"NO_SCANNER": "Can not find any scanner",
|
||||
"DEFAULT": "Default",
|
||||
"HEALTHY": "Healthy",
|
||||
"UNHEALTHY": "Unhealthy",
|
||||
"SCANNERS": "Scanners",
|
||||
"SCANNER": "Scanner",
|
||||
"EDIT": "Edit",
|
||||
"NOT_AVAILABLE": "Not Available",
|
||||
"ADAPTER": "Adapter",
|
||||
"VENDOR": "Vendor",
|
||||
"VERSION": "Version",
|
||||
"SELECT_SCANNER": "Select Scanner"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -921,11 +921,10 @@
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"STOPPED": "Taranmadı",
|
||||
"OTHER_STATUS": "Taranmadı",
|
||||
"QUEUED": "Sıraya alındı",
|
||||
"ERROR": "Günlüğü Görüntüle",
|
||||
"SCANNING": "Taranıyor",
|
||||
"UNKNOWN": "Bilinmeyen"
|
||||
"SCANNING": "Taranıyor"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "Herhangi bir tarama sonucu bulamadık!",
|
||||
@ -946,6 +945,7 @@
|
||||
"TOOLTIPS_TITLE_ZERO": "Tanınabilir bir güvenlik açığı paketi bulunamadı"
|
||||
},
|
||||
"SEVERITY": {
|
||||
"CRITICAL": "Kritik",
|
||||
"HIGH": "Yüksek",
|
||||
"MEDIUM": "Orta",
|
||||
"LOW": "Düşük",
|
||||
@ -1223,6 +1223,62 @@
|
||||
"DAYS_LARGE": "Parameter \"DAYS\" is too large",
|
||||
"EXECUTION_TYPE": "Execution Type",
|
||||
"ACTION": "ACTION"
|
||||
},
|
||||
"SCANNER": {
|
||||
"DELETION_SUMMARY": "Do you want to delete scanner {{param}}?",
|
||||
"SKIP_CERT_VERIFY": "Check this box to skip certificate verification when the remote registry uses a self-signed or untrusted certificate.",
|
||||
"NAME": "Name",
|
||||
"NAME_EXISTS": "Name already exists",
|
||||
"NAME_REQUIRED": "Name is required",
|
||||
"NAME_REX": "Name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
|
||||
"DESCRIPTION": "Description",
|
||||
"ENDPOINT": "Endpoint",
|
||||
"ENDPOINT_EXISTS": "EndpointUrl already exists",
|
||||
"ENDPOINT_REQUIRED": "EndpointUrl is required",
|
||||
"ILLEGAL_ENDPOINT": "EndpointUrl is illegal",
|
||||
"AUTH": "Authorization",
|
||||
"NONE": "None",
|
||||
"BASIC": "Basic",
|
||||
"BEARER": "Bearer",
|
||||
"API_KEY": "APIKey",
|
||||
"USERNAME": "Username",
|
||||
"USERNAME_REQUIRED": "Username is required",
|
||||
"PASSWORD": "Password",
|
||||
"PASSWORD_REQUIRED": "Password is required",
|
||||
"TOKEN": "Token",
|
||||
"TOKEN_REQUIRED": "Token is required",
|
||||
"API_KEY_REQUIRED": "APIKey is required",
|
||||
"SKIP": "Skip Certificate Verification",
|
||||
"ADD_SCANNER": "Add Scanner",
|
||||
"EDIT_SCANNER": "Edit Scanner",
|
||||
"TEST_CONNECTION": "TEST CONNECTION",
|
||||
"ADD_SUCCESS": "Added successfully",
|
||||
"TEST_PASS": "Test passed",
|
||||
"TEST_FAILED": "Test failed",
|
||||
"UPDATE_SUCCESS": "Updated successfully",
|
||||
"SCANNER_COLON": "Scanner:",
|
||||
"NAME_COLON": "Name:",
|
||||
"VENDOR_COLON": "Vendor:",
|
||||
"VERSION_COLON": "Version:",
|
||||
"CAPABILITIES": "Capabilities",
|
||||
"CONSUMES_MIME_TYPES_COLON": "Consumes Mime Types:",
|
||||
"PRODUCTS_MIME_TYPES_COLON": "Produces Mime Types:",
|
||||
"PROPERTIES": "Properties",
|
||||
"NEW_SCANNER": "NEW SCANNER",
|
||||
"SET_AS_DEFAULT": "SET AS DEFAULT",
|
||||
"HEALTH": "Health",
|
||||
"DISABLED": "Disabled",
|
||||
"NO_SCANNER": "Can not find any scanner",
|
||||
"DEFAULT": "Default",
|
||||
"HEALTHY": "Healthy",
|
||||
"UNHEALTHY": "Unhealthy",
|
||||
"SCANNERS": "Scanners",
|
||||
"SCANNER": "Scanner",
|
||||
"EDIT": "Edit",
|
||||
"NOT_AVAILABLE": "Not Available",
|
||||
"ADAPTER": "Adapter",
|
||||
"VENDOR": "Vendor",
|
||||
"VERSION": "Version",
|
||||
"SELECT_SCANNER": "Select Scanner"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -921,11 +921,10 @@
|
||||
},
|
||||
"VULNERABILITY": {
|
||||
"STATE": {
|
||||
"STOPPED": "未扫描",
|
||||
"OTHER_STATUS": "未扫描",
|
||||
"QUEUED": "已入队列",
|
||||
"ERROR": "查看日志",
|
||||
"SCANNING": "扫描中",
|
||||
"UNKNOWN": "未知"
|
||||
"SCANNING": "扫描中"
|
||||
},
|
||||
"GRID": {
|
||||
"PLACEHOLDER": "没有扫描结果!",
|
||||
@ -946,6 +945,7 @@
|
||||
"TOOLTIPS_TITLE_ZERO": "没有发现可识别的漏洞包"
|
||||
},
|
||||
"SEVERITY": {
|
||||
"CRITICAL": "危急",
|
||||
"HIGH": "严重",
|
||||
"MEDIUM": "中等",
|
||||
"LOW": "较低",
|
||||
@ -1220,6 +1220,62 @@
|
||||
"DAYS_LARGE": "参数“天数”太大",
|
||||
"EXECUTION_TYPE": "执行类型",
|
||||
"ACTION": "操作"
|
||||
},
|
||||
"SCANNER": {
|
||||
"DELETION_SUMMARY": "确定删除扫描器{{param}}?",
|
||||
"SKIP_CERT_VERIFY": "当远程注册使用自签或不可信证书时可勾选此项以跳过证书认证。",
|
||||
"NAME": "名称",
|
||||
"NAME_EXISTS": "名称已存在",
|
||||
"NAME_REQUIRED": "名称为必填项",
|
||||
"NAME_REX": "名称由小写字符、数字和._-组成且至少2个字符并以字符或者数字开头。",
|
||||
"DESCRIPTION": "描述",
|
||||
"ENDPOINT": "地址",
|
||||
"ENDPOINT_EXISTS": "地址已存在",
|
||||
"ENDPOINT_REQUIRED": "地址为必填项",
|
||||
"ILLEGAL_ENDPOINT": "非法地址",
|
||||
"AUTH": "Authorization",
|
||||
"NONE": "None",
|
||||
"BASIC": "Basic",
|
||||
"BEARER": "Bearer",
|
||||
"API_KEY": "APIKey",
|
||||
"USERNAME": "用户名",
|
||||
"USERNAME_REQUIRED": "用户名为必填项",
|
||||
"PASSWORD": "密码",
|
||||
"PASSWORD_REQUIRED": "密码为必填项",
|
||||
"TOKEN": "令牌",
|
||||
"TOKEN_REQUIRED": "令牌为必填项",
|
||||
"API_KEY_REQUIRED": "APIKey为必填项",
|
||||
"SKIP": "跳过证书认证",
|
||||
"ADD_SCANNER": "添加扫描器",
|
||||
"EDIT_SCANNER": "编辑扫描器",
|
||||
"TEST_CONNECTION": "测试连接",
|
||||
"ADD_SUCCESS": "添加成功",
|
||||
"TEST_PASS": "测试成功",
|
||||
"TEST_FAILED": "测试失败",
|
||||
"UPDATE_SUCCESS": "更新成功",
|
||||
"SCANNER_COLON": "扫描器:",
|
||||
"NAME_COLON": "Name:",
|
||||
"VENDOR_COLON": "Vendor:",
|
||||
"VERSION_COLON": "Version:",
|
||||
"CAPABILITIES": "Capabilities",
|
||||
"CONSUMES_MIME_TYPES_COLON": "Consumes Mime Types:",
|
||||
"PRODUCTS_MIME_TYPES_COLON": "Produces Mime Types:",
|
||||
"PROPERTIES": "Properties",
|
||||
"NEW_SCANNER": "新建扫描器",
|
||||
"SET_AS_DEFAULT": "设为默认",
|
||||
"HEALTH": "健康",
|
||||
"DISABLED": "禁用",
|
||||
"NO_SCANNER": "暂无记录",
|
||||
"DEFAULT": "默认",
|
||||
"HEALTHY": "健康",
|
||||
"UNHEALTHY": "不健康",
|
||||
"SCANNERS": "扫描器",
|
||||
"SCANNER": "扫描器",
|
||||
"EDIT": "编辑",
|
||||
"NOT_AVAILABLE": "不可用",
|
||||
"ADAPTER": "适配器",
|
||||
"VENDOR": "供应商",
|
||||
"VERSION": "版本",
|
||||
"SELECT_SCANNER": "选择扫描器"
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user