diff --git a/.gitignore b/.gitignore index 7f8396f03..be445166e 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/src/portal/lib/src/index.ts b/src/portal/lib/src/index.ts index c37e890a6..d8728c3f8 100644 --- a/src/portal/lib/src/index.ts +++ b/src/portal/lib/src/index.ts @@ -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"; diff --git a/src/portal/lib/src/repository/repository.component.spec.ts b/src/portal/lib/src/repository/repository.component.spec.ts index acc029589..5d9c3de46 100644 --- a/src/portal/lib/src/repository/repository.component.spec.ts +++ b/src/portal/lib/src/repository/repository.component.spec.ts @@ -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, diff --git a/src/portal/lib/src/service/interface.ts b/src/portal/lib/src/service/interface.ts index 17e739d91..02cce0dda 100644 --- a/src/portal/lib/src/service/interface.ts +++ b/src/portal/lib/src/service/interface.ts @@ -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 { diff --git a/src/portal/lib/src/service/scanning.service.ts b/src/portal/lib/src/service/scanning.service.ts index 15a31754f..6f564d497 100644 --- a/src/portal/lib/src/service/scanning.service.ts +++ b/src/portal/lib/src/service/scanning.service.ts @@ -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; + | Observable; /** * Start a new vulnerability scanning @@ -106,17 +107,22 @@ export class ScanningResultDefaultService extends ScanningResultService { tagId: string, queryParams?: RequestQueryParams ): - | Observable { + | Observable { 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))); } diff --git a/src/portal/lib/src/tag/tag-detail.component.html b/src/portal/lib/src/tag/tag-detail.component.html index bee923073..bc5d47b83 100644 --- a/src/portal/lib/src/tag/tag-detail.component.html +++ b/src/portal/lib/src/tag/tag-detail.component.html @@ -32,45 +32,18 @@
{{tagDetails.docker_version}}
-
+
{{scanCompletedDatetime | date}}
-
-
- -
-
-
-
-
- -
- {{highCount}} {{packageText(highCount) | translate}} {{haveText(highCount) | translate}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{suffixForHigh | translate }} -
-
-
- -
- {{mediumCount}} {{packageText(mediumCount) | translate}} {{haveText(mediumCount) | translate}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{suffixForMedium | translate }} -
-
-
- -
- {{lowCount}} {{packageText(lowCount) | translate}} {{haveText(lowCount) | translate}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{suffixForLow | translate }} -
-
-
- -
- {{unknownCount}} {{packageText(unknownCount) | translate}} {{haveText(unknownCount) | translate}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{suffixForUnknown | translate }} -
-
+
+
+
+
{{'TAG.LABELS' | translate }}
@@ -83,7 +56,7 @@
- + @@ -96,4 +69,4 @@ -
\ No newline at end of file +
diff --git a/src/portal/lib/src/tag/tag-detail.component.scss b/src/portal/lib/src/tag/tag-detail.component.scss index 1afa55ce0..522d4e557 100644 --- a/src/portal/lib/src/tag/tag-detail.component.scss +++ b/src/portal/lib/src/tag/tag-detail.component.scss @@ -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; +} diff --git a/src/portal/lib/src/tag/tag-detail.component.spec.ts b/src/portal/lib/src/tag/tag-detail.component.spec.ts index f1547d703..93838d7eb 100644 --- a/src/portal/lib/src/tag/tag-detail.component.spec.ts +++ b/src/portal/lib/src/tag/tag-detail.component.spec.ts @@ -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); diff --git a/src/portal/lib/src/tag/tag-detail.component.ts b/src/portal/lib/src/tag/tag-detail.component.ts index 494a3df74..38e1b91bd 100644 --- a/src/portal/lib/src/tag/tag-detail.component.ts +++ b/src/portal/lib/src/tag/tag-detail.component.ts @@ -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' + }, + ]; + } } diff --git a/src/portal/lib/src/tag/tag.component.html b/src/portal/lib/src/tag/tag.component.html index 445d6d9ef..e269582a2 100644 --- a/src/portal/lib/src/tag/tag.component.html +++ b/src/portal/lib/src/tag/tag.component.html @@ -55,23 +55,23 @@
- + -
- -
-
{{'LABEL.NO_LABELS' | translate }}
-
- +
-
@@ -81,7 +81,7 @@ {{'REPOSITORY.TAG' | translate}} {{'REPOSITORY.SIZE' | translate}} {{'REPOSITORY.PULL_COMMAND' | translate}} - {{'REPOSITORY.VULNERABILITY' | translate}} + {{'REPOSITORY.VULNERABILITY' | translate}} {{'REPOSITORY.SIGNED' | translate}} {{'REPOSITORY.AUTHOR' | translate}} {{'REPOSITORY.CREATED' | translate}} @@ -98,8 +98,8 @@ - - + + diff --git a/src/portal/lib/src/tag/tag.component.scss b/src/portal/lib/src/tag/tag.component.scss index 96361363b..182143adf 100644 --- a/src/portal/lib/src/tag/tag.component.scss +++ b/src/portal/lib/src/tag/tag.component.scss @@ -21,7 +21,6 @@ .embeded-datagrid { width: 98%; float: right; - /*add for issue #2688*/ } .hidden-tag { @@ -249,4 +248,4 @@ clr-datagrid { ::ng-deep .clr-form-control { margin-top: 0; -} \ No newline at end of file +} diff --git a/src/portal/lib/src/tag/tag.component.spec.ts b/src/portal/lib/src/tag/tag.component.spec.ts index ccd4b82ad..9e328c5f6 100644 --- a/src/portal/lib/src/tag/tag.component.spec.ts +++ b/src/portal/lib/src/tag/tag.component.spec.ts @@ -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(() => { diff --git a/src/portal/lib/src/tag/tag.component.ts b/src/portal/lib/src/tag/tag.component.ts index 36c074eda..e8204532a 100644 --- a/src/portal/lib/src/tag/tag.component.ts +++ b/src/portal/lib/src/tag/tag.component.ts @@ -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; + } } diff --git a/src/portal/lib/src/utils.ts b/src/portal/lib/src/utils.ts index 6bc34dfce..30c58b8f4 100644 --- a/src/portal/lib/src/utils.ts +++ b/src/portal/lib/src/utils.ts @@ -225,16 +225,35 @@ export class CustomComparator implements Comparator { */ 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" }; /** diff --git a/src/portal/lib/src/vulnerability-scanning/histogram-chart/histogram-chart.component.html b/src/portal/lib/src/vulnerability-scanning/histogram-chart/histogram-chart.component.html new file mode 100644 index 000000000..897b1e59d --- /dev/null +++ b/src/portal/lib/src/vulnerability-scanning/histogram-chart/histogram-chart.component.html @@ -0,0 +1 @@ + HTML5 canvas not supported diff --git a/src/portal/lib/src/vulnerability-scanning/histogram-chart/histogram-chart.component.scss b/src/portal/lib/src/vulnerability-scanning/histogram-chart/histogram-chart.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/portal/lib/src/vulnerability-scanning/histogram-chart/histogram-chart.component.spec.ts b/src/portal/lib/src/vulnerability-scanning/histogram-chart/histogram-chart.component.spec.ts new file mode 100644 index 000000000..b56048b02 --- /dev/null +++ b/src/portal/lib/src/vulnerability-scanning/histogram-chart/histogram-chart.component.spec.ts @@ -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; + 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(); + }); +}); diff --git a/src/portal/lib/src/vulnerability-scanning/histogram-chart/histogram-chart.component.ts b/src/portal/lib/src/vulnerability-scanning/histogram-chart/histogram-chart.component.ts new file mode 100644 index 000000000..293aa59fe --- /dev/null +++ b/src/portal/lib/src/vulnerability-scanning/histogram-chart/histogram-chart.component.ts @@ -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 = []; + @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); + } +} diff --git a/src/portal/lib/src/vulnerability-scanning/index.ts b/src/portal/lib/src/vulnerability-scanning/index.ts index bbadc768b..b8cbb3d4a 100644 --- a/src/portal/lib/src/vulnerability-scanning/index.ts +++ b/src/portal/lib/src/vulnerability-scanning/index.ts @@ -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[] = [ ResultGridComponent, ResultTipComponent, - ResultBarChartComponent + ResultBarChartComponent, + HistogramChartComponent, + ResultTipHistogramComponent ]; diff --git a/src/portal/lib/src/vulnerability-scanning/result-bar-chart-component.html b/src/portal/lib/src/vulnerability-scanning/result-bar-chart-component.html index 3096d211e..3f3795b09 100644 --- a/src/portal/lib/src/vulnerability-scanning/result-bar-chart-component.html +++ b/src/portal/lib/src/vulnerability-scanning/result-bar-chart-component.html @@ -1,7 +1,4 @@
-
- {{'VULNERABILITY.STATE.STOPPED' | translate}} -
{{'VULNERABILITY.STATE.QUEUED' | translate}}
@@ -16,10 +13,9 @@
- +
-
- - {{'VULNERABILITY.STATE.UNKNOWN' | translate}} +
+ {{'VULNERABILITY.STATE.OTHER_STATUS' | translate}}
-
\ No newline at end of file + diff --git a/src/portal/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts b/src/portal/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts index 8a0e97bea..60bacb316 100644 --- a/src/portal/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts +++ b/src/portal/lib/src/vulnerability-scanning/result-bar-chart.component.spec.ts @@ -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"); }); - })); + }); }); diff --git a/src/portal/lib/src/vulnerability-scanning/result-bar-chart.component.ts b/src/portal/lib/src/vulnerability-scanning/result-bar-chart.component.ts index 22ff39a23..53b0f9fac 100644 --- a/src/portal/lib/src/vulnerability-scanning/result-bar-chart.component.ts +++ b/src/portal/lib/src/vulnerability-scanning/result-bar-chart.component.ts @@ -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`; } } diff --git a/src/portal/lib/src/vulnerability-scanning/result-grid.component.html b/src/portal/lib/src/vulnerability-scanning/result-grid.component.html index 211e44dc0..7c340399e 100644 --- a/src/portal/lib/src/vulnerability-scanning/result-grid.component.html +++ b/src/portal/lib/src/vulnerability-scanning/result-grid.component.html @@ -1,51 +1,65 @@
-
-
+
+
- - + +
-
-
- - +
+
+ + - - {{'VULNERABILITY.GRID.COLUMN_ID' | translate}} - {{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}} - {{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}} - {{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}} - {{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}} - - {{'VULNERABILITY.CHART.TOOLTIPS_TITLE_ZERO' | translate}} - - {{res.id}} - - {{severityText(res.severity) | translate}} - {{severityText(res.severity) | translate}} - {{severityText(res.severity) | translate}} - {{severityText(res.severity) | translate}} + + {{'VULNERABILITY.GRID.COLUMN_ID' | translate}} + {{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}} + {{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}} + {{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}} + {{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}} + + {{'VULNERABILITY.CHART.TOOLTIPS_TITLE_ZERO' | translate}} + + + {{res.id}} + {{res.id}} + + {{res.id}} + + +
+ {{link}} +
+
+
+
+
+ + {{severityText(res.severity) | translate}} + {{severityText(res.severity) | translate}} + {{severityText(res.severity) | translate}} + {{severityText(res.severity) | translate}} + {{severityText(res.severity) | translate}} + {{severityText(res.severity) | translate}} {{severityText(res.severity) | translate}} - - {{res.package}} - {{res.version}} - -
-  {{res.fixedVersion}} -
- {{res.fixedVersion}} -
- - {{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}} - -
- - - {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'VULNERABILITY.GRID.FOOT_OF' | translate}} - {{pagination.totalItems}} {{'VULNERABILITY.GRID.FOOT_ITEMS' | translate}} - - -
-
-
\ No newline at end of file + + {{res.package}} + {{res.version}} + +
+  {{res.fix_version}} +
+ {{res.fix_version}} +
+ + {{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}} + + + + + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'VULNERABILITY.GRID.FOOT_OF' | translate}} {{pagination.totalItems}} {{'VULNERABILITY.GRID.FOOT_ITEMS' | translate}} + + + +
+
diff --git a/src/portal/lib/src/vulnerability-scanning/result-grid.component.spec.ts b/src/portal/lib/src/vulnerability-scanning/result-grid.component.spec.ts index 70efd52e9..3601ff18c 100644 --- a/src/portal/lib/src/vulnerability-scanning/result-grid.component.spec.ts +++ b/src/portal/lib/src/vulnerability-scanning/result-grid.component.spec.ts @@ -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; @@ -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'); diff --git a/src/portal/lib/src/vulnerability-scanning/result-grid.component.ts b/src/portal/lib/src/vulnerability-scanning/result-grid.component.ts index 931bb2fdf..b77929505 100644 --- a/src/portal/lib/src/vulnerability-scanning/result-grid.component.ts +++ b/src/portal/lib/src/vulnerability-scanning/result-grid.component.ts @@ -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'; diff --git a/src/portal/lib/src/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component.html b/src/portal/lib/src/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component.html new file mode 100644 index 000000000..27a0ba3fc --- /dev/null +++ b/src/portal/lib/src/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component.html @@ -0,0 +1,55 @@ +
+ +
+ +
{{criticalCount}}
+
{{highCount}}
+
{{mediumCount}}
+
{{lowCount}}
+
{{negligibleCount}}
+
{{unknownCount}}
+
+
{{'VULNERABILITY.NO_VULNERABILITY' | translate }}
+
+ +
+ + + {{'VULNERABILITY.OVERALL_SEVERITY' | translate }} {{'VULNERABILITY.SEVERITY.CRITICAL' | translate | titlecase }} + + + + {{'VULNERABILITY.OVERALL_SEVERITY' | translate }} {{'VULNERABILITY.SEVERITY.HIGH' | translate | titlecase }} + + + + {{'VULNERABILITY.OVERALL_SEVERITY' | translate }} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate | titlecase}} + + + + {{'VULNERABILITY.OVERALL_SEVERITY' | translate }} {{'VULNERABILITY.SEVERITY.LOW' | translate | titlecase }} + + + + {{'VULNERABILITY.OVERALL_SEVERITY' | translate }} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate | titlecase }} + + + + {{'VULNERABILITY.OVERALL_SEVERITY' | translate }} {{'VULNERABILITY.SEVERITY.NEGLIGIBLE' | translate | titlecase }} + + + + {{'VULNERABILITY.NO_VULNERABILITY' | translate }} + +
+
+
+ +
+
+ {{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} + {{completeTimestamp | date:'short'}} +
+
+
+
diff --git a/src/portal/lib/src/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component.scss b/src/portal/lib/src/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component.scss new file mode 100644 index 000000000..c52d761e7 --- /dev/null +++ b/src/portal/lib/src/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component.scss @@ -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; +} diff --git a/src/portal/lib/src/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component.spec.ts b/src/portal/lib/src/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component.spec.ts new file mode 100644 index 000000000..ff936cabe --- /dev/null +++ b/src/portal/lib/src/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component.spec.ts @@ -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; + 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(); + }); +}); diff --git a/src/portal/lib/src/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component.ts b/src/portal/lib/src/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component.ts new file mode 100644 index 000000000..c4f345e24 --- /dev/null +++ b/src/portal/lib/src/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component.ts @@ -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' + }, + ]; + } +} diff --git a/src/portal/lib/src/vulnerability-scanning/result-tip.component.spec.ts b/src/portal/lib/src/vulnerability-scanning/result-tip.component.spec.ts index 1c29e487e..45c6d29a1 100644 --- a/src/portal/lib/src/vulnerability-scanning/result-tip.component.spec.ts +++ b/src/portal/lib/src/vulnerability-scanning/result-tip.component.spec.ts @@ -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"); }); })); diff --git a/src/portal/lib/src/vulnerability-scanning/result-tip.component.ts b/src/portal/lib/src/vulnerability-scanning/result-tip.component.ts index ef058c535..9d6d659c1 100644 --- a/src/portal/lib/src/vulnerability-scanning/result-tip.component.ts +++ b/src/portal/lib/src/vulnerability-scanning/result-tip.component.ts @@ -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 { diff --git a/src/portal/lib/src/vulnerability-scanning/scanning.scss b/src/portal/lib/src/vulnerability-scanning/scanning.scss index 8aa011d16..6203b35d9 100644 --- a/src/portal/lib/src/vulnerability-scanning/scanning.scss +++ b/src/portal/lib/src/vulnerability-scanning/scanning.scss @@ -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; -} \ No newline at end of file +} + +.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; +} diff --git a/src/portal/src/app/config/config.component.html b/src/portal/src/app/config/config.component.html index bc62674d9..04a93b92e 100644 --- a/src/portal/src/app/config/config.component.html +++ b/src/portal/src/app/config/config.component.html @@ -38,6 +38,12 @@ + + + + + + diff --git a/src/portal/src/app/config/config.module.ts b/src/portal/src/app/config/config.module.ts index aef1aed16..c62d0fec3 100644 --- a/src/portal/src/app/config/config.module.ts +++ b/src/portal/src/app/config/config.module.ts @@ -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 { +} diff --git a/src/portal/src/app/config/scanner/config-scanner.component.html b/src/portal/src/app/config/scanner/config-scanner.component.html new file mode 100644 index 000000000..ace2429fd --- /dev/null +++ b/src/portal/src/app/config/scanner/config-scanner.component.html @@ -0,0 +1,76 @@ +
+
+ + +
+
+ + + + {{'MEMBER.ACTION' | translate}} + + + + + + +
+
+
+ + + +
+
+
+
+ {{'SCANNER.NAME' | translate}} + {{'SCANNER.ENDPOINT' | translate}} + {{'SCANNER.HEALTH' | translate}} + {{'SCANNER.DISABLED' | translate}} + {{'SCANNER.AUTH' | translate}} + + {{'SCANNER.NO_SCANNER' | translate}} + + + + {{scanner.name}} + {{'SCANNER.DEFAULT' | translate}} + + {{scanner.url}} + + {{'SCANNER.HEALTHY' | translate}} + + {{'SCANNER.UNHEALTHY' | translate}} + + + {{scanner.disabled}} + {{scanner.auth?scanner.auth:'None'}} + + + + 1 - {{scanners?.length}} {{'WEBHOOK.OF' | translate}} {{scanners?.length}} {{'WEBHOOK.ITEMS' | translate}} + + +
+
+ +
diff --git a/src/portal/src/app/config/scanner/config-scanner.component.scss b/src/portal/src/app/config/scanner/config-scanner.component.scss new file mode 100644 index 000000000..d0dbac951 --- /dev/null +++ b/src/portal/src/app/config/scanner/config-scanner.component.scss @@ -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; +} diff --git a/src/portal/src/app/config/scanner/config-scanner.component.spec.ts b/src/portal/src/app/config/scanner/config-scanner.component.spec.ts new file mode 100644 index 000000000..840b13218 --- /dev/null +++ b/src/portal/src/app/config/scanner/config-scanner.component.spec.ts @@ -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; + 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(); + }); + }); +}); diff --git a/src/portal/src/app/config/scanner/config-scanner.component.ts b/src/portal/src/app/config/scanner/config-scanner.component.ts new file mode 100644 index 000000000..43f82e03f --- /dev/null +++ b/src/portal/src/app/config/scanner/config-scanner.component.ts @@ -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); + } + } +} diff --git a/src/portal/src/app/config/scanner/config-scanner.service.spec.ts b/src/portal/src/app/config/scanner/config-scanner.service.spec.ts new file mode 100644 index 000000000..2c50aab4e --- /dev/null +++ b/src/portal/src/app/config/scanner/config-scanner.service.spec.ts @@ -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(); + })); +}); diff --git a/src/portal/src/app/config/scanner/config-scanner.service.ts b/src/portal/src/app/config/scanner/config-scanner.service.ts new file mode 100644 index 000000000..2a8ef21a1 --- /dev/null +++ b/src/portal/src/app/config/scanner/config-scanner.service.ts @@ -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 { + return this.http.get(`/api/scanners?ex_name=${name}`) + .pipe(catchError(error => observableThrowError(error))) + .pipe(map(response => response as Scanner[])); + } + getScannersByEndpointUrl(endpointUrl: string): Observable { + return this.http.get(`/api/scanners?ex_url=${endpointUrl}`) + .pipe(catchError(error => observableThrowError(error))) + .pipe(map(response => response as Scanner[])); + } + testEndpointUrl(testValue: any): Observable { + return this.http.post(`/api/scanners/ping`, testValue) + .pipe(catchError(error => observableThrowError(error))); + } + addScanner(scanner: Scanner): Observable { + return this.http.post('/api/scanners', scanner ) + .pipe(catchError(error => observableThrowError(error))); + } + getScanners(): Observable { + return this.http.get('/api/scanners') + .pipe(map(response => response as Scanner[])) + .pipe(catchError(error => observableThrowError(error))); + } + updateScanner(scanner: Scanner): Observable { + return this.http.put(`/api/scanners/${scanner.uuid}`, scanner ) + .pipe(catchError(error => observableThrowError(error))); + } + deleteScanner(scanner: Scanner): Observable { + return this.http.delete(`/api/scanners/${scanner.uuid}`) + .pipe(catchError(error => observableThrowError(error))); + } + deleteScanners(scanners: Scanner[]): Observable { + let observableLists: any[] = []; + if (scanners && scanners.length > 0) { + scanners.forEach(scanner => { + observableLists.push(this.deleteScanner(scanner)); + }); + return forkJoin(...observableLists); + } + } + getProjectScanner(projectId: number): Observable { + 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 { + return this.http.put(`/api/projects/${projectId}/scanner` , {uuid: uid}) + .pipe(catchError(error => observableThrowError(error))); + } + getScannerMetadata(uid: string): Observable { + return this.http.get(`/api/scanners/${uid}/metadata`) + .pipe(map(response => response as ScannerMetadata)) + .pipe(catchError(error => observableThrowError(error))); + } + setAsDefault(uid: string): Observable { + return this.http.patch(`/api/scanners/${uid}`, {is_default: true} ) + .pipe(catchError(error => observableThrowError(error))); + } +} diff --git a/src/portal/src/app/config/scanner/new-scanner-form/new-scanner-form.component.html b/src/portal/src/app/config/scanner/new-scanner-form/new-scanner-form.component.html new file mode 100644 index 000000000..cd00d6f6a --- /dev/null +++ b/src/portal/src/app/config/scanner/new-scanner-form/new-scanner-form.component.html @@ -0,0 +1,124 @@ +
+
+
+ +
+
+ + + +
+ + {{nameTooltip | translate}} + +
+
+
+ +
+ +
+
+
+ +
+
+ + + +
+ + {{endpointTooltip | translate}} + +
+
+
+ +
+
+ +
+
+
+ +
+ +
+
+ + +
+ + {{"SCANNER.USERNAME_REQUIRED" | translate}} + +
+
+
+ +
+
+ + +
+ + {{"SCANNER.PASSWORD_REQUIRED" | translate}} + +
+
+
+ +
+
+ + +
+ + {{"SCANNER.TOKEN_REQUIRED" | translate}} + +
+
+
+ +
+
+ + +
+ + {{"SCANNER.API_KEY_REQUIRED" | translate}} + +
+
+
+
+ +
+
+ +
+
+
+
+
diff --git a/src/portal/src/app/config/scanner/new-scanner-form/new-scanner-form.component.scss b/src/portal/src/app/config/scanner/new-scanner-form/new-scanner-form.component.scss new file mode 100644 index 000000000..e996b3e08 --- /dev/null +++ b/src/portal/src/app/config/scanner/new-scanner-form/new-scanner-form.component.scss @@ -0,0 +1,6 @@ +.width-312 { + width: 312px; +} +.padding-top-3 { + padding-top: 3px; +} diff --git a/src/portal/src/app/config/scanner/new-scanner-form/new-scanner-form.component.spec.ts b/src/portal/src/app/config/scanner/new-scanner-form/new-scanner-form.component.spec.ts new file mode 100644 index 000000000..c78f276b7 --- /dev/null +++ b/src/portal/src/app/config/scanner/new-scanner-form/new-scanner-form.component.spec.ts @@ -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; + 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(); + }); +}); diff --git a/src/portal/src/app/config/scanner/new-scanner-form/new-scanner-form.component.ts b/src/portal/src/app/config/scanner/new-scanner-form/new-scanner-form.component.ts new file mode 100644 index 000000000..c073d140c --- /dev/null +++ b/src/portal/src/app/config/scanner/new-scanner-form/new-scanner-form.component.ts @@ -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)); + } +} diff --git a/src/portal/src/app/config/scanner/new-scanner-modal/new-scanner-modal.component.html b/src/portal/src/app/config/scanner/new-scanner-modal/new-scanner-modal.component.html new file mode 100644 index 000000000..6d9ffce4d --- /dev/null +++ b/src/portal/src/app/config/scanner/new-scanner-modal/new-scanner-modal.component.html @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/portal/src/app/config/scanner/new-scanner-modal/new-scanner-modal.component.spec.ts b/src/portal/src/app/config/scanner/new-scanner-modal/new-scanner-modal.component.spec.ts new file mode 100644 index 000000000..0e60d7aa9 --- /dev/null +++ b/src/portal/src/app/config/scanner/new-scanner-modal/new-scanner-modal.component.spec.ts @@ -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; + + 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); + }); +}); + + + diff --git a/src/portal/src/app/config/scanner/new-scanner-modal/new-scanner-modal.component.ts b/src/portal/src/app/config/scanner/new-scanner-modal/new-scanner-modal.component.ts new file mode 100644 index 000000000..3614dba64 --- /dev/null +++ b/src/portal/src/app/config/scanner/new-scanner-modal/new-scanner-modal.component.ts @@ -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(); + @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; + }); + } +} diff --git a/src/portal/src/app/config/scanner/scanner-metadata.ts b/src/portal/src/app/config/scanner/scanner-metadata.ts new file mode 100644 index 000000000..8610b51fb --- /dev/null +++ b/src/portal/src/app/config/scanner/scanner-metadata.ts @@ -0,0 +1,16 @@ +export class ScannerMetadata { + scanner?: { + name?: string; + vendor?: string; + version?: string; + }; + capabilities?: [{ + consumes_mime_types?: Array; + produces_mime_types?: Array; + }]; + properties?: { + [key: string]: string; + }; + constructor() { + } +} diff --git a/src/portal/src/app/config/scanner/scanner-metadata/scanner-metadata.component.spec.ts b/src/portal/src/app/config/scanner/scanner-metadata/scanner-metadata.component.spec.ts new file mode 100644 index 000000000..d88e8e255 --- /dev/null +++ b/src/portal/src/app/config/scanner/scanner-metadata/scanner-metadata.component.spec.ts @@ -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; + 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'); + }); + }); +}); diff --git a/src/portal/src/app/config/scanner/scanner-metadata/scanner-metadata.component.ts b/src/portal/src/app/config/scanner/scanner-metadata/scanner-metadata.component.ts new file mode 100644 index 000000000..2de6873f3 --- /dev/null +++ b/src/portal/src/app/config/scanner/scanner-metadata/scanner-metadata.component.ts @@ -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; + } +} diff --git a/src/portal/src/app/config/scanner/scanner-metadata/scanner-metadata.html b/src/portal/src/app/config/scanner/scanner-metadata/scanner-metadata.html new file mode 100644 index 000000000..af1e329e8 --- /dev/null +++ b/src/portal/src/app/config/scanner/scanner-metadata/scanner-metadata.html @@ -0,0 +1,31 @@ +
+
{{'SCANNER.SCANNER_COLON' | translate}}
+
+ {{'SCANNER.NAME_COLON' | translate}} + {{scannerMetadata?.scanner?.name}} +
+
+ {{'SCANNER.VENDOR_COLON' | translate}} + {{scannerMetadata?.scanner?.vendor}} +
+
+ {{'SCANNER.VERSION_COLON' | translate}} + {{scannerMetadata?.scanner?.version}} +
+
{{'SCANNER.CAPABILITIES' | translate}}
+
+ {{'SCANNER.CONSUMES_MIME_TYPES_COLON' | translate}} + +
+
+ {{'SCANNER.PRODUCTS_MIME_TYPES_COLON' | translate}} + +
+
{{'SCANNER.PROPERTIES' | translate}}
+
+ {{item?.key}}: + {{parseDate(item?.value)}} +
+
+ + diff --git a/src/portal/src/app/config/scanner/scanner-metadata/scanner-metadata.scss b/src/portal/src/app/config/scanner/scanner-metadata/scanner-metadata.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/portal/src/app/config/scanner/scanner.ts b/src/portal/src/app/config/scanner/scanner.ts new file mode 100644 index 000000000..cf5d27cf1 --- /dev/null +++ b/src/portal/src/app/config/scanner/scanner.ts @@ -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() { + } +} diff --git a/src/portal/src/app/harbor-routing.module.ts b/src/portal/src/app/harbor-routing.module.ts index df4180392..6ec95562a 100644 --- a/src/portal/src/app/harbor-routing.module.ts +++ b/src/portal/src/app/harbor-routing.module.ts @@ -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 } ] }, diff --git a/src/portal/src/app/project/project-detail/project-detail.component.html b/src/portal/src/app/project/project-detail/project-detail.component.html index ed9dedad3..4f8f55f80 100644 --- a/src/portal/src/app/project/project-detail/project-detail.component.html +++ b/src/portal/src/app/project/project-detail/project-detail.component.html @@ -31,9 +31,12 @@ + - \ No newline at end of file + diff --git a/src/portal/src/app/project/project.module.ts b/src/portal/src/app/project/project.module.ts index d29d255f0..0d4ddccbd 100644 --- a/src/portal/src/app/project/project.module.ts +++ b/src/portal/src/app/project/project.module.ts @@ -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 { diff --git a/src/portal/src/app/project/scanner/scanner.component.html b/src/portal/src/app/project/scanner/scanner.component.html new file mode 100644 index 000000000..b926ebd33 --- /dev/null +++ b/src/portal/src/app/project/scanner/scanner.component.html @@ -0,0 +1,97 @@ +
+ +
+
+
+ +
+ + +
+
+ +
+ +
+
+
+ {{scanner?.name}} + {{'SCANNER.DISABLED' | translate}} + {{'SCANNER.HEALTHY' | translate}} + {{'SCANNER.Unhealthy' | translate}} +
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+ + + + + diff --git a/src/portal/src/app/project/scanner/scanner.component.scss b/src/portal/src/app/project/scanner/scanner.component.scss new file mode 100644 index 000000000..1a531ebec --- /dev/null +++ b/src/portal/src/app/project/scanner/scanner.component.scss @@ -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; +} diff --git a/src/portal/src/app/project/scanner/scanner.component.spec.ts b/src/portal/src/app/project/scanner/scanner.component.spec.ts new file mode 100644 index 000000000..1a63b494a --- /dev/null +++ b/src/portal/src/app/project/scanner/scanner.component.spec.ts @@ -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; + 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(); + }); + }); +}); diff --git a/src/portal/src/app/project/scanner/scanner.component.ts b/src/portal/src/app/project/scanner/scanner.component.ts new file mode 100644 index 000000000..19ec8985c --- /dev/null +++ b/src/portal/src/app/project/scanner/scanner.component.ts @@ -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; + }); + } +} diff --git a/src/portal/src/app/shared/shared.const.ts b/src/portal/src/app/shared/shared.const.ts index 07ed9a844..631d9e66d 100644 --- a/src/portal/src/app/shared/shared.const.ts +++ b/src/portal/src/app/shared/shared.const.ts @@ -41,7 +41,8 @@ export const enum ConfirmationTargets { CONFIG_TAB, HELM_CHART, HELM_CHART_VERSION, - WEBHOOK + WEBHOOK, + SCANNER } export const enum ActionType { diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 260e7ba4b..6151edf3c 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -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" } - } diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index bdff7bba4..3c58c7206 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -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" } - } diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 632544f29..39b307994 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -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" } - } diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index e1f357268..a91f0e43f 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -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" } - } diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 13ce579c1..e79ea7c2e 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -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" } - } diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 6234396e6..b3c103528 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -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": "选择扫描器" } - }