Add scanner UI

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
AllForNothing 2019-10-10 17:42:45 +08:00 committed by sshijun
parent 0076f23195
commit c2e30b4bad
67 changed files with 3270 additions and 485 deletions

1
.gitignore vendored
View File

@ -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/

View File

@ -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";

View File

@ -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,

View File

@ -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 {

View File

@ -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)));
}

View File

@ -32,45 +32,18 @@
<label class="detail-label">{{'TAG.DOCKER_VERSION' | translate }}</label>
<div class="image-details" [title]="tagDetails.docker_version">{{tagDetails.docker_version}}</div>
</section>
<section class="detail-row">
<section class="detail-row" *ngIf="hasCve">
<label class="detail-label">{{'TAG.SCAN_COMPLETION_TIME' | translate }}</label>
<div class="image-details" [title]="scanCompletedDatetime | date">{{scanCompletedDatetime | date}}</div>
</section>
</section>
</div>
</div>
<div *ngIf="withClair" class="col-md-4 col-sm-6">
<div class="vulnerability">
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="tagDetails.scan_overview"></hbr-vulnerability-bar>
</div>
<div class="flex-block vulnerabilities-info">
<div class="second-column">
<div class="row-flex">
<div class="icon-position">
<clr-icon shape="error" size="24" class="is-error"></clr-icon>
</div>
<span class="detail-count">{{highCount}}</span> {{packageText(highCount) | translate}} {{haveText(highCount) | translate}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{suffixForHigh | translate }}
</div>
<div class="second-row row-flex">
<div class="icon-position">
<clr-icon shape="exclamation-triangle" size="24" class="tip-icon-medium"></clr-icon>
</div>
<span class="detail-count">{{mediumCount}}</span> {{packageText(mediumCount) | translate}} {{haveText(mediumCount) | translate}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{suffixForMedium | translate }}
</div>
<div class="second-row row-flex">
<div class="icon-position">
<clr-icon shape="play" size="22" class="tip-icon-low rotate-90"></clr-icon>
</div>
<span class="detail-count">{{lowCount}}</span> {{packageText(lowCount) | translate}} {{haveText(lowCount) | translate}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{suffixForLow | translate }}
</div>
<div class="second-row row-flex">
<div class="icon-position">
<clr-icon shape="help" size="20"></clr-icon>
</div>
<span class="detail-count">{{unknownCount}}</span> {{packageText(unknownCount) | translate}} {{haveText(unknownCount) | translate}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{suffixForUnknown | translate }}
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="vulnerability" [hidden]="hasCve">
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="vulnerabilitySummary"></hbr-vulnerability-bar>
</div>
<histogram-chart *ngIf="hasCve" class="margin-top-5px" [metadata]="passMetadataToChart()" [isWhiteBackground]="true"></histogram-chart>
</div>
<div *ngIf="!withAdmiral && tagDetails?.labels?.length">
<div class="third-column detail-title">{{'TAG.LABELS' | translate }}</div>
@ -83,7 +56,7 @@
</div>
</section>
<clr-tabs>
<clr-tab *ngIf="hasVulnerabilitiesListPermission && withClair">
<clr-tab *ngIf="hasVulnerabilitiesListPermission">
<button clrTabLink [clrTabLinkInOverflow]="false" class="btn btn-link nav-link" id="tag-vulnerability" [class.active]='isCurrentTabLink("tag-vulnerability")' type="button" (click)='tabLinkClick("tag-vulnerability")'>{{'REPOSITORY.VULNERABILITY' | translate}}</button>
<clr-tab-content id="content1" *clrIfActive="true">
<hbr-vulnerabilities-grid [repositoryId]="repositoryId" [projectId]="projectId" [tagId]="tagId"></hbr-vulnerabilities-grid>
@ -96,4 +69,4 @@
</clr-tab-content>
</clr-tab>
</clr-tabs>
</div>
</div>

View File

@ -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;
}

View File

@ -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);

View File

@ -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'
},
];
}
}

View File

@ -55,23 +55,23 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" class="datagrid-top" [class.embeded-datagrid]="isEmbedded" [(clrDgSelected)]="selectedRow">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(canScanNow(selectedRow) && selectedRow.length==1)" (click)="scanNow(selectedRow)"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button [clrLoading]="scanBtnState" type="button" class="btn btn-sm btn-secondary" [disabled]="!(canScanNow(selectedRow) && selectedRow.length==1 && hasEnabledScanner)" (click)="scanNow(selectedRow)"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="showDigestId(selectedRow)"><clr-icon shape="copy" size="16"></clr-icon>&nbsp;{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<clr-dropdown *ngIf="!withAdmiral">
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger [disabled]="!(selectedRow.length==1)||!hasAddLabelImagePermission" (click)="addLabels(selectedRow)"><clr-icon shape="plus" size="16"></clr-icon>{{'REPOSITORY.ADD_LABELS' | translate}}</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<clr-dropdown>
<div class="filter-grid">
<label class="dropdown-header">{{'REPOSITORY.ADD_LABEL_TO_IMAGE' | translate}}</label>
<div class="form-group"><input clrInput type="text" placeholder="Filter labels" [(ngModel)]="stickName" (keyup)="handleStickInputFilter()"></div>
<div [hidden]='imageStickLabels.length' class="no-labels">{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' class="has-label">
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' [hidden]='!label.show' (click)="stickLabel(label)">
<div class="filter-grid">
<label class="dropdown-header">{{'REPOSITORY.ADD_LABEL_TO_IMAGE' | translate}}</label>
<div class="form-group"><input clrInput type="text" placeholder="Filter labels" [(ngModel)]="stickName" (keyup)="handleStickInputFilter()"></div>
<div [hidden]='imageStickLabels.length' class="no-labels">{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' class="has-label">
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' [hidden]='!label.show' (click)="stickLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label" [labelWidth]="130"></hbr-label-piece></div>
</button>
</div>
</div>
</div>
</clr-dropdown>
</clr-dropdown-menu>
</clr-dropdown>
@ -81,7 +81,7 @@
<clr-dg-column class="flex-max-width" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withClair">{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
@ -98,8 +98,8 @@
<clr-dg-cell class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
<hbr-copy-input #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"></hbr-copy-input>
</clr-dg-cell>
<clr-dg-cell *ngIf="withClair">
<hbr-vulnerability-bar [tagStatus]="t.scan_overview?.scan_status" [repoName]="repoName" [tagId]="t.name" [summary]="t.scan_overview"></hbr-vulnerability-bar>
<clr-dg-cell>
<hbr-vulnerability-bar [repoName]="repoName" [tagId]="t.name" [summary]="handleScanOverview(t.scan_overview)"></hbr-vulnerability-bar>
</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signature !== null">
<clr-icon shape="check-circle" *ngSwitchCase="true" size="20" class="color-green"></clr-icon>

View File

@ -21,7 +21,6 @@
.embeded-datagrid {
width: 98%;
float: right;
/*add for issue #2688*/
}
.hidden-tag {
@ -249,4 +248,4 @@ clr-datagrid {
::ng-deep .clr-form-control {
margin-top: 0;
}
}

View File

@ -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(() => {

View File

@ -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;
}
}

View File

@ -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"
};
/**

View File

@ -0,0 +1 @@
<canvas class="canvas" #barChart> HTML5 canvas not supported </canvas>

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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
];

View File

@ -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>
</div>

View File

@ -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");
});
}));
});
});

View File

@ -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`;
}
}

View File

@ -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>&nbsp;{{'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-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-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>
<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>&nbsp;<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-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-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>&nbsp;<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>

View File

@ -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');

View File

@ -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';

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
});
});

View File

@ -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'
},
];
}
}

View File

@ -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");
});
}));

View File

@ -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 {

View File

@ -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;
}
@ -144,4 +135,40 @@ 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;
}

View File

@ -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>

View File

@ -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 {
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
});
});
});

View 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);
}
}
}

View File

@ -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();
}));
});

View 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)));
}
}

View File

@ -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>

View File

@ -0,0 +1,6 @@
.width-312 {
width: 312px;
}
.padding-top-3 {
padding-top: 3px;
}

View File

@ -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();
});
});

View File

@ -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));
}
}

View File

@ -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>

View File

@ -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);
});
});

View File

@ -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;
});
}
}

View 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() {
}
}

View File

@ -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');
});
});
});

View File

@ -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;
}
}

View File

@ -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>

View 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() {
}
}

View File

@ -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
}
]
},

View File

@ -31,9 +31,12 @@
<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>
</ul>
</nav>
<router-outlet></router-outlet>
<router-outlet></router-outlet>

View File

@ -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 {

View 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>

View 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;
}

View 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();
});
});
});

View 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;
});
}
}

View File

@ -41,7 +41,8 @@ export const enum ConfirmationTargets {
CONFIG_TAB,
HELM_CHART,
HELM_CHART_VERSION,
WEBHOOK
WEBHOOK,
SCANNER
}
export const enum ActionType {

View File

@ -804,7 +804,7 @@
"LDAP_UID": "The attribute used in a search to match a user. It could be uid, cn, email, sAMAccountName or other attributes depending on your LDAP/AD.",
"LDAP_SCOPE": "The scope to search for users.",
"TOKEN_EXPIRATION": "The expiration time (in minutes) of a token created by the token service. Default is 30 minutes.",
"ROBOT_TOKEN_EXPIRATION": "The expiration time (in days) of the token of the robot account, Default is 30 days. Show the number of days converted from minutes and rounds down",
"ROBOT_TOKEN_EXPIRATION": "The expiration time (in days) of the token of the robot account, Default is 30 days. Show the number of days converted from minutes and rounds down",
"PRO_CREATION_RESTRICTION": "The flag to define what users have permission to create projects. By default, everyone can create a project. Set to 'Admin Only' so that only an administrator can create a project.",
"ROOT_CERT_DOWNLOAD": "Download the root certificate of registry.",
"SCANNING_POLICY": "Set image scanning policy based on different requirements. 'None': No active policy; 'Daily At': Triggering scanning at the specified time everyday.",
@ -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"
}
}

View File

@ -803,7 +803,7 @@
"LDAP_UID": "El atributo usado en una búsqueda para encontrar un usuario. Debe ser el uid, cn, email, sAMAccountName u otro atributo dependiendo del LDAP/AD.",
"LDAP_SCOPE": "El ámbito de búsqueda para usuarios",
"TOKEN_EXPIRATION": "El tiempo de expiración (en minutos) del token creado por el servicio de tokens. Por defecto son 30 minutos.",
"ROBOT_TOKEN_EXPIRATION": "El tiempo de caducidad (días) del token de la cuenta del robot, el valor predeterminado es 30 días. Muestra el número de días convertidos de minutos y redondeos.",
"ROBOT_TOKEN_EXPIRATION": "El tiempo de caducidad (días) del token de la cuenta del robot, el valor predeterminado es 30 días. Muestra el número de días convertidos de minutos y redondeos.",
"PRO_CREATION_RESTRICTION": "Marca para definir qué usuarios tienen permisos para crear proyectos. Por defecto, todos pueden crear proyectos. Seleccione 'Solo Administradores' para que solamente los administradores puedan crear proyectos.",
"ROOT_CERT_DOWNLOAD": "Download the root certificate of registry.",
"SCANNING_POLICY": "Set image scanning policy based on different requirements. 'None': No active policy; 'Daily At': Triggering scanning at the specified time everyday.",
@ -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"
}
}

View File

@ -785,7 +785,7 @@
"LDAP_UID": "Attribut utilisé dans une recherche pour trouver un utilisateur. Cela peut être uid, cn, email, sAMAccountName ou d'autres attributs selon votre LDAP/AD.",
"LDAP_SCOPE": "Le scope de recherche des utilisateurs.",
"TOKEN_EXPIRATION": "Le temps d'expiration (en minutes) d'un jeton créé par le service de jeton. La valeur par défaut est 30 minutes.",
"ROBOT_TOKEN_EXPIRATION": "Le délai d'expiration (en jours) du jeton du compte robot est défini par défaut sur 30 jours. Afficher le nombre de jours convertis à partir des minutes et des arrondis",
"ROBOT_TOKEN_EXPIRATION": "Le délai d'expiration (en jours) du jeton du compte robot est défini par défaut sur 30 jours. Afficher le nombre de jours convertis à partir des minutes et des arrondis",
"PRO_CREATION_RESTRICTION": "L'indicateur pour définir quels utilisateurs ont le droit de créer des projets. Par défaut, tout le monde peut créer un projet. Définissez sur 'Administrateur Seulement' pour que seul un administrateur puisse créer un projet.",
"ROOT_CERT_DOWNLOAD": "Téléchargez le certificat racine du dépôt.",
"SCANNING_POLICY": "Définissez la politique d'analyse des images en fonction des différentes exigences. 'Aucune' : pas de politique active; 'Tousles jours à' : déclenchement du balayage à l'heure spécifiée tous les jours.",
@ -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"
}
}

View File

@ -798,7 +798,7 @@
"LDAP_UID": "O atributo utilizado na busca de um uusário. Pode ser uid, cn, email, sAMAccountName ou outro atributo dependendo LDAP/AD.",
"LDAP_SCOPE": "O escopo de busca de usuários.",
"TOKEN_EXPIRATION": "O tempo de expiração (em minutos) de um token criado pelo serviço de token. O padrão é 30 minutos.",
"ROBOT_TOKEN_EXPIRATION": "O tempo de expiração (dias) do token da conta do robô, o padrão é 30 dias. Mostra o número de dias convertidos de minutos e arredonda para baixo",
"ROBOT_TOKEN_EXPIRATION": "O tempo de expiração (dias) do token da conta do robô, o padrão é 30 dias. Mostra o número de dias convertidos de minutos e arredonda para baixo",
"PRO_CREATION_RESTRICTION": "A opção para definir quais usuários possuem permissão de criar projetos. Por padrão, qualquer um pode criar projetos. Configure para 'Apenas Administradores' para que apenas Administradores possam criar projetos.",
"ROOT_CERT_DOWNLOAD": "Baixar o certificado raiz do registry.",
"SCANNING_POLICY": "Configura a política de análise das imagens baseado em diferentes requisitos. 'Nenhum': Nenhuma política ativa; 'Diariamente em': Dispara a análise diariamente no horário especificado.",
@ -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",
@ -1091,7 +1091,7 @@
"AT": "at",
"NOSCHEDULE": "An error occurred in Get schedule"
},
},
"GC": {
"CURRENT_SCHEDULE": "Agendamento atual",
"ON": "em",
@ -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"
}
}

View File

@ -803,7 +803,7 @@
"LDAP_UID": "Bir kullanıcıyla eşleşmek için aramada kullanılan özellik. LDAP / AD'nize bağlı olarak, kullanıcı kimliği, cn, e-posta, sAMAccountName veya diğer özellikler olabilir.",
"LDAP_SCOPE": "Kullanıcıları aramak için kapsam.",
"TOKEN_EXPIRATION": "Token servisi tarafından oluşturulan bir tokenın sona erme süresi (dakika cinsinden). Varsayılan 30 dakikadır.",
"ROBOT_TOKEN_EXPIRATION": "Robot hesabının token son kullanma süresi (gün olarak), Varsayılan 30 gündür. Dakika ve yuvarlamadan dönüştürülen gün sayısını göster",
"ROBOT_TOKEN_EXPIRATION": "Robot hesabının token son kullanma süresi (gün olarak), Varsayılan 30 gündür. Dakika ve yuvarlamadan dönüştürülen gün sayısını göster",
"PRO_CREATION_RESTRICTION": "Hangi kullanıcıların proje oluşturma iznine sahip olduğunu belirten bayrak. Varsayılan olarak, herkes bir proje oluşturabilir. 'Yalnızca Yönetici' olarak ayarlayın, böylece yalnızca bir yönetici bir proje oluşturabilir.",
"ROOT_CERT_DOWNLOAD": "Kayıt defteri kök sertifikasını indirin.",
"SCANNING_POLICY": "Farklı gereksinimlere göre imaj tarama politikasını ayarlayın. 'Yok': Aktif politika yok; 'Günlük': Her gün belirtilen saatte taramayı tetikler.",
@ -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"
}
}

View File

@ -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": "选择扫描器"
}
}