harbor/src/portal/src/app/base/project/repository/artifact/vulnerability-scanning/result-bar-chart.component.ts

326 lines
11 KiB
TypeScript

import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
} from '@angular/core';
import { Subscription, timer } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { ScannerVo } from '../../../../../shared/services';
import { ErrorHandler } from '../../../../../shared/units/error-handler';
import {
clone,
CURRENT_BASE_HREF,
dbEncodeURIComponent,
DEFAULT_SUPPORTED_MIME_TYPES,
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';
import { ScanType } from 'ng-swagger-gen/models';
import { ScanTypes } from '../../../../../shared/entities/shared.const';
const STATE_CHECK_INTERVAL: number = 3000; // 3s
const RETRY_TIMES: number = 3;
@Component({
selector: 'hbr-vulnerability-bar',
templateUrl: './result-bar-chart-component.html',
styleUrls: ['./scanning.scss'],
})
export class ResultBarChartComponent implements OnInit, OnDestroy {
@Input() inputScanner: ScannerVo;
@Input() repoName: string = '';
@Input() projectName: string = '';
@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 scanService: ScanService,
private errorHandler: ErrorHandler,
private eventService: EventService
) {}
ngOnInit(): void {
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();
}
);
}
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 {
if (this.stateCheckTimer) {
this.stateCheckTimer.unsubscribe();
this.stateCheckTimer = null;
}
if (this.scanSubscription) {
this.scanSubscription.unsubscribe();
this.scanSubscription = null;
}
if (this.stopSubscription) {
this.stopSubscription.unsubscribe();
this.stopSubscription = null;
}
}
// Get vulnerability scanning status
public get status(): string {
if (this.summary && this.summary.scan_status) {
return this.summary.scan_status;
}
return VULNERABILITY_SCAN_STATUS.NOT_SCANNED;
}
public get completed(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.SUCCESS;
}
public get error(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.ERROR;
}
public get queued(): boolean {
return this.status === VULNERABILITY_SCAN_STATUS.PENDING;
}
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 ||
this.stopped
);
}
scanNow(): void {
if (this.onSubmitting) {
// Avoid duplicated submitting
console.error('duplicated submit');
return;
}
if (!this.repoName || !this.artifactDigest) {
console.error('bad repository or tag');
return;
}
this.onSubmitting = true;
this.scanService
.scanArtifact({
projectName: this.projectName,
reference: this.artifactDigest,
repositoryName: dbEncodeURIComponent(this.repoName),
scanType: <ScanType>{
scan_type: ScanTypes.VULNERABILITY,
},
})
.pipe(finalize(() => this.submitFinish.emit(false)))
.subscribe(
() => {
this.onSubmitting = false;
// Forcely change status to queued after successful submitting
this.summary = {
scan_status: VULNERABILITY_SCAN_STATUS.PENDING,
};
// 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();
});
}
},
error => {
this.onSubmitting = false;
if (error && error.error && error.error.code === 409) {
console.error(error.error.message);
} else {
this.errorHandler.error(error);
}
}
);
}
getSummary(): void {
if (!this.repoName || !this.artifactDigest) {
return;
}
this.artifactService
.getArtifact({
projectName: this.projectName,
repositoryName: dbEncodeURIComponent(this.repoName),
reference: this.artifactDigest,
withScanOverview: true,
XAcceptVulnerabilities: DEFAULT_SUPPORTED_MIME_TYPES,
})
.subscribe(
(artifact: Artifact) => {
// To keep the same summary reference, use value copy.
if (artifact.scan_overview) {
this.copyValue(
Object.values(artifact.scan_overview)[0]
);
}
if (!this.queued && !this.scanning) {
// Scanning should be done
if (this.stateCheckTimer) {
this.stateCheckTimer.unsubscribe();
this.stateCheckTimer = null;
}
this.scanFinished.emit(artifact);
}
this.eventService.publish(
HarborEvent.UPDATE_VULNERABILITY_INFO,
artifact
);
},
error => {
this.errorHandler.error(error);
this.retryCounter++;
if (this.retryCounter >= RETRY_TIMES) {
// Stop timer
if (this.stateCheckTimer) {
this.stateCheckTimer.unsubscribe();
this.stateCheckTimer = null;
}
this.retryCounter = 0;
}
}
);
}
copyValue(newVal: NativeReportSummary): void {
if (!this.summary || !newVal || !newVal.scan_status) {
return;
}
this.summary = clone(newVal);
}
viewLog(): string {
return `${CURRENT_BASE_HREF}/projects/${
this.projectName
}/repositories/${dbEncodeURIComponent(this.repoName)}/artifacts/${
this.artifactDigest
}/scan/${this.summary.report_id}/log`;
}
getScanner(): ScannerVo {
if (this.summary && this.summary.scanner) {
return this.summary.scanner;
}
return this.inputScanner;
}
stopScan() {
if (this.onStopping) {
// Avoid duplicated stopping command
console.error('duplicated stopping command');
return;
}
if (!this.repoName || !this.artifactDigest) {
console.error('bad repository or artifact');
return;
}
this.onStopping = true;
this.scanService
.stopScanArtifact({
projectName: this.projectName,
reference: this.artifactDigest,
repositoryName: dbEncodeURIComponent(this.repoName),
scanType: <ScanType>{
scan_type: ScanTypes.VULNERABILITY,
},
})
.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);
}
);
}
}