Add stop scan functionality (#15528)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
孙世军 2021-09-15 17:00:08 +08:00 committed by GitHub
parent c5003f38ba
commit 9e9c4a03bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 466 additions and 244 deletions

View File

@ -13,10 +13,10 @@
<div class="clr-row">
<div class="clr-col-2 flex-200">
<div class="btn-scan-right btn-scan margin-top-16px">
<button id="scan-now" class="btn btn-primary btn-scan" (click)="scanNow()"
[disabled]="!scanAvailable">
<span *ngIf="scanAvailable">{{ 'CONFIG.SCANNING.SCAN_NOW' | translate }}</span>
<span *ngIf="!scanAvailable">{{ 'CONFIG.SCANNING.SCAN' | translate }}</span>
<button id="scan" class="btn btn-primary btn-scan" (click)="scanOrStop()"
[disabled]="onSubmitting || !hasDefaultScanner">
<span *ngIf="!isOnScanning()">{{ 'CONFIG.SCANNING.SCAN_NOW' | translate }}</span>
<span *ngIf="isOnScanning()">{{ 'VULNERABILITY.STOP_NOW' | translate }}</span>
</button>
<span [hidden]="!isOnScanning()" class="spinner spinner-inline margin-left-5 v-mid"></span>
</div>

View File

@ -4,9 +4,10 @@ import { ScanAllRepoService } from "./scanAll.service";
import { of } from "rxjs";
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { ScanningMetrics, Triggers } from "../../config/config";
import { ErrorHandler } from "../../../../shared/units/error-handler";
import { Scanner } from "../scanner/scanner";
import { SharedTestingModule } from "../../../../shared/shared.module";
import { delay } from "rxjs/operators";
import { ScanAllService } from "../../../../../../ng-swagger-gen/services/scan-all.service";
let component: VulnerabilityConfigComponent;
let fixture: ComponentFixture<VulnerabilityConfigComponent>;
@ -51,9 +52,10 @@ let fakedScanAllRepoService = {
return of(null);
}
};
let fakedErrorHandler = {
info() {
return null;
const fakedScanAllService = {
stopScanAll() {
return of(null);
}
};
@ -70,8 +72,8 @@ describe('VulnerabilityConfigComponent', () => {
VulnerabilityConfigComponent
],
providers: [
{provide: ErrorHandler, useValue: fakedErrorHandler},
{provide: ScanAllRepoService, useValue: fakedScanAllRepoService},
{provide: ScanAllService, useValue: fakedScanAllService},
// open auto detect
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
@ -94,7 +96,7 @@ describe('VulnerabilityConfigComponent', () => {
expect(component.scanAvailable).toBeFalsy();
});
it('will trigger scan now and get manual metrics', () => {
const button = fixture.nativeElement.querySelector('#scan-now');
const button = fixture.nativeElement.querySelector('#scan');
button.click();
const ele = fixture.nativeElement.querySelector('.finished');
expect(ele.style.width).toEqual('80px');
@ -104,5 +106,11 @@ describe('VulnerabilityConfigComponent', () => {
component.scanningMetrics.ongoing = false;
component.hasDefaultScanner = true;
expect(component.scanAvailable).toBeTruthy();
const spnManual: HTMLSpanElement = fixture.nativeElement.querySelector(".float-left");
expect(spnManual.innerText).toEqual('CONFIG.SCANNING.SCHEDULED');
const scanBtn: HTMLButtonElement = fixture.nativeElement.querySelector("#scan");
scanBtn.click();
const spnSchedule: HTMLSpanElement = fixture.nativeElement.querySelector(".float-left");
expect(spnSchedule.innerText).toEqual('CONFIG.SCANNING.MANUAL');
});
});

View File

@ -1,4 +1,4 @@
import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { finalize } from "rxjs/operators";
import { ScanningMetrics, Triggers } from '../../config/config';
import { ErrorHandler } from '../../../../shared/units/error-handler';
@ -9,9 +9,11 @@ import { CronScheduleComponent } from "../../../../shared/components/cron-schedu
import { DATABASE_UPDATED_PROPERTY, VULNERABILITY_SCAN_STATUS } from "../../../../shared/units/utils";
import { DatePipe } from "@angular/common";
import { errorHandler } from "../../../../shared/units/shared.utils";
import { ScanAllService } from "../../../../../../ng-swagger-gen/services/scan-all.service";
const SCHEDULE_TYPE_NONE = "None";
const TIMEOUT: number = 5000;
@Component({
selector: 'vulnerability-config',
templateUrl: './vulnerability-config.component.html',
@ -31,29 +33,34 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
private _scanningMetrics: ScanningMetrics;
totalWidth: number = 200;
i18nKeyMap = {
"Pending": "CONFIG.SCANNING.STATUS.PENDING",
"Running": "CONFIG.SCANNING.STATUS.RUNNING",
"Stopped": "CONFIG.SCANNING.STATUS.STOPPED",
"Error": "CONFIG.SCANNING.STATUS.ERROR",
"Success": "CONFIG.SCANNING.STATUS.SUCCESS",
"Scheduled": "CONFIG.SCANNING.STATUS.SCHEDULED"
"Pending": "CONFIG.SCANNING.STATUS.PENDING",
"Running": "CONFIG.SCANNING.STATUS.RUNNING",
"Stopped": "CONFIG.SCANNING.STATUS.STOPPED",
"Error": "CONFIG.SCANNING.STATUS.ERROR",
"Success": "CONFIG.SCANNING.STATUS.SUCCESS",
"Scheduled": "CONFIG.SCANNING.STATUS.SCHEDULED"
};
updatedTimeStr: string;
onGettingUpdatedTimeStr: boolean;
hasDefaultScanner: boolean = false;
private _timeout: any;
constructor(
private scanningService: ScanAllRepoService,
private newScanAllService: ScanAllService,
private errorHandlerEntity: ErrorHandler,
private translate: TranslateService,
) { }
) {
}
get scanningMetrics(): ScanningMetrics {
return this._scanningMetrics;
}
set scanningMetrics(metrics: ScanningMetrics) {
this._scanningMetrics = metrics;
}
get scanAvailable(): boolean {
return !this.onSubmitting
&& !this.gettingMetrics
@ -63,21 +70,23 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
getScanText() {
this.translate.get('CONFIG.SCANNING.SCHEDULE_TO_SCAN_ALL').subscribe((res: string) => {
this.getLabelCurrent = res;
this.getLabelCurrent = res;
});
}
getSchedule() {
this.onGoing = true;
this.scanningService.getSchedule()
.pipe(finalize(() => {
this.onGoing = false;
}))
.subscribe(schedule => {
this.initSchedule(schedule);
}, error => {
this.errorHandlerEntity.error(error);
});
this.onGoing = true;
this.scanningService.getSchedule()
.pipe(finalize(() => {
this.onGoing = false;
}))
.subscribe(schedule => {
this.initSchedule(schedule);
}, error => {
this.errorHandlerEntity.error(error);
});
}
getScanners() {
this.onGettingUpdatedTimeStr = true;
this.scanningService.getScanners()
@ -99,43 +108,45 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
if (!flag) {
this.onGettingUpdatedTimeStr = false;
this.translate.get("SCANNER.NO_DEFAULT_SCANNER")
.subscribe(res => {
this.errorHandlerEntity.warning(res);
}
);
.subscribe(res => {
this.errorHandlerEntity.warning(res);
}
);
}
}, error => {
this.onGettingUpdatedTimeStr = false;
});
}
getScannerMetadata(uid: string) {
this.scanningService.getScannerMetadata(uid)
.pipe(finalize(() => this.onGettingUpdatedTimeStr = false))
.subscribe(metadata => {
if (metadata && metadata.properties) {
for (let key in metadata.properties) {
if (key === DATABASE_UPDATED_PROPERTY && metadata.properties[key]) {
this.updatedTimeStr = new DatePipe('en-us').transform(metadata.properties[key], 'short');
}
}
}
});
this.scanningService.getScannerMetadata(uid)
.pipe(finalize(() => this.onGettingUpdatedTimeStr = false))
.subscribe(metadata => {
if (metadata && metadata.properties) {
for (let key in metadata.properties) {
if (key === DATABASE_UPDATED_PROPERTY && metadata.properties[key]) {
this.updatedTimeStr = new DatePipe('en-us').transform(metadata.properties[key], 'short');
}
}
}
});
}
public initSchedule(schedule: any) {
public initSchedule(schedule: any) {
if (schedule && schedule.schedule !== null) {
this.schedule = schedule;
this.originCron = this.schedule.schedule;
this.schedule = schedule;
this.originCron = this.schedule.schedule;
} else {
this.originCron = {
type: SCHEDULE_TYPE_NONE,
cron: ''
};
this.originCron = {
type: SCHEDULE_TYPE_NONE,
cron: ''
};
}
}
}
ngOnInit(): void {
this.getScanText();
this.getScanners();
this.getScanText();
this.getScanners();
}
ngOnDestroy() {
@ -144,10 +155,12 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
this._timeout = null;
}
}
isOnScanning(): boolean {
return this.scanningMetrics
&& this.scanningMetrics.ongoing;
&& this.scanningMetrics.ongoing;
}
getMetrics() {
this.gettingMetrics = true;
this.scanningService.getMetrics()
@ -155,26 +168,28 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
.subscribe(response => {
if (response) {
if (response.ongoing) {
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
if (!this._timeout) {
this._timeout = setTimeout(() => {
this.getMetrics();
}, TIMEOUT);
}
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
if (!this._timeout) {
this._timeout = setTimeout(() => {
this.getMetrics();
}, TIMEOUT);
}
}
this.scanningMetrics = response;
}
});
}
getI18nKey(str: string): string {
if (str && this.i18nKeyMap[str]) {
return this.i18nKeyMap[str];
}
return str;
}
errorWidth() {
if (this.scanningMetrics
&& this.scanningMetrics.metrics
@ -185,6 +200,7 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
}
return '0';
}
finishedWidth() {
if (this.scanningMetrics
&& this.scanningMetrics.metrics
@ -195,6 +211,7 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
}
return '0';
}
runningWidth() {
if (this.scanningMetrics
&& this.scanningMetrics.metrics
@ -205,6 +222,7 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
}
return '0';
}
abortWidth() {
if (this.scanningMetrics
&& this.scanningMetrics.metrics
@ -215,6 +233,7 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
}
return '0';
}
scanNow(): void {
if (this.onSubmitting) {
return; // Aoid duplicated submitting
@ -226,32 +245,32 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
this.onSubmitting = true;
this.scanningService.manualScan()
.pipe(finalize(() => this.onSubmitting = false))
.pipe(finalize(() => this.onSubmitting = false))
.subscribe(() => {
this.translate.get("CONFIG.SCANNING.TRIGGER_SCAN_ALL_SUCCESS").subscribe((res: string) => {
this.errorHandlerEntity.info(res);
});
// reset metrics and then get new metrics
this.scanningMetrics = null;
this.getMetrics();
}
, error => {
if (error && error.status && error.status === 412) {
this.translate.get("CONFIG.SCANNING.TRIGGER_SCAN_ALL_FAIL",
{ error: '' + errorHandler(error) }).subscribe((res: string) => {
this.errorHandlerEntity.error(res);
this.translate.get("CONFIG.SCANNING.TRIGGER_SCAN_ALL_SUCCESS").subscribe((res: string) => {
this.errorHandlerEntity.info(res);
});
} else {
this.errorHandlerEntity.error(error);
// reset metrics and then get new metrics
this.scanningMetrics = null;
this.getMetrics();
}
});
, error => {
if (error && error.status && error.status === 412) {
this.translate.get("CONFIG.SCANNING.TRIGGER_SCAN_ALL_FAIL",
{error: '' + errorHandler(error)}).subscribe((res: string) => {
this.errorHandlerEntity.error(res);
});
} else {
this.errorHandlerEntity.error(error);
}
});
}
reset(cron): void {
this.schedule = {
schedule: {
type: this.CronScheduleComponent.scheduleType,
cron: cron
type: this.CronScheduleComponent.scheduleType,
cron: cron
}
};
}
@ -259,51 +278,76 @@ export class VulnerabilityConfigComponent implements OnInit, OnDestroy {
saveSchedule(cron: string): void {
let schedule = this.schedule;
if (schedule && schedule.schedule && schedule.schedule.type !== SCHEDULE_TYPE_NONE) {
this.scanningService.putSchedule(this.CronScheduleComponent.scheduleType, cron)
.subscribe(response => {
this.translate
.get("CONFIG.SAVE_SUCCESS")
.subscribe((res) => {
this.errorHandlerEntity.info(res);
this.CronScheduleComponent.resetSchedule();
});
this.reset(cron);
},
error => {
this.errorHandlerEntity.error(error);
}
);
this.scanningService.putSchedule(this.CronScheduleComponent.scheduleType, cron)
.subscribe(response => {
this.translate
.get("CONFIG.SAVE_SUCCESS")
.subscribe((res) => {
this.errorHandlerEntity.info(res);
this.CronScheduleComponent.resetSchedule();
});
this.reset(cron);
},
error => {
this.errorHandlerEntity.error(error);
}
);
} else {
this.scanningService.postSchedule(this.CronScheduleComponent.scheduleType, cron)
.subscribe(response => {
this.translate.get("CONFIG.SAVE_SUCCESS").subscribe((res) => {
this.errorHandlerEntity.info(res);
this.CronScheduleComponent.resetSchedule();
});
this.reset(cron);
},
error => {
this.errorHandlerEntity.error(error);
}
);
this.scanningService.postSchedule(this.CronScheduleComponent.scheduleType, cron)
.subscribe(response => {
this.translate.get("CONFIG.SAVE_SUCCESS").subscribe((res) => {
this.errorHandlerEntity.info(res);
this.CronScheduleComponent.resetSchedule();
});
this.reset(cron);
},
error => {
this.errorHandlerEntity.error(error);
}
);
}
}
isError(status: string): boolean {
return status === VULNERABILITY_SCAN_STATUS.ERROR;
}
isFinished(status: string): boolean {
return status === VULNERABILITY_SCAN_STATUS.SUCCESS;
}
isInProgress(status: string): boolean {
return status === VULNERABILITY_SCAN_STATUS.RUNNING;
}
isAborted(status: string): boolean {
return status === VULNERABILITY_SCAN_STATUS.STOPPED;
}
isManual() {
return this.scanningMetrics && this.scanningMetrics.trigger === Triggers.MANUAL;
}
isSchedule() {
return this.scanningMetrics && this.scanningMetrics.trigger === Triggers.SCHEDULE;
}
isError(status: string): boolean {
return status === VULNERABILITY_SCAN_STATUS.ERROR;
}
isFinished(status: string): boolean {
return status === VULNERABILITY_SCAN_STATUS.SUCCESS;
}
isInProgress(status: string): boolean {
return status === VULNERABILITY_SCAN_STATUS.RUNNING;
}
isAborted(status: string): boolean {
return status === VULNERABILITY_SCAN_STATUS.STOPPED;
}
isManual() {
return this.scanningMetrics && this.scanningMetrics.trigger === Triggers.MANUAL;
}
isSchedule() {
return this.scanningMetrics && this.scanningMetrics.trigger === Triggers.SCHEDULE;
}
scanOrStop() {
if (this.isOnScanning()) {
this.stopNow();
} else {
this.scanNow();
}
}
stopNow() {
this.onSubmitting = true;
this.newScanAllService.stopScanAll()
.pipe(finalize(() => this.onSubmitting = false))
.subscribe(res => {
this.errorHandlerEntity.info('CONFIG.SCANNING.STOP_SCAN_ALL_SUCCESS');
}, error => {
this.errorHandlerEntity.error(error);
});
}
}

View File

@ -92,6 +92,11 @@ export const INITIAL_ACCESSES: FrontAccess[] = [
"resource": "scan",
"action": "create",
"checked": true
},
{
"resource": "scan",
"action": "stop",
"checked": true
}
];
@ -108,7 +113,8 @@ export const ACTION_RESOURCE_I18N_MAP = {
'tag': 'REPLICATION.TAG',
'artifact-label': 'SYSTEM_ROBOT.ARTIFACT_LABEL',
'scan': 'SYSTEM_ROBOT.SCAN',
'scanner-pull': 'SYSTEM_ROBOT.SCANNER_PULL'
'scanner-pull': 'SYSTEM_ROBOT.SCANNER_PULL',
'stop': 'SYSTEM_ROBOT.STOP'
};
export enum ExpirationType {

View File

@ -11,14 +11,16 @@
<clr-dg-action-bar>
<div class="clr-row center">
<div class="ml-05">
<button id="scan-btn" (click)="scanNow()" type="button" class="btn btn-secondary" [clrLoading]="scanBtnState" [disabled]="!(hasEnabledScanner && hasScanningPermission && !onSendingScanCommand)">
<clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;
<span>{{'VULNERABILITY.SCAN_NOW' | translate}}</span>
<button id="scan-btn" (click)="scanOrStop()" type="button" class="btn btn-secondary" [clrLoading]="scanBtnState" [disabled]="!canScan()">
<clr-icon shape="shield-check" size="16" *ngIf="!isRunningState()"></clr-icon>&nbsp;
<clr-icon shape="stop" size="16" *ngIf="isRunningState()"></clr-icon>&nbsp;
<span *ngIf="!isRunningState()">{{'VULNERABILITY.SCAN_NOW' | translate}}</span>
<span *ngIf="isRunningState()">{{'VULNERABILITY.STOP_NOW' | translate}}</span>
</button>
</div>
<div class="ml-1">
<div [hidden]="!shouldShowBar()">
<hbr-vulnerability-bar [summary]="handleScanOverview(artifact?.scan_overview)" [inputScanner]="scanner"
<hbr-vulnerability-bar (submitStopFinish)="submitStopFinish($event)" [summary]="handleScanOverview(artifact?.scan_overview)" [inputScanner]="scanner"
(submitFinish)="submitFinish($event)" [projectName]="projectName" [repoName]="repoName"
[artifactDigest]="digest">
</hbr-vulnerability-bar>

View File

@ -15,7 +15,6 @@ import {
} from "../../../../../../shared/services";
import { AdditionLink } from "../../../../../../../../ng-swagger-gen/models/addition-link";
import { ErrorHandler } from "../../../../../../shared/units/error-handler";
import { ChannelService } from "../../../../../../shared/services/channel.service";
import { SessionService } from "../../../../../../shared/services/session.service";
import { SessionUser } from "../../../../../../shared/entities/session-user";
import { delay } from "rxjs/operators";
@ -66,13 +65,6 @@ describe('ArtifactVulnerabilitiesComponent', () => {
return of(true);
}
};
const fakedChannelService = {
ArtifactDetail$: {
subscribe() {
return null;
}
}
};
const mockedUser: SessionUser = {
user_id: 1,
username: 'admin',
@ -120,7 +112,6 @@ describe('ArtifactVulnerabilitiesComponent', () => {
{provide: AdditionsService, useValue: fakedAdditionsService},
{provide: UserPermissionService, useValue: fakedUserPermissionService},
{provide: ScanningResultService, useValue: fakedScanningResultService},
{provide: ChannelService, useValue: fakedChannelService},
{provide: SessionService, useValue: fakedSessionService},
{provide: ProjectService, useValue: fakedProjectService},
],
@ -160,4 +151,10 @@ describe('ArtifactVulnerabilitiesComponent', () => {
const cells = firstRow.querySelectorAll("clr-dg-cell");
expect(cells[cells.length - 1].innerText).toEqual("TAG_RETENTION.YES");
});
it("scan button should show the right text", async () => {
fixture.autoDetectChanges(true);
const scanBtn: HTMLButtonElement = fixture.nativeElement.querySelector("#scan-btn");
expect(scanBtn.innerText).toContain("VULNERABILITY.SCAN_NOW");
});
});

View File

@ -14,11 +14,11 @@ import {
} from "../../../../../../shared/services";
import { ErrorHandler } from "../../../../../../shared/units/error-handler";
import { SEVERITY_LEVEL_MAP, VULNERABILITY_SEVERITY } from "../../../../../../shared/units/utils";
import { ChannelService } from "../../../../../../shared/services/channel.service";
import { ResultBarChartComponent } from "../../vulnerability-scanning/result-bar-chart.component";
import { Subscription } from "rxjs";
import { Artifact } from "../../../../../../../../ng-swagger-gen/models/artifact";
import { SessionService } from "../../../../../../shared/services/session.service";
import { EventService, HarborEvent } from "../../../../../../services/event-service/event.service";
@Component({
selector: 'hbr-artifact-vulnerabilities',
@ -49,6 +49,7 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
cvssSort: ClrDatagridComparatorInterface<VulnerabilityItem>;
hasScanningPermission: boolean = false;
onSendingScanCommand: boolean = false;
onSendingStopCommand: boolean = false;
hasShowLoading: boolean = false;
@ViewChild(ResultBarChartComponent)
resultBarChartComponent: ResultBarChartComponent;
@ -60,7 +61,7 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
private additionsService: AdditionsService,
private userPermissionService: UserPermissionService,
private scanningService: ScanningResultService,
private channel: ChannelService,
private eventService: EventService,
private session: SessionService,
private projectService: ProjectService,
private systemInfoService: SystemInfoService,
@ -86,8 +87,10 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
this.getScanningPermission();
this.getProjectScanner();
if (!this.sub) {
this.sub = this.channel.ArtifactDetail$.subscribe(tag => {
this.getVulnerabilities();
this.sub = this.eventService.subscribe(HarborEvent.UPDATE_VULNERABILITY_INFO, (artifact: Artifact) => {
if (artifact?.digest === this.artifact?.digest) {
this.getVulnerabilities();
}
});
}
setTimeout(() => {
@ -190,20 +193,27 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
}
scanNow() {
this.onSendingScanCommand = true;
this.channel.publishScanEvent(this.repoName + "/" + this.digest);
this.eventService.publish(HarborEvent.START_SCAN_ARTIFACT, this.repoName + "/" + this.digest);
}
submitFinish(e: boolean) {
this.onSendingScanCommand = e;
}
submitStopFinish(e: boolean) {
this.onSendingStopCommand = e;
}
shouldShowBar(): boolean {
return this.hasViewInitWithDelay && this.resultBarChartComponent
&& (this.resultBarChartComponent.queued || this.resultBarChartComponent.scanning || this.resultBarChartComponent.error);
&& (this.resultBarChartComponent.queued
|| this.resultBarChartComponent.scanning
|| this.resultBarChartComponent.error
|| this.resultBarChartComponent.stopped);
}
hasScanned(): boolean {
return this.hasViewInitWithDelay && this.resultBarChartComponent
&& !(this.resultBarChartComponent.completed
|| this.resultBarChartComponent.error
|| this.resultBarChartComponent.queued
|| this.resultBarChartComponent.stopped
|| this.resultBarChartComponent.scanning);
}
handleScanOverview(scanOverview: any): any {
@ -256,4 +266,23 @@ export class ArtifactVulnerabilitiesComponent implements OnInit, OnDestroy {
}
return "";
}
isRunningState(): boolean {
return this.hasViewInitWithDelay && this.resultBarChartComponent
&& (this.resultBarChartComponent.queued || this.resultBarChartComponent.scanning);
}
scanOrStop() {
if (this.isRunningState()) {
this.stopNow();
} else {
this.scanNow();
}
}
stopNow() {
this.onSendingStopCommand = true;
this.eventService.publish(HarborEvent.STOP_SCAN_ARTIFACT, this.repoName + "/" + this.digest);
}
canScan(): boolean {
return this.hasEnabledScanner && this.hasScanningPermission && !this.onSendingScanCommand;
}
}

View File

@ -96,6 +96,12 @@
<span>{{'VULNERABILITY.SCAN_NOW' | translate}}</span>
</button>
<button id="stop-scan" [clrLoading]="stopBtnState" type="button" class="btn btn-secondary scan-btn"
[disabled]="!(canStopScan() && hasScanImagePermission )" (click)="stopNow()">
<clr-icon shape="stop" size="16"></clr-icon>&nbsp;
<span>{{'VULNERABILITY.STOP_NOW' | translate}}</span>
</button>
<clr-dropdown class="btn btn-link" *ngIf="!depth">
<span clrDropdownTrigger id="artifact-list-action" class="btn pl-0">
{{'BUTTON.ACTIONS' | translate}}
@ -257,7 +263,7 @@
<span *ngIf="!hasVul(artifact)">
{{'ARTIFACT.SCAN_UNSUPPORTED' | translate}}
</span>
<hbr-vulnerability-bar (scanFinished)="scanFinished($event)" *ngIf="hasVul(artifact)" [inputScanner]="handleScanOverview(artifact.scan_overview)?.scanner"
<hbr-vulnerability-bar (submitStopFinish)="submitStopFinish($event)" (scanFinished)="scanFinished($event)" *ngIf="hasVul(artifact)" [inputScanner]="handleScanOverview(artifact.scan_overview)?.scanner"
(submitFinish)="submitFinish($event)" [projectName]="projectName" [repoName]="repoName"
[artifactDigest]="artifact.digest" [summary]="handleScanOverview(artifact.scan_overview)">
</hbr-vulnerability-bar>

View File

@ -22,7 +22,6 @@ import { ConfirmationDialogComponent } from "../../../../../../../shared/compone
import { ImageNameInputComponent } from "../../../../../../../shared/components/image-name-input/image-name-input.component";
import { CopyInputComponent } from "../../../../../../../shared/components/push-image/copy-input.component";
import { ErrorHandler } from "../../../../../../../shared/units/error-handler";
import { ChannelService } from "../../../../../../../shared/services/channel.service";
import { OperationService } from "../../../../../../../shared/components/operation/operation.service";
import { ArtifactService as NewArtifactService } from "../../../../../../../../../ng-swagger-gen/services/artifact.service";
import { Tag } from "../../../../../../../../../ng-swagger-gen/models/tag";
@ -307,8 +306,6 @@ describe("ArtifactListTabComponent (inline template)", () => {
CopyInputComponent
],
providers: [
ErrorHandler,
ChannelService,
ArtifactDefaultService,
{ provide: Router, useValue: mockRouter },
{ provide: ArtifactService, useValue: mockNewArtifactService },

View File

@ -36,7 +36,6 @@ import { CopyInputComponent } from "../../../../../../../shared/components/push-
import { ErrorHandler } from "../../../../../../../shared/units/error-handler";
import { ArtifactService } from "../../../artifact.service";
import { OperationService } from "../../../../../../../shared/components/operation/operation.service";
import { ChannelService } from "../../../../../../../shared/services/channel.service";
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../../../../../../../shared/entities/shared.const";
import { operateChanges, OperateInfo, OperationState } from "../../../../../../../shared/components/operation/operate";
import { artifactDefault, ArtifactFront as Artifact, ArtifactFront, artifactPullCommands, mutipleFilter } from '../../../artifact';
@ -52,6 +51,7 @@ import { ConfirmationAcknowledgement } from "../../../../../../global-confirmati
import { UN_LOGGED_PARAM } from "../../../../../../../account/sign-in/sign-in.service";
import { Label } from "../../../../../../../../../ng-swagger-gen/models/label";
import { LabelService } from "../../../../../../../../../ng-swagger-gen/services/label.service";
import { EventService, HarborEvent } from "../../../../../../../services/event-service/event.service";
export interface LabelState {
iconsShow: boolean;
@ -142,6 +142,9 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
hasEnabledScanner: boolean;
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
onSendingScanCommand: boolean;
onSendingStopScanCommand: boolean = false;
onStopScanArtifactsLength: number = 0;
scanStoppedArtifactLength: number = 0;
artifactDigest: string;
depth: string;
@ -157,6 +160,8 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
scanFinishedArtifactLength: number = 0;
onScanArtifactsLength: number = 0;
stopBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
updateArtifactSub: Subscription;
constructor(
private errorHandlerService: ErrorHandler,
private userPermissionService: UserPermissionService,
@ -165,7 +170,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
private newArtifactService: NewArtifactService,
private translateService: TranslateService,
private operationService: OperationService,
private channel: ChannelService,
private eventService: EventService,
private activatedRoute: ActivatedRoute,
private scanningService: ScanningResultService,
private router: Router,
@ -186,6 +191,17 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
}
this.init();
});
if (!this.updateArtifactSub) {
this.updateArtifactSub = this.eventService.subscribe(HarborEvent.UPDATE_VULNERABILITY_INFO, (artifact: Artifact) => {
if (this.artifactList && this.artifactList.length) {
this.artifactList.forEach(item => {
if (item.digest === artifact.digest) {
item.scan_overview = artifact.scan_overview;
}
});
}
});
}
}
ngOnDestroy() {
if (this.triggerSub) {
@ -200,6 +216,10 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
this.stickLabelNameFilterSub.unsubscribe();
this.stickLabelNameFilterSub = null;
}
if (this.updateArtifactSub) {
this.updateArtifactSub.unsubscribe();
this.updateArtifactSub = null;
}
}
init() {
this.hasInit = true;
@ -921,12 +941,21 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
}
return VULNERABILITY_SCAN_STATUS.NOT_SCANNED;
}
// Whether show the 'scan now' menu
// if has running job, return false
canScanNow(): boolean {
if (!this.hasScanImagePermission) { return false; }
if (this.onSendingScanCommand) { return false; }
let st: string = this.scanStatus(this.selectedRow[0]);
return st !== VULNERABILITY_SCAN_STATUS.RUNNING;
if (this.selectedRow && this.selectedRow.length) {
let flag: boolean = true;
this.selectedRow.forEach(item => {
const st: string = this.scanStatus(item);
if (this.isRunningState(st)) {
flag = false;
}
});
return flag;
}
return false;
}
getImagePermissionRule(projectId: number): void {
const permissions = [
@ -958,7 +987,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
this.onSendingScanCommand = true;
this.selectedRow.forEach((data: any) => {
let digest = data.digest;
this.channel.publishScanEvent(this.repoName + "/" + digest);
this.eventService.publish(HarborEvent.START_SCAN_ARTIFACT, this.repoName + "/" + digest);
});
}
selectedRowHasVul(): boolean {
@ -972,11 +1001,18 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
}
submitFinish(e: boolean) {
this.scanFinishedArtifactLength += 1;
// all selected scan action has start
// all selected scan action has started
if (this.scanFinishedArtifactLength === this.onScanArtifactsLength) {
this.onSendingScanCommand = e;
}
}
submitStopFinish(e: boolean) {
this.scanStoppedArtifactLength += 1;
// all selected scan action has stopped
if (this.scanStoppedArtifactLength === this.onStopScanArtifactsLength) {
this.onSendingScanCommand = e;
}
}
// pull command
onCpError($event: any): void {
this.copyInput.setPullCommendShow();
@ -1108,7 +1144,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
if (res.headers) {
let xHeader: string = res.headers.get("x-total-count");
if (xHeader) {
item.tagNumber = Number.parseInt(xHeader);
item.tagNumber = Number.parseInt(xHeader, 10);
}
}
item.tags = res.body;
@ -1117,4 +1153,35 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
});
}
}
// return true if all selected rows are in "running" state
canStopScan(): boolean {
if (this.onSendingStopScanCommand) { return false; }
if (this.selectedRow && this.selectedRow.length) {
let flag: boolean = true;
this.selectedRow.forEach(item => {
const st: string = this.scanStatus(item);
if (!this.isRunningState(st)) {
flag = false;
}
});
return flag;
}
return false;
}
isRunningState(state: string): boolean {
return state === VULNERABILITY_SCAN_STATUS.RUNNING ||
state === VULNERABILITY_SCAN_STATUS.PENDING ||
state === VULNERABILITY_SCAN_STATUS.SCHEDULED;
}
stopNow() {
if (this.selectedRow && this.selectedRow.length) {
this.scanStoppedArtifactLength = 0;
this.onStopScanArtifactsLength = this.selectedRow.length;
this.onSendingStopScanCommand = true;
this.selectedRow.forEach((data: any) => {
let digest = data.digest;
this.eventService.publish(HarborEvent.STOP_SCAN_ARTIFACT, this.repoName + "/" + digest);
});
}
}
}

View File

@ -6,7 +6,6 @@ import { delay } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';
import { SystemInfo, SystemInfoDefaultService, SystemInfoService, } from "../../../../../../shared/services";
import { ArtifactDefaultService, ArtifactService } from "../../artifact.service";
import { ChannelService } from "../../../../../../shared/services/channel.service";
import { ErrorHandler } from "../../../../../../shared/units/error-handler";
import { RepositoryService as NewRepositoryService } from "../../../../../../../../ng-swagger-gen/services/repository.service";
import { SharedTestingModule } from "../../../../../../shared/shared.module";
@ -35,9 +34,6 @@ describe('ArtifactListComponent (inline template)', () => {
},
snapshot: { data: null }
};
let mockChannelService = {
scanCommand$: of(1)
};
let mockSystemInfo: SystemInfo = {
'with_notary': true,
'with_admiral': false,
@ -72,7 +68,6 @@ describe('ArtifactListComponent (inline template)', () => {
],
providers: [
{ provide: ErrorHandler, useValue: fakedErrorHandler },
{ provide: ChannelService, useValue: mockChannelService },
{ provide: SystemInfoService, useClass: SystemInfoDefaultService },
{ provide: ArtifactService, useClass: ArtifactDefaultService },
{ provide: ActivatedRoute, useValue: mockActivatedRoute },

View File

@ -15,6 +15,9 @@
<div *ngIf="completed" class="bar-state bar-state-chart">
<hbr-result-tip-histogram [scanner]="getScanner()" [vulnerabilitySummary]="summary"></hbr-result-tip-histogram>
</div>
<div *ngIf="stopped" class="bar-state">
<span class="label stopped">{{'VULNERABILITY.STATE.STOPPED' | translate}}</span>
</div>
<div *ngIf="otherStatus" class="bar-state">
<span class="label not-scan">{{'VULNERABILITY.STATE.OTHER_STATUS' | translate}}</span>
</div>

View File

@ -6,7 +6,6 @@ import { HistogramChartComponent } from "./histogram-chart/histogram-chart.compo
import { JobLogDefaultService, JobLogService, ScanningResultDefaultService, ScanningResultService, } from "../../../../../shared/services";
import { VULNERABILITY_SCAN_STATUS } from "../../../../../shared/units/utils";
import { ErrorHandler } from "../../../../../shared/units/error-handler";
import { ChannelService } from "../../../../../shared/services/channel.service";
import { ArtifactDefaultService, ArtifactService } from "../artifact.service";
import { NativeReportSummary } from "../../../../../../../ng-swagger-gen/models/native-report-summary";
import { SharedTestingModule } from "../../../../../shared/shared.module";
@ -40,7 +39,6 @@ describe('ResultBarChartComponent (inline template)', () => {
HistogramChartComponent],
providers: [
ErrorHandler,
ChannelService,
ArtifactDefaultService,
{ provide: ArtifactService, useValue: ArtifactDefaultService },
{ provide: ScanningResultService, useValue: ScanningResultDefaultService },
@ -61,15 +59,14 @@ describe('ResultBarChartComponent (inline template)', () => {
it('should be created', () => {
expect(component).toBeTruthy();
});
it('should show "not scanned" if status is STOPPED', () => {
it('should show "scan stopped" 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.OTHER_STATUS');
expect(el.textContent).toEqual('VULNERABILITY.STATE.STOPPED');
});
});

View File

@ -1,13 +1,14 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, } from '@angular/core';
import { Subscription, timer } from "rxjs";
import { finalize } from "rxjs/operators";
import { ScannerVo, ScanningResultService } from "../../../../../shared/services";
import { ScannerVo } from "../../../../../shared/services";
import { ErrorHandler } from "../../../../../shared/units/error-handler";
import { ChannelService } from "../../../../../shared/services/channel.service";
import { clone, CURRENT_BASE_HREF, dbEncodeURIComponent, VULNERABILITY_SCAN_STATUS } from "../../../../../shared/units/utils";
import { ArtifactService } from "../../../../../../../ng-swagger-gen/services/artifact.service";
import { Artifact } from "../../../../../../../ng-swagger-gen/models/artifact";
import { NativeReportSummary } from "../../../../../../../ng-swagger-gen/models/native-report-summary";
import { EventService, HarborEvent } from "../../../../../services/event-service/event.service";
import { ScanService } from "../../../../../../../ng-swagger-gen/services/scan.service";
const STATE_CHECK_INTERVAL: number = 3000; // 3s
@ -25,20 +26,25 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
@Input() artifactDigest: string = "";
@Input() summary: NativeReportSummary;
onSubmitting: boolean = false;
onStopping: boolean = false;
retryCounter: number = 0;
stateCheckTimer: Subscription;
scanSubscription: Subscription;
stopSubscription: Subscription;
timerHandler: any;
@Output()
submitFinish: EventEmitter<boolean> = new EventEmitter<boolean>();
// if sending stop scan request is finished, emit to farther component
@Output()
submitStopFinish: EventEmitter<boolean> = new EventEmitter<boolean>();
@Output()
scanFinished: EventEmitter<Artifact> = new EventEmitter<Artifact>();
constructor(
private artifactService: ArtifactService,
private scanningService: ScanningResultService,
private scanService: ScanService,
private errorHandler: ErrorHandler,
private channel: ChannelService,
private eventService: EventService,
) { }
ngOnInit(): void {
@ -50,12 +56,22 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
this.getSummary();
});
}
this.scanSubscription = this.channel.scanCommand$.subscribe((artifactDigest: string) => {
let myFullTag: string = this.repoName + "/" + this.artifactDigest;
if (myFullTag === artifactDigest) {
this.scanNow();
}
});
if (!this.scanSubscription) {
this.scanSubscription = this.eventService.subscribe( HarborEvent.START_SCAN_ARTIFACT, (artifactDigest: string) => {
let myFullTag: string = this.repoName + "/" + this.artifactDigest;
if (myFullTag === artifactDigest) {
this.scanNow();
}
});
}
if (!this.stopSubscription) {
this.stopSubscription = this.eventService.subscribe( HarborEvent.STOP_SCAN_ARTIFACT, (artifactDigest: string) => {
let myFullTag: string = this.repoName + "/" + this.artifactDigest;
if (myFullTag === artifactDigest) {
this.stopScan();
}
});
}
}
ngOnDestroy(): void {
@ -65,6 +81,11 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
}
if (this.scanSubscription) {
this.scanSubscription.unsubscribe();
this.scanSubscription = null;
}
if (!this.stopSubscription) {
this.stopSubscription.unsubscribe();
this.stopSubscription = null;
}
}
@ -91,8 +112,11 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
public get scanning(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.RUNNING;
}
public get stopped(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.STOPPED;
}
public get otherStatus(): boolean {
return !(this.completed || this.error || this.queued || this.scanning);
return !(this.completed || this.error || this.queued || this.scanning || this.stopped);
}
scanNow(): void {
@ -109,8 +133,11 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
this.onSubmitting = true;
this.scanningService.startVulnerabilityScanning(this.projectName, dbEncodeURIComponent(this.repoName), this.artifactDigest)
.pipe(finalize(() => this.submitFinish.emit(false)))
this.scanService.scanArtifact({
projectName: this.projectName,
reference: this.artifactDigest,
repositoryName: dbEncodeURIComponent(this.repoName)
}).pipe(finalize(() => this.submitFinish.emit(false)))
.subscribe(() => {
this.onSubmitting = false;
// Forcely change status to queued after successful submitting
@ -157,7 +184,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
}
this.scanFinished.emit(artifact);
}
this.channel.ArtifactDetail$.next(artifact);
this.eventService.publish(HarborEvent.UPDATE_VULNERABILITY_INFO, artifact);
}, error => {
this.errorHandler.error(error);
this.retryCounter++;
@ -186,4 +213,38 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
}
return this.inputScanner;
}
stopScan() {
if (this.onStopping) {
// Avoid duplicated stopping command
console.log("duplicated stopping command");
return;
}
if (!this.repoName || !this.artifactDigest) {
console.log("bad repository or artifact");
return;
}
this.onStopping = true;
this.scanService.stopScanArtifact({
projectName: this.projectName,
reference: this.artifactDigest,
repositoryName: dbEncodeURIComponent(this.repoName)
}).pipe(
finalize(() => {
this.submitStopFinish.emit(false);
this.onStopping = false;
}))
.subscribe(() => {
// Start check status util the job is done
if (!this.stateCheckTimer) {
// Avoid duplicated subscribing
this.stateCheckTimer = timer(STATE_CHECK_INTERVAL, STATE_CHECK_INTERVAL).subscribe(() => {
this.getSummary();
});
}
this.errorHandler.info('VULNERABILITY.TRIGGER_STOP_SUCCESS');
}, error => {
this.errorHandler.error(error);
});
}
}

View File

@ -168,4 +168,7 @@ hbr-vulnerability-bar {
.label,.not-scan {
width: 90%;
}
}
}
.stopped {
border-color: #cccc15;
}

View File

@ -76,5 +76,8 @@ export class EventService {
export enum HarborEvent {
SCROLL = 'scroll',
SCROLL_TO_POSITION = 'scrollToPosition',
REFRESH_PROJECT_INFO = 'refreshProjectInfo'
REFRESH_PROJECT_INFO = 'refreshProjectInfo',
START_SCAN_ARTIFACT = 'startScanArtifact',
STOP_SCAN_ARTIFACT = 'stopScanArtifact',
UPDATE_VULNERABILITY_INFO = 'UpdateVulnerabilityInfo'
}

View File

@ -1,12 +1,9 @@
import { waitForAsync, ComponentFixture, TestBed } from "@angular/core/testing";
import { ImageNameInputComponent } from "./image-name-input.component";
import { ErrorHandler } from "../../units/error-handler/error-handler";
import { ProjectDefaultService, ProjectService } from "../../services";
import { Project } from "../../../base/project/project-config/project-policy-config/project";
import { of } from "rxjs";
import { HttpResponse } from "@angular/common/http";
import { ChannelService } from "../../services/channel.service";
import { CURRENT_BASE_HREF } from "../../units/utils";
import { SharedTestingModule } from "../../shared.module";
describe("ImageNameInputComponent (inline template)", () => {
@ -35,8 +32,6 @@ describe("ImageNameInputComponent (inline template)", () => {
ImageNameInputComponent
],
providers: [
ErrorHandler,
ChannelService,
{ provide: ProjectService, useClass: ProjectDefaultService }
]
});

View File

@ -1,31 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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 { Injectable } from '@angular/core';
import { Subject } from "rxjs";
import { ArtifactFront as Artifact } from "../../base/project/repository/artifact/artifact";
@Injectable({
providedIn: 'root',
})
export class ChannelService {
// Declare for publishing scan event
scanCommandSource = new Subject<string>();
scanCommand$ = this.scanCommandSource.asObservable();
publishScanEvent(tagId: string): void {
this.scanCommandSource.next(tagId);
}
ArtifactDetail$ = new Subject<Artifact>();
}

View File

@ -965,6 +965,7 @@
"OIDC_ADMIN_GROUP_INFO": "Spezifiziere den Namen einer OIDC Administratorengruppe. Alle Mitglieder dieser Gruppe haben in Harbor administrative Berechtigungen. Falls dies nicht gewünscht ist, kann das Feld leer gelassen werden."
},
"SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"TRIGGER_SCAN_ALL_SUCCESS": "Scan erfolgreich gestartet!",
"TRIGGER_SCAN_ALL_FAIL": "Fehler beim Start des Scans: {{error}}",
"TITLE": "Schwachstellen-Scanning",
@ -1035,7 +1036,8 @@
"OTHER_STATUS": "Nicht gescannt",
"QUEUED": "In Warteschlange",
"ERROR": "Log anzeigen",
"SCANNING": "Scanning"
"SCANNING": "Scanning",
"STOPPED": "Scan stopped"
},
"GRID": {
"PLACEHOLDER": "Es wurden keine Scan-Ergebnisse gefunden!",
@ -1078,7 +1080,9 @@
"SCAN_NOW": "Scan",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
"NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
},
"PUSH_IMAGE": {
"TITLE": "Push Befehl",
@ -1697,6 +1701,7 @@
"FINAL_PROJECT_NAME_TIP": "Der zusammengesetzte Robot-Account-Name besteht aus dem Prefix, dem Projektnamen, einem plus und dem Inhalt des Eingabefeldes.",
"FINAL_SYSTEM_NAME_TIP": "Der zusammengesetzte systemweite Robot-Account-Name besteht aus dem Prefix und dem Inhalt des Eingabefeldes.",
"PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission"
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission",
"STOP": "Stop"
}
}

View File

@ -965,6 +965,7 @@
"OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to."
},
"SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!",
"TRIGGER_SCAN_ALL_FAIL": "Failed to trigger scan all with error: {{error}}",
"TITLE": "Vulnerability Scanning",
@ -1035,7 +1036,8 @@
"OTHER_STATUS": "Not Scanned",
"QUEUED": "Queued",
"ERROR": "View Log",
"SCANNING": "Scanning"
"SCANNING": "Scanning",
"STOPPED": "Scan stopped"
},
"GRID": {
"PLACEHOLDER": "We couldn't find any scanning results!",
@ -1078,7 +1080,9 @@
"SCAN_NOW": "Scan",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
"NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
},
"PUSH_IMAGE": {
"TITLE": "Push Command",
@ -1697,6 +1701,7 @@
"FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value",
"FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value",
"PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission"
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission",
"STOP": "Stop"
}
}

View File

@ -966,6 +966,7 @@
"OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to."
},
"SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"TRIGGER_SCAN_ALL_SUCCESS": "Trigger scan all successfully!",
"TRIGGER_SCAN_ALL_FAIL": "Failed to trigger scan all with error: {{error}}",
"TITLE": "Vulnerability Scanning",
@ -1036,7 +1037,8 @@
"OTHER_STATUS": "Not Scanned",
"QUEUED": "Queued",
"ERROR": "View Log",
"SCANNING": "Scanning"
"SCANNING": "Scanning",
"STOPPED": "Scan stopped"
},
"GRID": {
"PLACEHOLDER": "We couldn't find any scanning results!",
@ -1079,7 +1081,9 @@
"SCAN_NOW": "Scan",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
"NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
},
"PUSH_IMAGE": {
"TITLE": "Push Command",
@ -1696,6 +1700,7 @@
"FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value",
"FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value",
"PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission"
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission",
"STOP": "Stop"
}
}

View File

@ -938,6 +938,7 @@
"OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to."
},
"SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"TRIGGER_SCAN_ALL_SUCCESS": "Déclenchement d'analyse globale avec succès !",
"TRIGGER_SCAN_ALL_FAIL": "Echec du déclenchement d'analyse globale avec des erreurs : {{error}}",
"TITLE": "Analyse de vulnérabilité",
@ -1008,7 +1009,8 @@
"OTHER_STATUS": "Non Analysé",
"QUEUED": "En fil d'attente",
"ERROR": "Voir le Log",
"SCANNING": "En cours d'analyse"
"SCANNING": "En cours d'analyse",
"STOPPED": "Scan stopped"
},
"GRID": {
"PLACEHOLDER": "Nous n'avons pas trouvé de résultats d'analyse !",
@ -1051,7 +1053,9 @@
"SCAN_NOW": "Analyser",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
"NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
},
"PUSH_IMAGE": {
"TITLE": "Push Command",
@ -1665,6 +1669,7 @@
"FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value",
"FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value",
"PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission"
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission",
"STOP": "Stop"
}
}

View File

@ -961,6 +961,7 @@
"OIDC_ADMIN_GROUP_INFO": "Informe o nome do grupo OIDC que será considerado administrativo. Todos os usuários deste grupo obterão privilégios de adiministração no Harbor. Deixe vazio para ser ignorado."
},
"SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"TRIGGER_SCAN_ALL_SUCCESS": "Disparo de análise geral efetuado com sucesso!",
"TRIGGER_SCAN_ALL_FAIL": "Falha ao disparar análise geral com erro: {{error}}",
"TITLE": "Análise de vulnerabilidades",
@ -1031,7 +1032,8 @@
"OTHER_STATUS": "Não analisado",
"QUEUED": "Solicitado",
"ERROR": "Ver erros...",
"SCANNING": "Analisando"
"SCANNING": "Analisando",
"STOPPED": "Scan stopped"
},
"GRID": {
"PLACEHOLDER": "Não foi possível encontrar nenhum resultado de análise!",
@ -1074,7 +1076,9 @@
"SCAN_NOW": "Analisar",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Relatado por {{scanner}}",
"NO_SCANNER": "NO SCANNER"
"NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
},
"PUSH_IMAGE": {
"TITLE": "Comando Push",
@ -1693,7 +1697,8 @@
"FINAL_PROJECT_NAME_TIP": "Para evitar conflitos entre projetos, o nome completo será composto pelo nome do projeto concatenado a um marcador e o valor aqui informado.",
"FINAL_SYSTEM_NAME_TIP": "Este valor será concatenado ao prefixo do projeto.",
"PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Permissões de envio (push) presume também a permissão e recebimento (pull)."
"PUSH_PERMISSION_TOOLTIP": "Permissões de envio (push) presume também a permissão e recebimento (pull).",
"STOP": "Stop"
}
}

View File

@ -965,6 +965,7 @@
"OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to."
},
"SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"TRIGGER_SCAN_ALL_SUCCESS": "Tümünü başarılı bir şekilde tara!",
"TRIGGER_SCAN_ALL_FAIL": "Tüm taramayı hatayla tetikleyemedi:{{error}}",
"TITLE": "Güvenlik açığı taraması",
@ -1035,7 +1036,8 @@
"OTHER_STATUS": "Taranmadı",
"QUEUED": "Sıraya alındı",
"ERROR": "Günlüğü Görüntüle",
"SCANNING": "Taranıyor"
"SCANNING": "Taranıyor",
"STOPPED": "Scan stopped"
},
"GRID": {
"PLACEHOLDER": "Herhangi bir tarama sonucu bulamadık!",
@ -1078,7 +1080,9 @@
"SCAN_NOW": "Tara",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
"NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
},
"PUSH_IMAGE": {
"TITLE": "Push Command",
@ -1697,6 +1701,7 @@
"FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value",
"FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value",
"PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission"
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission",
"STOP": "Stop"
}
}

View File

@ -966,6 +966,7 @@
"OIDC_ADMIN_GROUP_INFO": "OIDC管理员组名称。所有该组内用户都会有管理员权限此属性可以为空。"
},
"SCANNING": {
"STOP_SCAN_ALL_SUCCESS": "停止扫描所有镜像任务成功!",
"TRIGGER_SCAN_ALL_SUCCESS": "启动扫描所有镜像任务成功!",
"TRIGGER_SCAN_ALL_FAIL": "启动扫描所有镜像任务失败:{{error}}",
"TITLE": "缺陷扫描",
@ -1036,7 +1037,8 @@
"OTHER_STATUS": "未扫描",
"QUEUED": "已入队列",
"ERROR": "查看日志",
"SCANNING": "扫描中"
"SCANNING": "扫描中",
"STOPPED": "扫描中止"
},
"GRID": {
"PLACEHOLDER": "没有扫描结果!",
@ -1079,7 +1081,9 @@
"SCAN_NOW": "扫描",
"SCAN_BY": "使用 {{scanner}} 进行扫描",
"REPORTED_BY": "结果由 {{scanner}} 提供",
"NO_SCANNER": "无扫描器"
"NO_SCANNER": "无扫描器",
"TRIGGER_STOP_SUCCESS": "停止扫描成功",
"STOP_NOW": "停止扫描"
},
"PUSH_IMAGE": {
"TITLE": "推送命令",
@ -1695,6 +1699,7 @@
"FINAL_PROJECT_NAME_TIP": "项目级机器人的最终名称由前缀,项目名称,一个加号以及当前输入值组成",
"FINAL_SYSTEM_NAME_TIP": "系统级机器人的最终名称由前缀和当前输入值组成",
"PUSH_AND_PULL": "推送",
"PUSH_PERMISSION_TOOLTIP": "推送权限无法单独工作,请在选择推送权限的时,确保已经勾选了拉取权限"
"PUSH_PERMISSION_TOOLTIP": "推送权限无法单独工作,请在选择推送权限的时,确保已经勾选了拉取权限",
"STOP": "停止"
}
}

View File

@ -961,6 +961,7 @@
"OIDC_ADMIN_GROUP_INFO": "Specify an OIDC admin group name. All OIDC users in this group will have harbor admin privilege. Keep it blank if you do not want to."
},
"SCANNING":{
"STOP_SCAN_ALL_SUCCESS": "Trigger stopping scan all successfully!",
"TRIGGER_SCAN_ALL_SUCCESS": "啟動掃描所有鏡像任務成功!",
"TRIGGER_SCAN_ALL_FAIL": "啟動掃描所有鏡像任務失敗:{{error}}",
"TITLE": "缺陷掃描",
@ -1031,7 +1032,8 @@
"OTHER_STATUS": "未掃描",
"QUEUED": "已入隊列",
"ERROR": "查看日誌",
"SCANNING": "掃描中"
"SCANNING": "掃描中",
"STOPPED": "Scan stopped"
},
"GRID":{
"PLACEHOLDER": "沒有掃描結果!",
@ -1074,7 +1076,9 @@
"SCAN_NOW":"掃描",
"SCAN_BY": "SCAN BY {{scanner}}",
"REPORTED_BY": "Reported by {{scanner}}",
"NO_SCANNER": "NO SCANNER"
"NO_SCANNER": "NO SCANNER",
"TRIGGER_STOP_SUCCESS": "Trigger stopping scan successfully",
"STOP_NOW": "Stop Scan"
},
"PUSH_IMAGE":{
"TITLE": "推送鏡像的Docker命令",
@ -1682,6 +1686,7 @@
"FINAL_PROJECT_NAME_TIP": "The final project robot name consists of the prefix,the project name,a plus mark and the current input value",
"FINAL_SYSTEM_NAME_TIP": "The final system robot name consists of the prefix and the current input value",
"PUSH_AND_PULL": "Push",
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission"
"PUSH_PERMISSION_TOOLTIP": "Push permission can not work alone, it must work with pull permission",
"STOP": "Stop"
}
}