import {Component, OnInit} from '@angular/core'; import {PentestService} from '@shared/services/pentest.service'; import {BehaviorSubject, Observable} from 'rxjs'; import {Pentest} from '@shared/models/pentest.model'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators'; import {NotificationService, PopupType} from '@shared/services/notification.service'; import { Finding, FindingDialogBody, FindingEntry, transformFindingsToObjectiveEntries, transformFindingToRequestBody, } from '@shared/models/finding.model'; import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme'; import * as FA from '@fortawesome/free-solid-svg-icons'; import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined'; import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service'; import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component'; import {PentestStatus} from '@shared/models/pentest-status.model'; import {Store} from '@ngxs/store'; import {UpdatePentestFindings} from '@shared/stores/project-state/project-state.actions'; import {ProjectState} from '@shared/stores/project-state/project-state'; import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {FindingService} from '@shared/services/finding.service'; @UntilDestroy() @Component({ selector: 'app-pentest-findings', templateUrl: './pentest-findings.component.html', styleUrls: ['./pentest-findings.component.scss'] }) export class PentestFindingsComponent implements OnInit { constructor(private readonly findingService: FindingService, private dataSourceBuilder: NbTreeGridDataSourceBuilder, private notificationService: NotificationService, private dialogService: DialogService, private findingDialogService: FindingDialogService, private store: Store) { this.dataSource = dataSourceBuilder.create(this.data, this.getters); } pentestInfo$: BehaviorSubject = new BehaviorSubject(null); loading$: BehaviorSubject = new BehaviorSubject(true); // HTML only readonly fa = FA; notStartedStatus: PentestStatus = PentestStatus.NOT_STARTED; columns: Array = [ FindingColumns.FINDING_ID, FindingColumns.SEVERITY, FindingColumns.TITLE, FindingColumns.IMPACT, FindingColumns.ACTIONS ]; dataSource: NbTreeGridDataSource; data: FindingEntry[] = []; getters: NbGetters = { dataGetter: (node: FindingEntry) => node, childrenGetter: (node: FindingEntry) => node.childEntries || undefined, expandedGetter: (node: FindingEntry) => !!node.expanded, }; ngOnInit(): void { this.store.select(ProjectState.pentest).pipe( untilDestroyed(this) ).subscribe({ next: (selectedPentest: Pentest) => { this.pentestInfo$.next(selectedPentest); this.loadFindingsData(); }, error: err => { console.error(err); } }); } loadFindingsData(): void { this.findingService.getFindingsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '') .pipe( untilDestroyed(this), /*filter(isNotNullOrUndefined),*/ tap(() => this.loading$.next(true)) ) .subscribe({ next: (findings: Finding[]) => { // ToDo: Handle this case before in pipe if (findings) { this.data = transformFindingsToObjectiveEntries(findings); } else { this.data = []; } this.dataSource.setData(this.data, this.getters); this.loading$.next(false); }, error: err => { console.error(err); // ToDo: Implement again after proper lazy loading and routing // this.notificationService.showPopup('findings.popup.not.found', PopupType.FAILURE); this.loading$.next(false); } }); } onClickAddFinding(): void { this.findingDialogService.openFindingDialog( FindingDialogComponent, null, { closeOnEsc: false, hasScroll: false, autoFocus: true, closeOnBackdropClick: false } ).pipe( filter(value => !!value), /*tap((value) => console.warn('FindingDialogBody: ', value)),*/ mergeMap((value: FindingDialogBody) => this.findingService.saveFinding( this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', transformFindingToRequestBody(value) ) ), untilDestroyed(this) ).subscribe({ next: (newFinding: Finding) => { this.store.dispatch(new UpdatePentestFindings(newFinding.id)); this.loadFindingsData(); this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS); }, error: err => { console.error(err); this.notificationService.showPopup('finding.popup.save.failed', PopupType.FAILURE); } }); } onClickEditFinding(findingEntry): void { this.findingService.getFindingById(findingEntry.data.findingId).pipe( filter(isNotNullOrUndefined), untilDestroyed(this) ).subscribe({ next: (existingFinding: Finding) => { if (existingFinding) { this.findingDialogService.openFindingDialog( FindingDialogComponent, existingFinding, { closeOnEsc: false, hasScroll: false, autoFocus: true, closeOnBackdropClick: false } ).pipe( filter(value => !!value), /*tap((value) => console.warn('FindingDialogBody: ', value)),*/ mergeMap((value: FindingDialogBody) => this.findingService.updateFinding( findingEntry.data.findingId, transformFindingToRequestBody(value) ) ), untilDestroyed(this) ).subscribe({ next: (updatedFinding: Finding) => { this.loadFindingsData(); this.notificationService.showPopup('finding.popup.update.success', PopupType.SUCCESS); }, error: err => { console.error(err); this.notificationService.showPopup('finding.popup.update.failed', PopupType.FAILURE); } }); } else { this.notificationService.showPopup('finding.popup.not.available', PopupType.INFO); } }, error: err => { console.error(err); } }); } onClickDeleteFinding(findingEntry): void { const message = { title: 'finding.delete.title', key: 'finding.delete.key', data: {name: findingEntry.data.title}, }; this.dialogService.openConfirmDialog( message ).onClose.pipe( filter((confirm) => !!confirm), switchMap(() => this.findingService.deleteFindingByPentestAndFindingId( this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', findingEntry.data.findingId) ), catchError(() => { this.notificationService.showPopup('finding.popup.delete.failed', PopupType.FAILURE); return []; }), untilDestroyed(this) ).subscribe({ next: (deletedFinding: any) => { this.store.dispatch(new UpdatePentestFindings(deletedFinding.id)); this.loadFindingsData(); this.notificationService.showPopup('finding.popup.delete.success', PopupType.SUCCESS); }, error: error => { console.error(error); } }); } isLoading(): Observable { return this.loading$.asObservable(); } } enum FindingColumns { FINDING_ID = 'findingId', SEVERITY = 'severity', TITLE = 'title', IMPACT = 'impact', ACTIONS = 'actions' }