226 lines
7.8 KiB
TypeScript
226 lines
7.8 KiB
TypeScript
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<FindingEntry>,
|
|
private notificationService: NotificationService,
|
|
private dialogService: DialogService,
|
|
private findingDialogService: FindingDialogService,
|
|
private store: Store) {
|
|
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
|
|
}
|
|
|
|
pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
|
|
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
|
|
|
// HTML only
|
|
readonly fa = FA;
|
|
notStartedStatus: PentestStatus = PentestStatus.NOT_STARTED;
|
|
|
|
columns: Array<FindingColumns> = [
|
|
FindingColumns.FINDING_ID, FindingColumns.SEVERITY, FindingColumns.TITLE, FindingColumns.IMPACT, FindingColumns.ACTIONS
|
|
];
|
|
dataSource: NbTreeGridDataSource<FindingEntry>;
|
|
|
|
data: FindingEntry[] = [];
|
|
|
|
getters: NbGetters<FindingEntry, FindingEntry> = {
|
|
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<boolean> {
|
|
return this.loading$.asObservable();
|
|
}
|
|
}
|
|
|
|
enum FindingColumns {
|
|
FINDING_ID = 'findingId',
|
|
SEVERITY = 'severity',
|
|
TITLE = 'title',
|
|
IMPACT = 'impact',
|
|
ACTIONS = 'actions'
|
|
}
|