Compare commits

...

1 Commits

Author SHA1 Message Date
Marcel Haag 7b961cb8b2 feat: As a user I want to edit my comment 2022-12-28 15:47:55 +01:00
21 changed files with 500 additions and 123 deletions

View File

@ -54,5 +54,4 @@
</table>
</nb-card>
<!--ToDo: Add loading spinner after routing fix to avoid circular dependency issues -->
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>

View File

@ -9,7 +9,7 @@ import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators';
import {
Comment,
CommentDialogBody,
CommentEntry,
CommentEntry, RelatedFindingOption,
transformCommentsToObjectiveEntries,
transformCommentToRequestBody
} from '@shared/models/comment.model';
@ -17,11 +17,13 @@ import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {Store} from '@ngxs/store';
import {PentestStatus} from '@shared/models/pentest-status.model';
import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component';
import {DialogService} from '@shared/services/dialog-service/dialog.service';
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
import {CommentService} from '@shared/services/comment.service';
import {UpdatePentestComments, UpdatePentestFindings} from '@shared/stores/project-state/project-state.actions';
import {UpdatePentestComments} from '@shared/stores/project-state/project-state.actions';
import {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component';
import {Finding} from '@shared/models/finding.model';
import {FindingService} from '@shared/services/finding.service';
@UntilDestroy()
@Component({
@ -36,6 +38,7 @@ export class PentestCommentsComponent implements OnInit {
notStartedStatus: PentestStatus = PentestStatus.NOT_STARTED;
pentestInfo$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
objectiveFindings: RelatedFindingOption[] = [];
// comments$: BehaviorSubject<Comment[]> = new BehaviorSubject<Comment[]>(null);
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
@ -43,7 +46,6 @@ export class PentestCommentsComponent implements OnInit {
CommentColumns.COMMENT_ID, CommentColumns.TITLE, CommentColumns.DESCRIPTION, CommentColumns.RELATED_FINDINGS, CommentColumns.ACTIONS
];
dataSource: NbTreeGridDataSource<CommentEntry>;
data: CommentEntry[] = [];
getters: NbGetters<CommentEntry, CommentEntry> = {
@ -53,6 +55,7 @@ export class PentestCommentsComponent implements OnInit {
};
constructor(private readonly commentService: CommentService,
private readonly findingService: FindingService,
private dataSourceBuilder: NbTreeGridDataSourceBuilder<CommentEntry>,
private notificationService: NotificationService,
private dialogService: DialogService,
@ -68,6 +71,7 @@ export class PentestCommentsComponent implements OnInit {
next: (selectedPentest: Pentest) => {
this.pentestInfo$.next(selectedPentest);
this.loadCommentsData();
this.requestFindingsData(selectedPentest.id);
},
error: err => {
console.error(err);
@ -103,8 +107,9 @@ export class PentestCommentsComponent implements OnInit {
onClickAddComment(): void {
this.commentDialogService.openCommentDialog(
FindingDialogComponent,
CommentDialogComponent,
this.pentestInfo$.getValue().findingIds,
this.objectiveFindings,
null,
{
closeOnEsc: false,
@ -114,7 +119,6 @@ export class PentestCommentsComponent implements OnInit {
}
).pipe(
filter(value => !!value),
tap((value) => console.warn('CommentDialogBody: ', value)),
mergeMap((value: CommentDialogBody) =>
this.commentService.saveComment(
this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '',
@ -126,19 +130,74 @@ export class PentestCommentsComponent implements OnInit {
next: (newComment: Comment) => {
this.store.dispatch(new UpdatePentestComments(newComment.id));
this.loadCommentsData();
// Todo: Fix trans keys
this.notificationService.showPopup('comment.popup.save.success', PopupType.SUCCESS);
},
error: err => {
console.error(err);
// Todo: Fix trans keys
this.notificationService.showPopup('comment.popup.save.failed', PopupType.FAILURE);
}
});
}
onClickEditComment(commentEntry): void {
console.info('Coming soon..', commentEntry);
this.commentService.getCommentById(commentEntry.data.commentId).pipe(
filter(isNotNullOrUndefined),
untilDestroyed(this)
).subscribe({
next: (existingComment: Comment) => {
if (existingComment) {
this.commentDialogService.openCommentDialog(
CommentDialogComponent,
this.pentestInfo$.getValue().findingIds,
this.objectiveFindings,
existingComment,
{
closeOnEsc: false,
hasScroll: false,
autoFocus: false,
closeOnBackdropClick: false
}
).pipe(
filter(value => !!value),
mergeMap((value: CommentDialogBody) =>
this.commentService.updateComment(
commentEntry.data.commentId,
transformCommentToRequestBody(value)
)
),
untilDestroyed(this)
).subscribe({
next: (updatedComment: Comment) => {
this.loadCommentsData();
this.notificationService.showPopup('comment.popup.update.success', PopupType.SUCCESS);
},
error: err => {
console.error(err);
this.notificationService.showPopup('comment.popup.update.failed', PopupType.FAILURE);
}
});
} else {
this.notificationService.showPopup('comment.popup.not.available', PopupType.INFO);
}
},
error: err => {
console.error(err);
}
});
}
requestFindingsData(pentestId: string): void {
this.objectiveFindings = [];
this.findingService.getFindingsByPentestId(pentestId).pipe(
untilDestroyed(this)
).subscribe({
next: (findings: Finding[]) => {
findings.forEach(finding => this.objectiveFindings.push({id: finding.id, title: finding.title} as RelatedFindingOption));
},
error: err => {
console.error(err);
}
});
}
onClickDeleteComment(commentEntry): void {

View File

@ -163,7 +163,6 @@ export class PentestFindingsComponent implements OnInit {
untilDestroyed(this)
).subscribe({
next: (updatedFinding: Finding) => {
this.store.dispatch(new UpdatePentestFindings(updatedFinding.id));
this.loadFindingsData();
this.notificationService.showPopup('finding.popup.update.success', PopupType.SUCCESS);
},
@ -173,7 +172,7 @@ export class PentestFindingsComponent implements OnInit {
}
});
} else {
this.notificationService.showPopup('finding.popup.not.available', PopupType.FAILURE);
this.notificationService.showPopup('finding.popup.not.available', PopupType.INFO);
}
},
error: err => {

View File

@ -181,7 +181,8 @@
"update.success": "Kommentar erfolgreich aktualisiert",
"update.failed": "Kommentar konnte nicht aktualisiert werden",
"delete.success": "Kommentar erfolgreich gelöscht",
"delete.failed": "Kommentar konnte nicht gelöscht werden"
"delete.failed": "Kommentar konnte nicht gelöscht werden",
"not.available": "Kommentar ist nicht mehr verfügbar"
}
},
"pentest": {

View File

@ -181,7 +181,8 @@
"update.success": "Comment updated successfully",
"update.failed": "Comment could not be updated",
"delete.success": "Comment deleted successfully",
"delete.failed": "Comment could not be deleted"
"delete.failed": "Comment could not be deleted",
"not.available": "Comment is not available anymore"
}
},
"pentest": {

View File

@ -49,7 +49,7 @@ export function transformCommentToRequestBody(comment: CommentDialogBody | Comme
title: comment.title,
description: comment.description,
// Transforms related findings from RelatedFindingOption to list of finding ids
relatedFindings: comment.relatedFindings ? comment.relatedFindings.map(finding => finding.value.id) : [],
relatedFindings: comment.relatedFindings ? comment.relatedFindings.map(finding => finding.id) : [],
/* Remove Table Entry Object Properties */
childEntries: undefined,
kind: undefined,

View File

@ -23,4 +23,5 @@ export interface GenericFormFieldOption {
headerLabelKey: string;
buttonKey: string;
accentColor: string;
additionalData?: any;
}

View File

@ -57,13 +57,16 @@
{{formArray[2].labelKey | translate}}
</label>
<!--<fa-icon nbPrefix [icon]="fa.faExclamationCircle" size="lg" class="finding-icon"></fa-icon>-->
<nb-select placeholder="{{formArray[2].placeholder | translate}}" formControlName="{{formArray[2].fieldName}}" multiple
<nb-select placeholder="{{formArray[2].placeholder | translate}}"
id="{{formArray[2].fieldName}}"
formControlName="{{formArray[2].fieldName}}"
(selectedChange)="changeSelected($event)"
fullWidth shape="semi-round" filled status="info" [size]="'large'" class="form-field relatedFindings">
<nb-option class="reset-option">{{'global.action.reset' | translate}}</nb-option>
<nb-option class="finding-option" *ngFor="let finding of relatedFindings" [value]="{value: finding}">
{{finding.title}}
</nb-option>
multiple fullWidth shape="semi-round" filled status="info"
[size]="'large'" class="form-field relatedFindings">
<nb-option class="reset-option">{{'global.action.reset' | translate}}</nb-option>
<nb-option class="finding-option" *ngFor="let finding of relatedFindings" [value]="finding">
{{finding.title}}
</nb-option>
</nb-select>
</nb-form-field>
</form>

View File

@ -182,7 +182,8 @@ export const mockedCommentDialogData = {
{
headerLabelKey: 'comment.create.header',
buttonKey: 'global.action.save',
accentColor: 'info'
accentColor: 'info',
additionalData: []
},
]
};

View File

@ -1,18 +1,11 @@
import {ChangeDetectionStrategy, Component, Inject, OnChanges, OnInit} from '@angular/core';
import {ChangeDetectionStrategy, Component, Inject, OnInit} from '@angular/core';
import {FormBuilder, FormGroup} from '@angular/forms';
import {GenericDialogData, GenericFormFieldConfig} from '@shared/models/generic-dialog-data';
import * as FA from '@fortawesome/free-solid-svg-icons';
import deepEqual from 'deep-equal';
import {NB_DIALOG_CONFIG, NbDialogRef} from '@nebular/theme';
import {PentestService} from '@shared/services/pentest.service';
import {ProjectState} from '@shared/stores/project-state/project-state';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {Pentest} from '@shared/models/pentest.model';
import {Store} from '@ngxs/store';
import {Finding} from '@shared/models/finding.model';
import {UntilDestroy} from '@ngneat/until-destroy';
import {RelatedFindingOption} from '@shared/models/comment.model';
import {BehaviorSubject} from 'rxjs';
import {FindingService} from '@shared/services/finding.service';
@Component({
selector: 'app-comment-dialog',
@ -39,28 +32,14 @@ export class CommentDialogComponent implements OnInit {
constructor(
@Inject(NB_DIALOG_CONFIG) private data: GenericDialogData,
private fb: FormBuilder,
protected dialogRef: NbDialogRef<CommentDialogComponent>,
private readonly findingService: FindingService,
private store: Store
protected dialogRef: NbDialogRef<CommentDialogComponent>
) {
}
ngOnInit(): void {
this.commentFormGroup = this.generateFormCreationFieldArray();
this.dialogData = this.data;
// Resets related Findings input fields when comment was found in dialog context
// tslint:disable-next-line:no-string-literal
// this.commentFormGroup.controls['commentRelatedFindings'].reset('');
}
changeSelected($event): void {
// Latest Value
console.info($event[$event.length - 1].value);
// use this element at the end
// tslint:disable-next-line:no-string-literal
console.warn(this.commentFormGroup.controls['commentRelatedFindings'].value);
// tslint:disable-next-line:no-string-literal
this.selectedFindings = this.commentFormGroup.controls['commentRelatedFindings'].value;
this.relatedFindings = this.dialogData.options[0].additionalData;
this.commentFormGroup = this.generateFormCreationFieldArray();
}
generateFormCreationFieldArray(): FormGroup {
@ -70,52 +49,27 @@ export class CommentDialogComponent implements OnInit {
[currentValue?.fieldName]: currentValue?.controlsConfig
}), {});
// tslint:disable-next-line:no-string-literal
const relatedFindings = this.data.form['commentRelatedFindings'].controlsConfig[0].value;
if (relatedFindings && relatedFindings.length > 0) {
// ToDo: Select included findings here (selectedFindings / initialSelectedFindings)
console.warn('IF (EDIT)', relatedFindings);
this.renderRelatedFindings(relatedFindings);
} else {
this.renderRelatedFindings([]);
const preSelectedRelatedFindings = this.data.form['commentRelatedFindings'].controlsConfig[0].value;
if (preSelectedRelatedFindings && preSelectedRelatedFindings.length > 0) {
this.relatedFindings.forEach(finding => {
if (preSelectedRelatedFindings.includes(finding)) {
this.initialSelectedFindings.push(finding);
this.selectedFindings.push(finding);
}
});
}
return this.fb.group(config);
}
renderRelatedFindings(relatedFindings: any): void {
this.store.select(ProjectState.pentest).pipe(
untilDestroyed(this)
).subscribe({
next: (selectedPentest: Pentest) => {
// console.warn(selectedPentest.findingIds);
this.requestRelatedFindingsData(selectedPentest.id, relatedFindings);
},
error: err => {
console.error(err);
}
});
}
requestRelatedFindingsData(pentestId: string, relatedFindings: any): void {
this.findingService.getFindingsByPentestId(pentestId).pipe(
untilDestroyed(this)
).subscribe({
next: (findings: Finding[]) => {
findings.forEach(finding => this.relatedFindings.push({id: finding.id, title: finding.title} as RelatedFindingOption));
// ToDo: Only add the findings that were included in
console.info('initialSelectedFindings OnINIT ', relatedFindings);
// findings.forEach(finding => this.initialSelectedFindings.push({id: finding.id, title: finding.title} as RelatedFindingOption));
},
error: err => {
console.error(err);
}
});
changeSelected($event): void {
// tslint:disable-next-line:no-string-literal
this.selectedFindings = this.commentFormGroup.controls['commentRelatedFindings'].value;
}
onClickSave(value: any): void {
this.dialogRef.close({
title: value.commentTitle,
description: value.commentDescription,
// ToDo: Refactor this to only include the ids this.commentFormGroup.controls['commentRelatedFindings'].value
relatedFindings: this.selectedFindings ? this.selectedFindings : []
});
}
@ -141,11 +95,6 @@ export class CommentDialogComponent implements OnInit {
newCommentData[key] = '';
}
});
/* ToDo: Use for EDIT implementation
console.info('initialSelectedFindings: ', this.initialSelectedFindings);
console.info('selectedFindings: ', this.selectedFindings);
console.info('deepEqual related Findings: ', deepEqual(this.initialSelectedFindings, this.selectedFindings));
*/
const didChange = !deepEqual(oldCommentData, newCommentData) || !deepEqual(this.initialSelectedFindings, this.selectedFindings);
return didChange;
}
@ -155,17 +104,16 @@ export class CommentDialogComponent implements OnInit {
* @return parsed findingData
*/
private parseInitializedCommentDialogData(dialogData: GenericDialogData): any {
const findingData = {};
const commentData = {};
Object.entries(dialogData.form).forEach(entry => {
const [key, value] = entry;
// console.info(key);
findingData[key] = value.controlsConfig[0] ?
commentData[key] = value.controlsConfig[0] ?
(value.controlsConfig[0].value ? value.controlsConfig[0].value : value.controlsConfig[0]) : '';
// Related Findings form field can be ignored since changes here will be recognised inside commentRelatedFindings of tag-list
if (key === 'commentRelatedFindings') {
findingData[key] = '';
commentData[key] = '';
}
});
return findingData;
return commentData;
}
}

View File

@ -2,7 +2,7 @@ import {CommentDialogService} from '@shared/modules/comment-dialog/service/comme
import {ComponentType} from '@angular/cdk/overlay';
import {NbDialogConfig} from '@nebular/theme';
import {Observable, of} from 'rxjs';
import {Comment} from '@shared/models/comment.model';
import {Comment, RelatedFindingOption} from '@shared/models/comment.model';
export class CommentDialogServiceMock implements Required<CommentDialogService> {
@ -11,6 +11,7 @@ export class CommentDialogServiceMock implements Required<CommentDialogService>
openCommentDialog(
componentOrTemplateRef: ComponentType<any>,
findingIds: [],
relatedFindings: RelatedFindingOption[],
comment: Comment | undefined,
config: Partial<NbDialogConfig<Partial<any> | string>> | undefined): Observable<any> {
return of(undefined);

View File

@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import {Injectable} from '@angular/core';
import {NbDialogConfig, NbDialogService} from '@nebular/theme';
import {GenericDialogData} from '@shared/models/generic-dialog-data';
import {ComponentType} from '@angular/cdk/overlay';
import {Observable} from 'rxjs';
import {Validators} from '@angular/forms';
import {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component';
import {Comment} from '@shared/models/comment.model';
import {Comment, RelatedFindingOption} from '@shared/models/comment.model';
@Injectable()
export class CommentDialogService {
@ -30,10 +30,20 @@ export class CommentDialogService {
public openCommentDialog(componentOrTemplateRef: ComponentType<any>,
findingIds: string[],
relatedFindings: RelatedFindingOption[],
comment?: Comment,
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
let dialogOptions: Partial<NbDialogConfig<Partial<any> | string>>;
let dialogData: GenericDialogData;
// Preselect related findings
const selectedRelatedFindings: RelatedFindingOption[] = [];
if (comment && comment.relatedFindings.length > 0 && relatedFindings) {
relatedFindings.forEach(finding => {
if (comment.relatedFindings.includes(finding.id)) {
selectedRelatedFindings.push(finding);
}
});
}
// Setup CommentDialogBody
dialogData = {
form: {
@ -69,7 +79,10 @@ export class CommentDialogService {
labelKey: 'comment.relatedFindings.label',
placeholder: findingIds.length === 0 ? 'comment.noFindingsInObjectivePlaceholder' : 'comment.relatedFindingsPlaceholder',
controlsConfig: [
{value: comment ? comment.relatedFindings : [], disabled: findingIds.length === 0},
{
value: comment ? selectedRelatedFindings : [],
disabled: findingIds.length === 0
},
[]
],
errors: [
@ -84,7 +97,8 @@ export class CommentDialogService {
{
headerLabelKey: 'comment.edit.header',
buttonKey: 'global.action.update',
accentColor: 'warning'
accentColor: 'warning',
additionalData: relatedFindings
},
];
} else {
@ -92,7 +106,8 @@ export class CommentDialogService {
{
headerLabelKey: 'comment.create.header',
buttonKey: 'global.action.save',
accentColor: 'info'
accentColor: 'info',
additionalData: relatedFindings
},
];
}

View File

@ -4,6 +4,7 @@ import {HttpClient} from '@angular/common/http';
import {Store} from '@ngxs/store';
import {Observable} from 'rxjs';
import {Comment} from '@shared/models/comment.model';
import {Finding} from '@shared/models/finding.model';
@Injectable({
providedIn: 'root'
@ -26,6 +27,14 @@ export class CommentService {
return this.http.get<Comment[]>(`${this.apiBaseURL}/${pentestId}/comments`);
}
/**
* Get Comment by Id
* @param commentId the id of the comment
*/
public getCommentById(commentId: string): Observable<Comment> {
return this.http.get<Comment>(`${this.apiBaseURL}/${commentId}/comment`);
}
/**
* Save Comment
* @param pentestId the id of the pentest
@ -35,6 +44,15 @@ export class CommentService {
return this.http.post<Comment>(`${this.apiBaseURL}/${pentestId}/comment`, comment);
}
/**
* Update Comment
* @param commentId the id of the comment
* @param comment the information of the comment
*/
public updateComment(commentId: string, comment: Comment): Observable<Comment> {
return this.http.patch<Comment>(`${this.apiBaseURL}/${commentId}/comment`, comment);
}
/**
* Delete Comment
* @param pentestId the id of the pentest

View File

@ -481,7 +481,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"title\": \"Test Comment\",\n \"description\": \"Test Comment Description\",\n \"affectedUrls\": []\n}",
"raw": "{\n \"title\": \"Test Comment\",\n \"description\": \"Test Comment Description\",\n \"relatedFindings\": []\n}",
"options": {
"raw": {
"language": "json"
@ -512,7 +512,7 @@
"bearer": [
{
"key": "token",
"value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzE3MTM3MzQsImlhdCI6MTY3MTcxMzQzNCwiYXV0aF90aW1lIjoxNjcxNzEyNjkwLCJqdGkiOiJjNWYxYWZiZi1mZTczLTQ0NTAtYjA4YS1lMGEwMDcyNjMyOTgiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvYXV0aC9yZWFsbXMvYzRwb19yZWFsbV9sb2NhbCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIxMGUwNmQ3YS04ZGQwLTRlY2QtODk2My0wNTZiNDUwNzljNGYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjNHBvX2xvY2FsIiwibm9uY2UiOiIyZTYyNjNhNC1lM2U2LTRlMzUtYjQ5Yy1lMjYyNzM1ZTk2MGQiLCJzZXNzaW9uX3N0YXRlIjoiMGNmYmY4MGEtNzAxMS00NmQzLTllNGQtNTUxYWU4NTA5NjZmIiwiYWNyIjoiMCIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjNHBvX3VzZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYzRwb19sb2NhbCI6eyJyb2xlcyI6WyJ1c2VyIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoidGVzdCB1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidHR0IiwiZ2l2ZW5fbmFtZSI6InRlc3QiLCJmYW1pbHlfbmFtZSI6InVzZXIifQ.se43hq_vPjzAG6MpIxBiHb9vJHZmbLEko0tiN5m2hbhzd8s3YiBWpeiI6kgZ5kzl23iBQyMnXN4Sqpbt2ERKbKyUusezWcXhGTP22usi3b1vzFOAY9mqCI32i15sxCM2UDRYDFYcAblaKPxKsQf6EWduXpcn4L9_kQE4EpoLyWWWqFThGvFPSvkPGodffcEOz8BrnYDVUnwkodFsOWAnQmQHaR7jq1Y0hhZzWi3IlrRWlnRi0TKVWCZgUwO0PJttNq5wYZPsxgiS-khUCC1qtbKrRgBK_3sefxPkWDOQEubu0Kjyjq4rVZnq66anO3Qw82CSLn0nSCu-AL5Xd4Xchw",
"value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzIyMzc2MjQsImlhdCI6MTY3MjIzNzMyNCwianRpIjoiMTI2MTBmOWUtMjY1OC00ZmE3LTlkNTItZGU4NzNiZDVjZGE3IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI4NzQ5MTFkNS00ZTZlLTRjNmMtYmQxYy1hMzlmZjE2OTI0YWIiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.FOZNfhuWutaKUsVnr3A2jxJ6PM7fpX5BdB63SI_3CI7LFgnVmruprQC3ibhYGD77yX59yRWlGlIDlXvybd6v4tujKEL1Kuf4J-MSjj3dJcIx29PtqMe91I49MkIsjr3M24YW4bgOtdbUTYvT1l0IUisW1V_-t23qW_tsbXxviNr_9HSiJYZJZ7a47tmEptJaDZtAwjBaQc8s4BVIqiPbIcYE1Mj1Giu56C3k_v_boSxcl3rrRMXgIWTSO4TtV_jm2UfERzp82B6dZYdq0ZxeyaD0nCzXSkZ41pOeqFK4Qsm6ttCKe5OJ-RGpqvi0KH4YNuhUKHpzisVOL0cPDiWmLg",
"type": "string"
},
{
@ -574,6 +574,85 @@
}
},
"response": []
},
{
"name": "getCommentById",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzIyMzc2MjQsImlhdCI6MTY3MjIzNzMyNCwianRpIjoiMTI2MTBmOWUtMjY1OC00ZmE3LTlkNTItZGU4NzNiZDVjZGE3IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI4NzQ5MTFkNS00ZTZlLTRjNmMtYmQxYy1hMzlmZjE2OTI0YWIiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.FOZNfhuWutaKUsVnr3A2jxJ6PM7fpX5BdB63SI_3CI7LFgnVmruprQC3ibhYGD77yX59yRWlGlIDlXvybd6v4tujKEL1Kuf4J-MSjj3dJcIx29PtqMe91I49MkIsjr3M24YW4bgOtdbUTYvT1l0IUisW1V_-t23qW_tsbXxviNr_9HSiJYZJZ7a47tmEptJaDZtAwjBaQc8s4BVIqiPbIcYE1Mj1Giu56C3k_v_boSxcl3rrRMXgIWTSO4TtV_jm2UfERzp82B6dZYdq0ZxeyaD0nCzXSkZ41pOeqFK4Qsm6ttCKe5OJ-RGpqvi0KH4YNuhUKHpzisVOL0cPDiWmLg",
"type": "string"
},
{
"key": "undefined",
"type": "any"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8443/pentests/df516de6-ca5e-44a6-ac50-db89bb17aac3/comment",
"protocol": "http",
"host": [
"localhost"
],
"port": "8443",
"path": [
"pentests",
"df516de6-ca5e-44a6-ac50-db89bb17aac3",
"comment"
]
}
},
"response": []
},
{
"name": "updateComment",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzA0MTQ3ODYsImlhdCI6MTY3MDQxNDQ4NiwianRpIjoiM2FmOWU5M2MtY2YzNi00MjQwLTkzNWEtNDkxYTJkZTY2MWU4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI5M2ExNTBlMC03ZWRkLTQxZTgtYWE4Yi0yZWY5YTgzOWU4NDciLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.QjUkCInyCJ5Wsz4q56gfsLqERr6pYlGjwNw-VsKNJ_3Jp-8Dazq9UmDGN8AmAkQ0sp0b-FMm3jArKMBpr84gKd65trvQx_qHvXev5x2MWBG4_9v3C9MmjxWcAYRVmfRdURUOhfto-4YfRwMwNRsKJfwMIjfS5VT8bHJWipcCDzaidN8h_LLORbmmQZ2o0l4Jnv5qrrWzUcSTeEeBpHGOjes1-T0gOlDJa34Z9x_xrsTsybKAylrmX03mDSI-f2h5XqqtgnrxtddtHXHatfxB1BHWq-FILDsGf0UG47FEQjqapFvn9bFiNyq0GVrgdK42miEO7ywOtCOKpCfAUnMwdQ",
"type": "string"
},
{
"key": "undefined",
"type": "any"
}
]
},
"method": "PATCH",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"title\": \"Test Comment\",\n \"description\": \"Edited Test Comment Description\",\n \"relatedFindings\": []\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8443/pentests/df516de6-ca5e-44a6-ac50-db89bb17aac3/comment",
"protocol": "http",
"host": [
"localhost"
],
"port": "8443",
"path": [
"pentests",
"df516de6-ca5e-44a6-ac50-db89bb17aac3",
"comment"
]
}
},
"response": []
}
]
},

View File

@ -317,20 +317,63 @@ include::{snippets}/deleteCommentByPentestAndCommentId/http-response.adoc[]
include::{snippets}/deleteCommentByPentestAndCommentId/response-fields.adoc[]
=== Get comment for id
To get a comment by id, call the GET request /pentests/+{commentId}+/comment.
==== Request example
include::{snippets}/getCommentById/http-request.adoc[]
==== Request structure
include::{snippets}/getCommentById/path-parameters.adoc[]
==== Response example
include::{snippets}/getCommentById/http-response.adoc[]
==== Response structure
include::{snippets}/getCommentById/response-fields.adoc[]
=== Update comment
To update a comment, call the PATCH request /pentests/+{commentId}+/comment
==== Request example
include::{snippets}/updateCommentById/http-request.adoc[]
==== Request structure
include::{snippets}/updateCommentById/path-parameters.adoc[]
==== Response example
include::{snippets}/updateCommentById/http-response.adoc[]
==== Response structure
include::{snippets}/updateCommentById/response-fields.adoc[]
== Change History
|===
|Date |Change
|2022-12-28
|Added GET, PATCH endpoint for Comment
|2022-12-23
|Added DELETE endpoint for Comment
|2022-12-22
|Added GET, POST endpoint for Comment
|Added GET, POST endpoint for Comment(s)
|2022-12-09
|Added DELETE endpoint for Finding
|2022-12-08
|Added GET and PATCH endpoint for Finding
|2022-12-02
|Added GET and POST endpoint for Findings
|Added GET and POST endpoint for Finding(s)
|2022-11-21
|Added GET, POST and PATCH endpoint for Pentests
|2022-03-07

View File

@ -4,11 +4,7 @@ import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.extensions.getLoggerFor
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import com.securityc4po.api.ResponseBody
import com.securityc4po.api.configuration.error.handler.EntityNotFoundException
import com.securityc4po.api.configuration.error.handler.Errorcode
import com.securityc4po.api.pentest.finding.toFindingResponseBody
import com.securityc4po.api.pentest.PentestService
import com.securityc4po.api.pentest.finding.toFindingDeleteResponseBody
import org.springframework.http.ResponseEntity
import org.springframework.http.ResponseEntity.noContent
import org.springframework.web.bind.annotation.*
@ -40,6 +36,13 @@ class CommentController(private val pentestService: PentestService, private val
}
}
@GetMapping("/{commentId}/comment")
fun getComment(@PathVariable(value = "commentId") commentId: String):Mono<ResponseEntity<ResponseBody>> {
return this.commentService.getCommentById(commentId).map {
ResponseEntity.ok().body(it.toCommentResponseBody())
}
}
@PostMapping("/{pentestId}/comment")
fun saveComment(
@PathVariable(value = "pentestId") pentestId: String,
@ -50,8 +53,18 @@ class CommentController(private val pentestService: PentestService, private val
}
}
@PatchMapping("/{commentId}/comment")
fun updateComment(
@PathVariable(value = "commentId") commentId: String,
@RequestBody body: CommentRequestBody
): Mono<ResponseEntity<ResponseBody>> {
return this.commentService.updateComment(commentId, body).map {
ResponseEntity.accepted().body(it.toCommentResponseBody())
}
}
@DeleteMapping("/{pentestId}/comment/{commentId}")
fun deleteFinding(
fun deleteComment(
@PathVariable(value = "pentestId") pentestId: String,
@PathVariable(value = "commentId") commentId: String
): Mono<ResponseEntity<ResponseBody>> {

View File

@ -8,10 +8,12 @@ import com.securityc4po.api.configuration.error.handler.InvalidModelException
import com.securityc4po.api.configuration.error.handler.TransactionInterruptedException
import com.securityc4po.api.extensions.getLoggerFor
import com.securityc4po.api.pentest.PentestService
import com.securityc4po.api.pentest.finding.*
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.switchIfEmpty
import java.time.Instant
@Service
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
@ -19,6 +21,36 @@ class CommentService(private val commentRepository: CommentRepository, private v
var logger = getLoggerFor<CommentService>()
/**
* Get all [Comments]s by commentId's
*
* @return list of [Comment]s
*/
fun getCommentsByIds(commentIds: List<String>): Mono<List<Comment>> {
return commentRepository.findCommentsByIds(commentIds).collectList().map {
it.map { commentEntity -> commentEntity.toComment() }
}.switchIfEmpty {
val msg = "Comment not found."
val ex = EntityNotFoundException(msg, Errorcode.CommentsNotFound)
logger.warn(msg, ex)
throw ex
}
}
/**
* Get [Comment] by commentId
*
* @return of [Comment]
*/
fun getCommentById(commentId: String): Mono<Comment> {
return this.commentRepository.findCommentById(commentId).switchIfEmpty {
logger.warn("Comment with id $commentId not found.")
val msg = "Comment with id $commentId not found."
val ex = EntityNotFoundException(msg, Errorcode.CommentNotFound)
throw ex
}.map { it.toComment() }
}
/**
* Save [Comment]
*
@ -38,7 +70,7 @@ class CommentService(private val commentRepository: CommentRepository, private v
val commentEntity = CommentEntity(comment)
return commentRepository.insert(commentEntity).flatMap { newCommentEntity: CommentEntity ->
val comment = newCommentEntity.toComment()
// After successfully saving finding add id to pentest
// After successfully saving comment add id to pentest
pentestService.updatePentestComment(pentestId, comment.id).onErrorMap {
TransactionInterruptedException(
"Pentest could not be updated in Database.",
@ -59,18 +91,39 @@ class CommentService(private val commentRepository: CommentRepository, private v
}
/**
* Get all [Comments]s by commentId's
* Update [Comment]
*
* @return list of [Comment]s
* @throws [InvalidModelException] if the [Comment] is invalid
* @throws [TransactionInterruptedException] if the [Comment] could not be stored
* @return saved [Comment]
*/
fun getCommentsByIds(commentIds: List<String>): Mono<List<Comment>> {
return commentRepository.findCommentsByIds(commentIds).collectList().map {
it.map { commentEntity -> commentEntity.toComment() }
}.switchIfEmpty {
val msg = "Comment not found."
val ex = EntityNotFoundException(msg, Errorcode.CommentsNotFound)
logger.warn(msg, ex)
fun updateComment(commentId: String, body: CommentRequestBody): Mono<Comment> {
validate(
require = body.isValid(),
logging = { logger.warn("Comment not valid.") },
mappedException = InvalidModelException(
"Comment not valid.", Errorcode.CommentInvalid
)
)
return this.commentRepository.findCommentById(commentId).switchIfEmpty {
logger.warn("Comment with id $commentId not found.")
val msg = "Comment with id $commentId not found."
val ex = EntityNotFoundException(msg, Errorcode.CommentNotFound)
throw ex
}.flatMap { currentCommentEntity: CommentEntity ->
currentCommentEntity.lastModified = Instant.now()
currentCommentEntity.data = buildComment(body, currentCommentEntity)
commentRepository.save(currentCommentEntity).map {
it.toComment()
}
}.doOnError {
throw wrappedException(
logging = { logger.warn("Comment could not be updated in Database. Thrown exception: ", it) },
mappedException = TransactionInterruptedException(
"Comment could not be stored.",
Errorcode.CommentInsertionFailed
)
)
}
}

View File

@ -101,6 +101,54 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
)
}
@Nested
inner class GetComment {
@Test
fun getCommentById() {
val commentId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
webTestClient.get()
.uri("/pentests/{commentId}/comment", commentId)
.header("Authorization", "Bearer $tokenAdmin")
.exchange()
.expectStatus().isOk
.expectHeader().doesNotExist("")
.expectBody().json(Json.write(commentOne.toCommentResponseBody()))
.consumeWith(
WebTestClientRestDocumentation.document(
"{methodName}",
Preprocessors.preprocessRequest(
Preprocessors.prettyPrint(),
Preprocessors.modifyUris().removePort(),
Preprocessors.removeHeaders("Host", "Content-Length")
),
Preprocessors.preprocessResponse(
Preprocessors.prettyPrint()
),
RequestDocumentation.relaxedPathParameters(
RequestDocumentation.parameterWithName("commentId").description("The id of the comment you want to get")
),
PayloadDocumentation.relaxedResponseFields(
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
.description("The id of the requested comment"),
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
.description("The title of the requested comment"),
PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
.description("The description number of the comment"),
PayloadDocumentation.fieldWithPath("relatedFindings").type(JsonFieldType.ARRAY)
.description("List of related findings of the comment")
)
)
)
}
private val commentOne = Comment(
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
title = "Found Bug",
description = "OTG-INFO-002 Bug",
relatedFindings = emptyList()
)
}
@Nested
inner class SaveComment {
@Test
@ -149,6 +197,54 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
)
}
@Nested
inner class UpdateFinding {
@Test
fun updateCommentById() {
val commentId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
webTestClient.patch()
.uri("/pentests/{commentId}/comment", commentId)
.header("Authorization", "Bearer $tokenAdmin")
.body(Mono.just(commentBody), CommentRequestBody::class.java)
.exchange()
.expectStatus().isAccepted
.expectHeader().doesNotExist("")
.expectBody().json(Json.write(commentBody))
.consumeWith(
WebTestClientRestDocumentation.document(
"{methodName}",
Preprocessors.preprocessRequest(
Preprocessors.prettyPrint(),
Preprocessors.modifyUris().removePort(),
Preprocessors.removeHeaders("Host", "Content-Length")
),
Preprocessors.preprocessResponse(
Preprocessors.prettyPrint()
),
RequestDocumentation.relaxedPathParameters(
RequestDocumentation.parameterWithName("commentId").description("The id of the comment you want to update")
),
PayloadDocumentation.relaxedResponseFields(
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
.description("The id of the updated comment"),
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
.description("The title of the requested comment"),
PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
.description("The description number of the comment"),
PayloadDocumentation.fieldWithPath("relatedFindings").type(JsonFieldType.ARRAY)
.description("List of related findings of the comment")
)
)
)
}
private val commentBody = CommentRequestBody(
title = "Updated Comment",
description = "Updated Description",
relatedFindings = emptyList()
)
}
@Nested
inner class DeleteComment {
@Test

View File

@ -29,7 +29,7 @@ import java.time.Duration
NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
)
class CommentControllerIntegrationTest: BaseIntTest() {
class CommentControllerIntTest : BaseIntTest() {
@LocalServerPort
private var port = 0
@ -85,6 +85,28 @@ class CommentControllerIntegrationTest: BaseIntTest() {
)
}
@Nested
inner class GetComment {
@Test
fun `requesting comment by commentId successfully`() {
val commentId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
webTestClient.get()
.uri("/pentests/{commentId}/comment", commentId)
.header("Authorization", "Bearer $tokenAdmin")
.exchange()
.expectStatus().isOk
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
.expectBody().json(Json.write(commentOne.toCommentResponseBody()))
}
private val commentOne = Comment(
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
title = "Found Bug",
description = "OTG-INFO-002 Bug",
relatedFindings = emptyList()
)
}
@Nested
inner class SaveComment {
@Test
@ -110,6 +132,31 @@ class CommentControllerIntegrationTest: BaseIntTest() {
)
}
@Nested
inner class UpdateComment {
@Test
fun `update comment successfully`() {
val commentId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
webTestClient.patch()
.uri("/pentests/{commentId}/comment", commentId)
.header("Authorization", "Bearer $tokenAdmin")
.body(Mono.just(commentBody), CommentRequestBody::class.java)
.exchange()
.expectStatus().isAccepted
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
.expectBody()
.jsonPath("$.title").isEqualTo("Updated Comment")
.jsonPath("$.description").isEqualTo("Updated Description")
.jsonPath("$.relatedFindings").isEmpty
}
private val commentBody = CommentRequestBody(
title = "Updated Comment",
description = "Updated Description",
relatedFindings = emptyList()
)
}
@Nested
inner class DeleteComment {
@Test

View File

@ -137,7 +137,7 @@ class FindingControllerDocumentationTest: BaseDocumentationIntTest() {
Preprocessors.prettyPrint()
),
RequestDocumentation.relaxedPathParameters(
RequestDocumentation.parameterWithName("findingId").description("The id of the feinidng you want to get")
RequestDocumentation.parameterWithName("findingId").description("The id of the finding you want to get")
),
PayloadDocumentation.relaxedResponseFields(
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)

View File

@ -152,8 +152,8 @@ class FindingControllerIntTest: BaseIntTest() {
inner class UpdateFinding {
@Test
fun `update finding successfully`() {
val findingId = "43fbc63c-f624-11ec-b939-0242ac120002"
webTestClient.post()
val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
webTestClient.patch()
.uri("/pentests/{findingId}/finding", findingId)
.header("Authorization", "Bearer $tokenAdmin")
.body(Mono.just(findingBody), FindingRequestBody::class.java)