Compare commits
2 Commits
main
...
c4po_mhg_1
Author | SHA1 | Date |
---|---|---|
|
7faa769b63 | |
|
f75751fe38 |
|
@ -18,6 +18,10 @@ import {NotificationService} from '@shared/services/notification.service';
|
|||
import {NotificationServiceMock} from '@shared/services/notification.service.mock';
|
||||
import {MockComponent} from 'ng-mocks';
|
||||
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
|
||||
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
|
||||
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
|
||||
import {CommentDialogServiceMock} from '@shared/modules/comment-dialog/service/comment-dialog.service.mock';
|
||||
|
||||
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
|
||||
selectedProject: {
|
||||
|
@ -74,7 +78,9 @@ describe('PentestCommentsComponent', () => {
|
|||
NgxsModule.forRoot([ProjectState])
|
||||
],
|
||||
providers: [
|
||||
{provide: NotificationService, useValue: new NotificationServiceMock()}
|
||||
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
||||
{provide: DialogService, useClass: DialogServiceMock},
|
||||
{provide: CommentDialogService, useClass: CommentDialogServiceMock},
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
|
|
@ -3,15 +3,25 @@ import {BehaviorSubject, Observable} from 'rxjs';
|
|||
import {Pentest} from '@shared/models/pentest.model';
|
||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
|
||||
import {PentestService} from '@shared/services/pentest.service';
|
||||
import {NotificationService} from '@shared/services/notification.service';
|
||||
import {NotificationService, PopupType} from '@shared/services/notification.service';
|
||||
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
||||
import {filter, tap} from 'rxjs/operators';
|
||||
import {Comment, CommentEntry, transformCommentsToObjectiveEntries} from '@shared/models/comment.model';
|
||||
import {filter, mergeMap, tap} from 'rxjs/operators';
|
||||
import {
|
||||
Comment,
|
||||
CommentDialogBody,
|
||||
CommentEntry,
|
||||
transformCommentsToObjectiveEntries,
|
||||
transformCommentToRequestBody
|
||||
} from '@shared/models/comment.model';
|
||||
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';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
|
@ -42,10 +52,12 @@ export class PentestCommentsComponent implements OnInit {
|
|||
expandedGetter: (node: CommentEntry) => !!node.expanded,
|
||||
};
|
||||
|
||||
constructor(private readonly pentestService: PentestService,
|
||||
constructor(private readonly commentService: CommentService,
|
||||
private dataSourceBuilder: NbTreeGridDataSourceBuilder<CommentEntry>,
|
||||
private store: Store,
|
||||
private notificationService: NotificationService) {
|
||||
private notificationService: NotificationService,
|
||||
private dialogService: DialogService,
|
||||
private commentDialogService: CommentDialogService,
|
||||
private store: Store) {
|
||||
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
|
||||
}
|
||||
|
||||
|
@ -64,7 +76,7 @@ export class PentestCommentsComponent implements OnInit {
|
|||
}
|
||||
|
||||
loadCommentsData(): void {
|
||||
this.pentestService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '')
|
||||
this.commentService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '')
|
||||
.pipe(
|
||||
untilDestroyed(this),
|
||||
/*filter(isNotNullOrUndefined),*/
|
||||
|
@ -90,15 +102,47 @@ export class PentestCommentsComponent implements OnInit {
|
|||
}
|
||||
|
||||
onClickAddComment(): void {
|
||||
console.info('Coming soon..');
|
||||
this.commentDialogService.openCommentDialog(
|
||||
FindingDialogComponent,
|
||||
this.pentestInfo$.getValue().findingIds,
|
||||
null,
|
||||
{
|
||||
closeOnEsc: false,
|
||||
hasScroll: false,
|
||||
autoFocus: false,
|
||||
closeOnBackdropClick: false
|
||||
}
|
||||
).pipe(
|
||||
filter(value => !!value),
|
||||
tap((value) => console.warn('CommentDialogBody: ', value)),
|
||||
mergeMap((value: CommentDialogBody) =>
|
||||
this.commentService.saveComment(
|
||||
this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '',
|
||||
transformCommentToRequestBody(value)
|
||||
)
|
||||
),
|
||||
untilDestroyed(this)
|
||||
).subscribe({
|
||||
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(comment): void {
|
||||
console.info('Coming soon..');
|
||||
onClickEditComment(commentEntry): void {
|
||||
console.info('Coming soon..', commentEntry);
|
||||
}
|
||||
|
||||
onClickDeleteComment(comment): void {
|
||||
console.info('Coming soon..');
|
||||
onClickDeleteComment(commentEntry): void {
|
||||
console.info('Coming soon..', commentEntry);
|
||||
}
|
||||
|
||||
// HTML only
|
||||
|
|
|
@ -20,6 +20,8 @@ import {Category} from '@shared/models/category.model';
|
|||
import {PentestStatus} from '@shared/models/pentest-status.model';
|
||||
import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service';
|
||||
import {FindingDialogServiceMock} from '@shared/modules/finding-dialog/service/finding-dialog.service.mock';
|
||||
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
|
||||
|
||||
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
|
||||
selectedProject: {
|
||||
|
@ -77,6 +79,7 @@ describe('PentestFindingsComponent', () => {
|
|||
],
|
||||
providers: [
|
||||
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
||||
{provide: DialogService, useClass: DialogServiceMock},
|
||||
{provide: FindingDialogService, useClass: FindingDialogServiceMock},
|
||||
]
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@ 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 {filter, mergeMap, tap} from 'rxjs/operators';
|
||||
import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators';
|
||||
import {NotificationService, PopupType} from '@shared/services/notification.service';
|
||||
import {
|
||||
Finding,
|
||||
|
@ -21,6 +21,7 @@ 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';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
|
@ -33,6 +34,7 @@ export class PentestFindingsComponent implements OnInit {
|
|||
constructor(private readonly pentestService: PentestService,
|
||||
private dataSourceBuilder: NbTreeGridDataSourceBuilder<FindingEntry>,
|
||||
private notificationService: NotificationService,
|
||||
private dialogService: DialogService,
|
||||
private findingDialogService: FindingDialogService,
|
||||
private store: Store) {
|
||||
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
|
||||
|
@ -180,7 +182,32 @@ export class PentestFindingsComponent implements OnInit {
|
|||
}
|
||||
|
||||
onClickDeleteFinding(findingEntry): void {
|
||||
console.info('Coming soon..', findingEntry.data.findingId);
|
||||
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.pentestService.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: () => {
|
||||
this.loadFindingsData();
|
||||
this.notificationService.showPopup('finding.popup.delete.success', PopupType.SUCCESS);
|
||||
}, error: error => {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isLoading(): Observable<boolean> {
|
||||
|
|
|
@ -3,18 +3,19 @@ import {CommonModule} from '@angular/common';
|
|||
import {RouterModule} from '@angular/router';
|
||||
import {PentestComponent} from './pentest.component';
|
||||
import {NbButtonModule, NbCardModule, NbLayoutModule, NbSelectModule, NbTabsetModule, NbTreeGridModule} from '@nebular/theme';
|
||||
import { PentestHeaderComponent } from './pentest-header/pentest-header.component';
|
||||
import { PentestContentComponent } from './pentest-content/pentest-content.component';
|
||||
import {PentestHeaderComponent} from './pentest-header/pentest-header.component';
|
||||
import {PentestContentComponent} from './pentest-content/pentest-content.component';
|
||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {StatusTagModule} from '@shared/widgets/status-tag/status-tag.module';
|
||||
import { PentestInfoComponent } from './pentest-content/pentest-info/pentest-info.component';
|
||||
import { PentestFindingsComponent } from './pentest-content/pentest-findings/pentest-findings.component';
|
||||
import { PentestCommentsComponent } from './pentest-content/pentest-comments/pentest-comments.component';
|
||||
import {PentestInfoComponent} from './pentest-content/pentest-info/pentest-info.component';
|
||||
import {PentestFindingsComponent} from './pentest-content/pentest-findings/pentest-findings.component';
|
||||
import {PentestCommentsComponent} from './pentest-content/pentest-comments/pentest-comments.component';
|
||||
import {CommonAppModule} from '../common-app.module';
|
||||
import {SeverityTagModule} from '@shared/widgets/severity-tag/severity-tag.module';
|
||||
import {FindingDialogModule} from '@shared/modules/finding-dialog/finding-dialog.module';
|
||||
import {CommentDialogModule} from '@shared/modules/comment-dialog/comment-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -42,8 +43,10 @@ import {FindingDialogModule} from '@shared/modules/finding-dialog/finding-dialog
|
|||
NbTabsetModule,
|
||||
NbTreeGridModule,
|
||||
SeverityTagModule,
|
||||
NbSelectModule,
|
||||
// Dialog Modules
|
||||
FindingDialogModule,
|
||||
NbSelectModule
|
||||
CommentDialogModule,
|
||||
]
|
||||
})
|
||||
export class PentestModule {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"action.exit": "Beenden",
|
||||
"action.update": "Speichern",
|
||||
"action.export": "Exportieren",
|
||||
"action.reset": "Zurücksetzen",
|
||||
"action.yes": "Ja",
|
||||
"action.no": "Nein",
|
||||
"username": "Nutzername",
|
||||
|
@ -148,10 +149,31 @@
|
|||
"commentId": "Kommentar Id",
|
||||
"title": "Titel",
|
||||
"description": "Beschreibung",
|
||||
"relatedFindings": "Verwandte Funde",
|
||||
"relatedFindings": "Verbundene Funde",
|
||||
"add": "Kommentar hinzufügen",
|
||||
"no.relatedFindings": "Nicht verbunden mit Fund",
|
||||
"add.finding": "Fund hinzufügen",
|
||||
"no.comments": "Keine Kommentare verfügbar",
|
||||
"no.relatedFindings": "Nicht verbunden mit Fund",
|
||||
"relatedFindingsPlaceholder": "Fund auswählen",
|
||||
"noFindingsInObjectivePlaceholder": "Objective hat keine Befunde, auf die es sich beziehen könnte.",
|
||||
"create": {
|
||||
"header": "Neuen Kommentar erstellen"
|
||||
},
|
||||
"edit": {
|
||||
"header": "Kommentar editieren"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Kommentar löschen",
|
||||
"key": "Möchten Sie den Kommentar \"{{name}}\" unwiderruflich löschen?"
|
||||
},
|
||||
"title.label": "Kommentartitel",
|
||||
"description.label": "Beschreibung des Kommentars",
|
||||
"relatedFindings.label": "Verbundene Funde",
|
||||
"validationMessage": {
|
||||
"titleRequired": "Titel ist erforderlich.",
|
||||
"descriptionRequired": "Beschreibung ist erforderlich.",
|
||||
"relatedFindings": "Verwandte Funde erforderlich."
|
||||
},
|
||||
"popup": {
|
||||
"not.found": "Keine Kommentare gefunden",
|
||||
"save.success": "Kommentar erfolgreich gespeichert",
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"action.exit": "Exit",
|
||||
"action.update": "Update",
|
||||
"action.export": "Export",
|
||||
"action.reset": "Reset",
|
||||
"action.yes": "Yes",
|
||||
"action.no": "No",
|
||||
"username": "Username",
|
||||
|
@ -150,8 +151,29 @@
|
|||
"description": "Description",
|
||||
"relatedFindings": "Related Findings",
|
||||
"add": "Add Comment",
|
||||
"add.finding": "Add related finding",
|
||||
"no.comments": "No comments available",
|
||||
"no.relatedFindings": "Not related to finding",
|
||||
"relatedFindingsPlaceholder": "Select a related finding",
|
||||
"noFindingsInObjectivePlaceholder": "Objective doesn't have any findings to relate to.",
|
||||
"create": {
|
||||
"header": "Create New Comment"
|
||||
},
|
||||
"edit": {
|
||||
"header": "Edit Comment"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete Comment",
|
||||
"key": "Do you want to permanently delete the comment \"{{name}}\"?"
|
||||
},
|
||||
"title.label": "Comment Title",
|
||||
"description.label": "Description of Comment",
|
||||
"relatedFindings.label": "Related Findings",
|
||||
"validationMessage": {
|
||||
"titleRequired": "Title is required.",
|
||||
"descriptionRequired": "Description is required.",
|
||||
"relatedFindings": "Related findings required."
|
||||
},
|
||||
"popup": {
|
||||
"not.found": "No comment found",
|
||||
"save.success": "Comment saved successfully",
|
||||
|
|
|
@ -42,3 +42,30 @@ export function transformCommentsToObjectiveEntries(findings: Comment[]): Commen
|
|||
});
|
||||
return findingEntries;
|
||||
}
|
||||
|
||||
export function transformCommentToRequestBody(comment: CommentDialogBody | Comment): Comment {
|
||||
const transformedComment = {
|
||||
...comment,
|
||||
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) : [],
|
||||
/* Remove Table Entry Object Properties */
|
||||
childEntries: undefined,
|
||||
kind: undefined,
|
||||
findings: undefined,
|
||||
expanded: undefined,
|
||||
} as unknown as Comment;
|
||||
return transformedComment;
|
||||
}
|
||||
|
||||
export interface CommentDialogBody {
|
||||
title: string;
|
||||
description: string;
|
||||
relatedFindings: Array<RelatedFindingOption>;
|
||||
}
|
||||
|
||||
export interface RelatedFindingOption {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
<nb-card #dialog accent="{{dialogData?.options[0].accentColor}}" class="comment-dialog">
|
||||
<nb-card-header fxLayoutAlign="start center" class="dialog-header">
|
||||
{{ dialogData?.options[0].headerLabelKey | translate }}
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<form *ngIf="formArray" [formGroup]="commentFormGroup">
|
||||
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||
<!-- Form Text Layout -->
|
||||
<div fxLayout="column" fxFlex fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||
<!-- Title Form Field -->
|
||||
<nb-form-field class="comment-form-field">
|
||||
<label for="{{formArray[0].fieldName}}" class="label">
|
||||
{{formArray[0].labelKey | translate}}
|
||||
</label>
|
||||
<input formControlName="{{formArray[0].fieldName}}"
|
||||
type="formText" required fullWidth
|
||||
id="{{formArray[0].fieldName}}" nbInput
|
||||
class="form-field form-text"
|
||||
[status]="commentFormGroup.get(formArray[0].fieldName).dirty ? (commentFormGroup.get(formArray[0].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||
placeholder="{{formArray[0].placeholder | translate}} *">
|
||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||
<ng-template ngFor let-error [ngForOf]="formArray[0].errors"
|
||||
*ngIf="commentFormGroup.get(formArray[0].fieldName).dirty">
|
||||
<span class="error-text"
|
||||
*ngIf="commentFormGroup.get(formArray[0].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||
{{error.translationKey | translate}}
|
||||
</span>
|
||||
</ng-template>
|
||||
</nb-form-field>
|
||||
<!-- Description Form Field -->
|
||||
<nb-form-field class="comment-form-field">
|
||||
<label for="{{formArray[1].fieldName}}" class="label">
|
||||
{{formArray[1].labelKey | translate}}
|
||||
</label>
|
||||
<textarea formControlName="{{formArray[1].fieldName}}"
|
||||
type="formText" required fullWidth
|
||||
id="{{formArray[1].fieldName}}" nbInput
|
||||
class="form-field form-textarea"
|
||||
[status]="commentFormGroup.get(formArray[1].fieldName).dirty ? (commentFormGroup.get(formArray[1].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||
placeholder="{{formArray[1].placeholder | translate}} *">
|
||||
</textarea>
|
||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||
<ng-template ngFor let-error [ngForOf]="formArray[1].errors"
|
||||
*ngIf="commentFormGroup.get(formArray[1].fieldName).dirty">
|
||||
<span class="error-text"
|
||||
*ngIf="commentFormGroup.get(formArray[1].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||
{{error.translationKey | translate}}
|
||||
</span>
|
||||
</ng-template>
|
||||
</nb-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Related Findings Layout -->
|
||||
<!-- Related Findings Form Field -->
|
||||
<nb-form-field class="comment-form-field">
|
||||
<label for="{{formArray[2].fieldName}}" class="label">
|
||||
{{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
|
||||
(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>
|
||||
</nb-select>
|
||||
</nb-form-field>
|
||||
</form>
|
||||
</nb-card-body>
|
||||
<nb-card-footer fxLayout="row" fxLayoutGap="1.5rem" fxLayoutAlign="end end">
|
||||
<button nbButton status="success" size="small" class="dialog-button" [disabled]="!allowSave()"
|
||||
(click)="onClickSave(commentFormGroup.value)">
|
||||
{{ dialogData?.options[0].buttonKey | translate}}
|
||||
</button>
|
||||
<button nbButton status="danger" size="small" class="dialog-button" (click)="onClickClose()">
|
||||
{{ 'global.action.cancel' | translate }}
|
||||
</button>
|
||||
</nb-card-footer>
|
||||
</nb-card>
|
|
@ -0,0 +1,65 @@
|
|||
@import "../../../assets/@theme/styles/_dialog.scss";
|
||||
@import '../../../assets/@theme/styles/themes';
|
||||
|
||||
.comment-dialog {
|
||||
width: 45.25rem !important;
|
||||
height: 45rem;
|
||||
|
||||
.comment-dialog-header {
|
||||
height: 8vh;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
nb-form-field {
|
||||
padding: 0.5rem 0 0.75rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
font-weight: normal;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
width: 42rem !important;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 42rem !important;
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
.comment-form-field {
|
||||
align-items: center;
|
||||
|
||||
.finding-icon {
|
||||
color: nb-theme(color-danger-default);
|
||||
}
|
||||
}
|
||||
|
||||
.relatedFindings {
|
||||
// font-family: Courier, serif;
|
||||
// background-color: nb-theme(card-header-basic-background-color);
|
||||
width: 100% !important;
|
||||
|
||||
.finding-option {
|
||||
color: nb-theme(color-danger-default) !important;
|
||||
background-color: nb-theme(color-danger-default) !important;
|
||||
}
|
||||
|
||||
.reset-option {
|
||||
color: nb-theme(color-danger-default) !important;
|
||||
background-color: nb-theme(color-danger-default) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
float: left;
|
||||
color: nb-theme(color-danger-default);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {CommentDialogComponent} from './comment-dialog.component';
|
||||
import {PROJECT_STATE_NAME, ProjectState, ProjectStateModel} from '@shared/stores/project-state/project-state';
|
||||
import {Category} from '@shared/models/category.model';
|
||||
import {PentestStatus} from '@shared/models/pentest-status.model';
|
||||
import {NgxsModule, Store} from '@ngxs/store';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {
|
||||
NB_DIALOG_CONFIG,
|
||||
NbButtonModule,
|
||||
NbCardModule,
|
||||
NbDialogRef,
|
||||
NbFormFieldModule,
|
||||
NbInputModule,
|
||||
NbLayoutModule, NbSelectModule,
|
||||
NbTagModule
|
||||
} from '@nebular/theme';
|
||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||
import {ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {ThemeModule} from '@assets/@theme/theme.module';
|
||||
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||
import {HttpLoaderFactory} from '../../../app/common-app.module';
|
||||
import {HttpClient, HttpClientModule} from '@angular/common/http';
|
||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||
import {NotificationService} from '@shared/services/notification.service';
|
||||
import {NotificationServiceMock} from '@shared/services/notification.service.mock';
|
||||
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||
import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock';
|
||||
import Mock = jest.Mock;
|
||||
import {Finding} from '@shared/models/finding.model';
|
||||
import {Severity} from '@shared/models/severity.enum';
|
||||
import {Comment} from '@shared/models/comment.model';
|
||||
|
||||
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
|
||||
selectedProject: {
|
||||
id: '56c47c56-3bcd-45f1-a05b-c197dbd33111',
|
||||
client: 'E Corp',
|
||||
title: 'Some Mock API (v1.0) Scanning',
|
||||
createdAt: new Date('2019-01-10T09:00:00'),
|
||||
tester: 'Novatester',
|
||||
testingProgress: 0,
|
||||
createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110'
|
||||
},
|
||||
// Manages Categories
|
||||
disabledCategories: [],
|
||||
selectedCategory: Category.INFORMATION_GATHERING,
|
||||
// Manages Pentests of Category
|
||||
disabledPentests: [],
|
||||
selectedPentest: {
|
||||
id: '56c47c56-3bcd-45f1-a05b-c197dbd33112',
|
||||
category: Category.INFORMATION_GATHERING,
|
||||
refNumber: 'OTF-001',
|
||||
childEntries: [],
|
||||
status: PentestStatus.NOT_STARTED,
|
||||
findingIds: [],
|
||||
commentIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112']
|
||||
},
|
||||
};
|
||||
|
||||
describe('CommentDialogComponent', () => {
|
||||
let component: CommentDialogComponent;
|
||||
let fixture: ComponentFixture<CommentDialogComponent>;
|
||||
let store: Store;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dialogSpy = createSpyObj('NbDialogRef', ['close']);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
CommentDialogComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
NbLayoutModule,
|
||||
NbCardModule,
|
||||
NbButtonModule,
|
||||
FlexLayoutModule,
|
||||
NbInputModule,
|
||||
NbFormFieldModule,
|
||||
NbTagModule,
|
||||
NbSelectModule,
|
||||
ReactiveFormsModule,
|
||||
BrowserAnimationsModule,
|
||||
ThemeModule.forRoot(),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient]
|
||||
}
|
||||
}),
|
||||
NgxsModule.forRoot([ProjectState]),
|
||||
HttpClientModule,
|
||||
HttpClientTestingModule
|
||||
],
|
||||
providers: [
|
||||
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
||||
{provide: DialogService, useClass: DialogServiceMock},
|
||||
{provide: NbDialogRef, useValue: dialogSpy},
|
||||
{provide: NB_DIALOG_CONFIG, useValue: mockedCommentDialogData}
|
||||
]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedCommentDialogData});
|
||||
fixture = TestBed.createComponent(CommentDialogComponent);
|
||||
store = TestBed.inject(Store);
|
||||
store.reset({
|
||||
...store.snapshot(),
|
||||
[PROJECT_STATE_NAME]: DESIRED_PROJECT_STATE_SESSION
|
||||
});
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
export const createSpyObj = (baseName, methodNames): { [key: string]: Mock<any> } => {
|
||||
const obj: any = {};
|
||||
for (const i of methodNames) {
|
||||
obj[i] = jest.fn();
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
export const mockComment: Comment = {
|
||||
id: '11-22-33',
|
||||
title: 'Test Finding',
|
||||
description: 'Test Description',
|
||||
relatedFindings: ['68c47c56-3bcd-45f1-a05b-c197dbd33224']
|
||||
};
|
||||
|
||||
export const mockedCommentDialogData = {
|
||||
form: {
|
||||
commentTitle: {
|
||||
fieldName: 'commentTitle',
|
||||
type: 'formText',
|
||||
labelKey: 'comment.title.label',
|
||||
placeholder: 'comment.title',
|
||||
controlsConfig: [
|
||||
{value: mockComment ? mockComment.title : '', disabled: false},
|
||||
[Validators.required]
|
||||
],
|
||||
errors: [
|
||||
{errorCode: 'required', translationKey: 'comment.validationMessage.titleRequired'}
|
||||
]
|
||||
},
|
||||
commentDescription: {
|
||||
fieldName: 'commentDescription',
|
||||
type: 'formText',
|
||||
labelKey: 'comment.description.label',
|
||||
placeholder: 'comment.description',
|
||||
controlsConfig: [
|
||||
{value: mockComment ? mockComment.description : '', disabled: false},
|
||||
[Validators.required]
|
||||
],
|
||||
errors: [
|
||||
{errorCode: 'required', translationKey: 'comment.validationMessage.descriptionRequired'}
|
||||
]
|
||||
},
|
||||
commentRelatedFindings: {
|
||||
fieldName: 'commentRelatedFindings',
|
||||
type: 'text',
|
||||
labelKey: 'comment.relatedFindings.label',
|
||||
placeholder: 'comment.relatedFindingsPlaceholder',
|
||||
controlsConfig: [
|
||||
{value: mockComment ? mockComment.relatedFindings : [], disabled: false},
|
||||
[]
|
||||
],
|
||||
errors: [
|
||||
{errorCode: 'required', translationKey: 'finding.validationMessage.relatedFindings'}
|
||||
]
|
||||
}
|
||||
},
|
||||
options: [
|
||||
{
|
||||
headerLabelKey: 'comment.create.header',
|
||||
buttonKey: 'global.action.save',
|
||||
accentColor: 'info'
|
||||
},
|
||||
]
|
||||
};
|
|
@ -0,0 +1,170 @@
|
|||
import {ChangeDetectionStrategy, Component, Inject, OnChanges, 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 {RelatedFindingOption} from '@shared/models/comment.model';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-comment-dialog',
|
||||
templateUrl: './comment-dialog.component.html',
|
||||
styleUrls: ['./comment-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
@UntilDestroy()
|
||||
export class CommentDialogComponent implements OnInit {
|
||||
// form control elements
|
||||
commentFormGroup: FormGroup;
|
||||
formArray: GenericFormFieldConfig[];
|
||||
|
||||
dialogData: GenericDialogData;
|
||||
|
||||
// HTML only
|
||||
readonly fa = FA;
|
||||
|
||||
relatedFindings: RelatedFindingOption[] = [];
|
||||
// Includes the findings that got selected as an option
|
||||
selectedFindings: RelatedFindingOption[] = [];
|
||||
initialSelectedFindings: RelatedFindingOption[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(NB_DIALOG_CONFIG) private data: GenericDialogData,
|
||||
private fb: FormBuilder,
|
||||
protected dialogRef: NbDialogRef<CommentDialogComponent>,
|
||||
private readonly pentestService: PentestService,
|
||||
private store: Store
|
||||
) {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
generateFormCreationFieldArray(): FormGroup {
|
||||
this.formArray = Object.values(this.data.form);
|
||||
const config = this.formArray?.reduce((accumulator: {}, currentValue: GenericFormFieldConfig) => ({
|
||||
...accumulator,
|
||||
[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([]);
|
||||
}
|
||||
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.pentestService.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 : []
|
||||
});
|
||||
}
|
||||
|
||||
onClickClose(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
allowSave(): boolean {
|
||||
return this.commentFormGroup.valid && this.commentDataChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if comment data is different from initial value
|
||||
*/
|
||||
private commentDataChanged(): boolean {
|
||||
const oldCommentData = this.parseInitializedCommentDialogData(this.dialogData);
|
||||
const newCommentData = this.commentFormGroup.getRawValue();
|
||||
Object.entries(newCommentData).forEach(entry => {
|
||||
const [key, value] = entry;
|
||||
// Related Findings form field can be ignored since changes here will be recognised inside commentRelatedFindings of tag-list
|
||||
if (value === null || key === 'commentRelatedFindings') {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dialogData of type GenericDialogData
|
||||
* @return parsed findingData
|
||||
*/
|
||||
private parseInitializedCommentDialogData(dialogData: GenericDialogData): any {
|
||||
const findingData = {};
|
||||
Object.entries(dialogData.form).forEach(entry => {
|
||||
const [key, value] = entry;
|
||||
// console.info(key);
|
||||
findingData[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] = '';
|
||||
}
|
||||
});
|
||||
return findingData;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component';
|
||||
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
|
||||
import {CommonAppModule} from '../../../app/common-app.module';
|
||||
import {NbButtonModule, NbCardModule, NbFormFieldModule, NbInputModule, NbSelectModule, NbTagModule} from '@nebular/theme';
|
||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {ReactiveFormsModule} from '@angular/forms';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CommentDialogComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
CommonAppModule,
|
||||
NbCardModule,
|
||||
NbButtonModule,
|
||||
NbFormFieldModule,
|
||||
NbInputModule,
|
||||
FlexLayoutModule,
|
||||
FontAwesomeModule,
|
||||
TranslateModule,
|
||||
NbSelectModule,
|
||||
NbTagModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
providers: [
|
||||
CommentDialogService,
|
||||
],
|
||||
entryComponents: [
|
||||
CommentDialogComponent
|
||||
]
|
||||
})
|
||||
export class CommentDialogModule {
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service';
|
||||
import {ComponentType} from '@angular/cdk/overlay';
|
||||
import {NbDialogConfig} from '@nebular/theme';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {Comment} from '@shared/models/comment.model';
|
||||
|
||||
export class CommentDialogServiceMock implements Required<CommentDialogService> {
|
||||
|
||||
dialog: any;
|
||||
|
||||
openCommentDialog(
|
||||
componentOrTemplateRef: ComponentType<any>,
|
||||
findingIds: [],
|
||||
comment: Comment | undefined,
|
||||
config: Partial<NbDialogConfig<Partial<any> | string>> | undefined): Observable<any> {
|
||||
return of(undefined);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommentDialogService } from './comment-dialog.service';
|
||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {NbDialogModule, NbDialogRef} from '@nebular/theme';
|
||||
import {CommentDialogServiceMock} from '@shared/modules/comment-dialog/service/comment-dialog.service.mock';
|
||||
|
||||
describe('CommentDialogService', () => {
|
||||
let service: CommentDialogService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
BrowserAnimationsModule,
|
||||
NbDialogModule.forChild()
|
||||
],
|
||||
providers: [
|
||||
{provide: CommentDialogService, useClass: CommentDialogServiceMock},
|
||||
{provide: NbDialogRef, useValue: {}},
|
||||
]
|
||||
});
|
||||
service = TestBed.inject(CommentDialogService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
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';
|
||||
|
||||
@Injectable()
|
||||
export class CommentDialogService {
|
||||
|
||||
constructor(private readonly dialog: NbDialogService) {
|
||||
}
|
||||
|
||||
private readonly MIN_LENGTH: number = 4;
|
||||
|
||||
static addDataToDialogConfig(
|
||||
dialogOptions?: Partial<NbDialogConfig<Partial<any> | string>>,
|
||||
commentData?: GenericDialogData
|
||||
): Partial<NbDialogConfig<Partial<any> | string>> {
|
||||
return {
|
||||
context: {data: commentData},
|
||||
closeOnEsc: dialogOptions?.closeOnEsc || false,
|
||||
hasScroll: dialogOptions?.hasScroll || false,
|
||||
autoFocus: dialogOptions?.autoFocus || false,
|
||||
closeOnBackdropClick: dialogOptions?.closeOnBackdropClick || false
|
||||
};
|
||||
}
|
||||
|
||||
public openCommentDialog(componentOrTemplateRef: ComponentType<any>,
|
||||
findingIds: string[],
|
||||
comment?: Comment,
|
||||
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
|
||||
let dialogOptions: Partial<NbDialogConfig<Partial<any> | string>>;
|
||||
let dialogData: GenericDialogData;
|
||||
// Setup CommentDialogBody
|
||||
dialogData = {
|
||||
form: {
|
||||
commentTitle: {
|
||||
fieldName: 'commentTitle',
|
||||
type: 'formText',
|
||||
labelKey: 'comment.title.label',
|
||||
placeholder: 'comment.title',
|
||||
controlsConfig: [
|
||||
{value: comment ? comment.title : '', disabled: false},
|
||||
[Validators.required]
|
||||
],
|
||||
errors: [
|
||||
{errorCode: 'required', translationKey: 'comment.validationMessage.titleRequired'}
|
||||
]
|
||||
},
|
||||
commentDescription: {
|
||||
fieldName: 'commentDescription',
|
||||
type: 'formText',
|
||||
labelKey: 'comment.description.label',
|
||||
placeholder: 'comment.description',
|
||||
controlsConfig: [
|
||||
{value: comment ? comment.description : '', disabled: false},
|
||||
[Validators.required]
|
||||
],
|
||||
errors: [
|
||||
{errorCode: 'required', translationKey: 'comment.validationMessage.descriptionRequired'}
|
||||
]
|
||||
},
|
||||
commentRelatedFindings: {
|
||||
fieldName: 'commentRelatedFindings',
|
||||
type: 'text',
|
||||
labelKey: 'comment.relatedFindings.label',
|
||||
placeholder: findingIds.length === 0 ? 'comment.noFindingsInObjectivePlaceholder' : 'comment.relatedFindingsPlaceholder',
|
||||
controlsConfig: [
|
||||
{value: comment ? comment.relatedFindings : [], disabled: findingIds.length === 0},
|
||||
[]
|
||||
],
|
||||
errors: [
|
||||
{errorCode: 'required', translationKey: 'finding.validationMessage.relatedFindings'}
|
||||
]
|
||||
}
|
||||
},
|
||||
options: []
|
||||
};
|
||||
if (comment) {
|
||||
dialogData.options = [
|
||||
{
|
||||
headerLabelKey: 'comment.edit.header',
|
||||
buttonKey: 'global.action.update',
|
||||
accentColor: 'warning'
|
||||
},
|
||||
];
|
||||
} else {
|
||||
dialogData.options = [
|
||||
{
|
||||
headerLabelKey: 'comment.create.header',
|
||||
buttonKey: 'global.action.save',
|
||||
accentColor: 'info'
|
||||
},
|
||||
];
|
||||
}
|
||||
// Merge dialog config with finding data
|
||||
dialogOptions = CommentDialogService.addDataToDialogConfig(config, dialogData);
|
||||
return this.dialog.open(CommentDialogComponent, dialogOptions).onClose;
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@
|
|||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<form *ngIf="formArray" [formGroup]="findingFormGroup">
|
||||
<div>
|
||||
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||
<!-- Form Text Layout -->
|
||||
<div fxLayout="column" fxFlex="50" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||
|
@ -119,7 +118,8 @@
|
|||
</button>
|
||||
<!---->
|
||||
<nb-tag-list (tagRemove)="onAffectedUrlTagRemove($event)" class="url-tag-list">
|
||||
<nb-tag status="info" appearance="outline" class="url-tag" removable *ngFor="let url of affectedUrls" [text]="url"></nb-tag>
|
||||
<nb-tag status="info" appearance="outline" class="url-tag" removable *ngFor="let url of affectedUrls"
|
||||
[text]="url"></nb-tag>
|
||||
</nb-tag-list>
|
||||
<!-- Additional Text Layout -->
|
||||
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||
|
@ -166,7 +166,6 @@
|
|||
</ng-template>
|
||||
</nb-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</nb-card-body>
|
||||
<nb-card-footer fxLayout="row" fxLayoutGap="1.5rem" fxLayoutAlign="end end">
|
||||
|
|
|
@ -96,13 +96,13 @@ describe('FindingDialogComponent', () => {
|
|||
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
||||
{provide: DialogService, useClass: DialogServiceMock},
|
||||
{provide: NbDialogRef, useValue: dialogSpy},
|
||||
{provide: NB_DIALOG_CONFIG, useValue: mockedFindingDialogData}
|
||||
{provide: NB_DIALOG_CONFIG, useValue: mockedCommentDialogData}
|
||||
]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedFindingDialogData});
|
||||
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedCommentDialogData});
|
||||
fixture = TestBed.createComponent(FindingDialogComponent);
|
||||
store = TestBed.inject(Store);
|
||||
store.reset({
|
||||
|
@ -137,7 +137,7 @@ export const mockFinding: Finding = {
|
|||
mitigation: 'Mitigation Test'
|
||||
};
|
||||
|
||||
export const mockedFindingDialogData = {
|
||||
export const mockedCommentDialogData = {
|
||||
form: {
|
||||
findingTitle: {
|
||||
fieldName: 'findingTitle',
|
||||
|
|
|
@ -6,7 +6,6 @@ import deepEqual from 'deep-equal';
|
|||
import {UntilDestroy} from '@ngneat/until-destroy';
|
||||
import {Severity} from '@shared/models/severity.enum';
|
||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component';
|
||||
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||
import {
|
||||
NbButtonModule,
|
||||
NbCardModule,
|
||||
NbDialogModule,
|
||||
NbDialogService,
|
||||
NbFormFieldModule,
|
||||
NbInputModule,
|
||||
NbSelectModule,
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommentService } from './comment.service';
|
||||
import {HttpClientTestingModule} from '@angular/common/http/testing';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {NgxsModule} from '@ngxs/store';
|
||||
import {ProjectState} from '@shared/stores/project-state/project-state';
|
||||
|
||||
describe('CommentService', () => {
|
||||
let service: CommentService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
BrowserAnimationsModule,
|
||||
NgxsModule.forRoot([ProjectState])
|
||||
]
|
||||
});
|
||||
service = TestBed.inject(CommentService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {environment} from '../../environments/environment';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Store} from '@ngxs/store';
|
||||
import {Observable} from 'rxjs';
|
||||
import {Comment} from '@shared/models/comment.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CommentService {
|
||||
|
||||
private apiBaseURL = `${environment.apiEndpoint}/pentests`;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private readonly store: Store) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get Comments for Pentest Id
|
||||
* @param pentestId the id of the project
|
||||
*/
|
||||
public getCommentsByPentestId(pentestId: string): Observable<Comment[]> {
|
||||
return this.http.get<Comment[]>(`${this.apiBaseURL}/${pentestId}/comments`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Comment
|
||||
* @param pentestId the id of the pentest
|
||||
* @param comment the information of the comment
|
||||
*/
|
||||
public saveComment(pentestId: string, comment: Comment): Observable<Comment> {
|
||||
return this.http.post<Comment>(`${this.apiBaseURL}/${pentestId}/comment`, comment);
|
||||
}
|
||||
}
|
|
@ -114,26 +114,11 @@ export class PentestService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get Comments for Pentest Id
|
||||
* @param pentestId the id of the project
|
||||
* Delete Finding
|
||||
* @param pentestId the id of the pentest
|
||||
* @param findingId the id of the finding
|
||||
*/
|
||||
public getCommentsByPentestId(pentestId: string): Observable<Comment[]> {
|
||||
return this.http.get<Comment[]>(`${this.apiBaseURL}/${pentestId}/comments`);
|
||||
// return of([]);
|
||||
/* ToDo: Use mocked Comments?
|
||||
return of([
|
||||
{
|
||||
id: 'ca96cc19-88ff-4874-8406-dc892620afd2',
|
||||
title: 'This is a creative title',
|
||||
description: 'This is a creative description',
|
||||
relatedFindings: ['ca96cc19-88ff-4874-8406-dc892620afd4'],
|
||||
},
|
||||
{
|
||||
id: 'ca96cc19-88ff-4874-8406-dc892620afd4',
|
||||
title: 'This is a creative title',
|
||||
description: 'This is a creative description',
|
||||
relatedFindings: [],
|
||||
}
|
||||
]);*/
|
||||
public deleteFindingByPentestAndFindingId(pentestId: string, findingId: string): Observable<string> {
|
||||
return this.http.delete<string>(`${this.apiBaseURL}/${pentestId}/finding/${findingId}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,3 +40,10 @@ export class UpdatePentestFindings {
|
|||
constructor(public findingId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdatePentestComments {
|
||||
static readonly type = '[ProjectState] UpdatePentestComments';
|
||||
|
||||
constructor(public commentId: string) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
ChangeCategory,
|
||||
ChangePentest,
|
||||
ChangeProject,
|
||||
InitProjectState,
|
||||
InitProjectState, UpdatePentestComments,
|
||||
UpdatePentestFindings
|
||||
} from '@shared/stores/project-state/project-state.actions';
|
||||
import {Category} from '@shared/models/category.model';
|
||||
|
@ -108,4 +108,26 @@ export class ProjectState {
|
|||
selectedPentest: stateSelectedPentest
|
||||
});
|
||||
}
|
||||
|
||||
@Action(UpdatePentestComments)
|
||||
updatePentestComments(ctx: StateContext<ProjectStateModel>, {commentId}: UpdatePentestComments): void {
|
||||
const state = ctx.getState();
|
||||
let stateSelectedPentest: Pentest = state.selectedPentest;
|
||||
const stateCommentIds: Array<string> = stateSelectedPentest.commentIds || [];
|
||||
let updatedCommentIds: Array<string> = [];
|
||||
if (!stateCommentIds.includes(commentId)) {
|
||||
updatedCommentIds = [...stateCommentIds, commentId];
|
||||
} else {
|
||||
// ToDo: Add logic to remove commentId from array
|
||||
}
|
||||
// overwrites only findingIds
|
||||
stateSelectedPentest = {
|
||||
...stateSelectedPentest,
|
||||
commentIds: updatedCommentIds
|
||||
};
|
||||
// path project state
|
||||
ctx.patchState({
|
||||
selectedPentest: stateSelectedPentest
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -418,6 +418,126 @@
|
|||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "deleteFinding",
|
||||
"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": "DELETE",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "http://localhost:8443/pentests/11601f51-bc17-47fd-847d-0c53df5405b5/finding/cb33fad4-7965-4654-a9f9-f007edaca35c",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8443",
|
||||
"path": [
|
||||
"pentests",
|
||||
"11601f51-bc17-47fd-847d-0c53df5405b5",
|
||||
"finding",
|
||||
"cb33fad4-7965-4654-a9f9-f007edaca35c"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "comments",
|
||||
"item": [
|
||||
{
|
||||
"name": "saveComment",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{
|
||||
"key": "token",
|
||||
"value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzE3MTM3MzQsImlhdCI6MTY3MTcxMzQzNCwiYXV0aF90aW1lIjoxNjcxNzEyNjkwLCJqdGkiOiJjNWYxYWZiZi1mZTczLTQ0NTAtYjA4YS1lMGEwMDcyNjMyOTgiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvYXV0aC9yZWFsbXMvYzRwb19yZWFsbV9sb2NhbCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIxMGUwNmQ3YS04ZGQwLTRlY2QtODk2My0wNTZiNDUwNzljNGYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjNHBvX2xvY2FsIiwibm9uY2UiOiIyZTYyNjNhNC1lM2U2LTRlMzUtYjQ5Yy1lMjYyNzM1ZTk2MGQiLCJzZXNzaW9uX3N0YXRlIjoiMGNmYmY4MGEtNzAxMS00NmQzLTllNGQtNTUxYWU4NTA5NjZmIiwiYWNyIjoiMCIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjNHBvX3VzZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYzRwb19sb2NhbCI6eyJyb2xlcyI6WyJ1c2VyIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoidGVzdCB1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidHR0IiwiZ2l2ZW5fbmFtZSI6InRlc3QiLCJmYW1pbHlfbmFtZSI6InVzZXIifQ.se43hq_vPjzAG6MpIxBiHb9vJHZmbLEko0tiN5m2hbhzd8s3YiBWpeiI6kgZ5kzl23iBQyMnXN4Sqpbt2ERKbKyUusezWcXhGTP22usi3b1vzFOAY9mqCI32i15sxCM2UDRYDFYcAblaKPxKsQf6EWduXpcn4L9_kQE4EpoLyWWWqFThGvFPSvkPGodffcEOz8BrnYDVUnwkodFsOWAnQmQHaR7jq1Y0hhZzWi3IlrRWlnRi0TKVWCZgUwO0PJttNq5wYZPsxgiS-khUCC1qtbKrRgBK_3sefxPkWDOQEubu0Kjyjq4rVZnq66anO3Qw82CSLn0nSCu-AL5Xd4Xchw",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "undefined",
|
||||
"type": "any"
|
||||
}
|
||||
]
|
||||
},
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"title\": \"Test Comment\",\n \"description\": \"Test Comment Description\",\n \"affectedUrls\": []\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8443/pentests/11601f51-bc17-47fd-847d-0c53df5405b5/comment",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8443",
|
||||
"path": [
|
||||
"pentests",
|
||||
"11601f51-bc17-47fd-847d-0c53df5405b5",
|
||||
"comment"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "getCommentsForPentesId",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{
|
||||
"key": "token",
|
||||
"value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzE3MTM3MzQsImlhdCI6MTY3MTcxMzQzNCwiYXV0aF90aW1lIjoxNjcxNzEyNjkwLCJqdGkiOiJjNWYxYWZiZi1mZTczLTQ0NTAtYjA4YS1lMGEwMDcyNjMyOTgiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvYXV0aC9yZWFsbXMvYzRwb19yZWFsbV9sb2NhbCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIxMGUwNmQ3YS04ZGQwLTRlY2QtODk2My0wNTZiNDUwNzljNGYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjNHBvX2xvY2FsIiwibm9uY2UiOiIyZTYyNjNhNC1lM2U2LTRlMzUtYjQ5Yy1lMjYyNzM1ZTk2MGQiLCJzZXNzaW9uX3N0YXRlIjoiMGNmYmY4MGEtNzAxMS00NmQzLTllNGQtNTUxYWU4NTA5NjZmIiwiYWNyIjoiMCIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjNHBvX3VzZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYzRwb19sb2NhbCI6eyJyb2xlcyI6WyJ1c2VyIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoidGVzdCB1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidHR0IiwiZ2l2ZW5fbmFtZSI6InRlc3QiLCJmYW1pbHlfbmFtZSI6InVzZXIifQ.se43hq_vPjzAG6MpIxBiHb9vJHZmbLEko0tiN5m2hbhzd8s3YiBWpeiI6kgZ5kzl23iBQyMnXN4Sqpbt2ERKbKyUusezWcXhGTP22usi3b1vzFOAY9mqCI32i15sxCM2UDRYDFYcAblaKPxKsQf6EWduXpcn4L9_kQE4EpoLyWWWqFThGvFPSvkPGodffcEOz8BrnYDVUnwkodFsOWAnQmQHaR7jq1Y0hhZzWi3IlrRWlnRi0TKVWCZgUwO0PJttNq5wYZPsxgiS-khUCC1qtbKrRgBK_3sefxPkWDOQEubu0Kjyjq4rVZnq66anO3Qw82CSLn0nSCu-AL5Xd4Xchw",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "undefined",
|
||||
"type": "any"
|
||||
}
|
||||
]
|
||||
},
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "http://localhost:8443/pentests/11601f51-bc17-47fd-847d-0c53df5405b5/comments",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8443",
|
||||
"path": [
|
||||
"pentests",
|
||||
"11601f51-bc17-47fd-847d-0c53df5405b5",
|
||||
"comments"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -235,11 +235,77 @@ include::{snippets}/updateFindingById/http-response.adoc[]
|
|||
|
||||
include::{snippets}/updateFindingById/response-fields.adoc[]
|
||||
|
||||
=== Delete finding
|
||||
|
||||
To delete a finding, call the PATCH request /pentests/+{pentestId}+/finding/+{findingId}+
|
||||
|
||||
==== Request example
|
||||
|
||||
include::{snippets}/deleteFindingByPentestAndFindingId/http-request.adoc[]
|
||||
|
||||
==== Request structure
|
||||
|
||||
include::{snippets}/deleteFindingByPentestAndFindingId/path-parameters.adoc[]
|
||||
|
||||
==== Response example
|
||||
|
||||
include::{snippets}/deleteFindingByPentestAndFindingId/http-response.adoc[]
|
||||
|
||||
==== Response structure
|
||||
|
||||
include::{snippets}/deleteFindingByPentestAndFindingId/response-fields.adoc[]
|
||||
|
||||
== Comment
|
||||
|
||||
=== Get comments for pentest
|
||||
|
||||
To get comments by pentestId, call the GET request /pentests/+{pentestId}+/comments.
|
||||
|
||||
==== Request example
|
||||
|
||||
include::{snippets}/getCommentsByPentestId/http-request.adoc[]
|
||||
|
||||
==== Request structure
|
||||
|
||||
include::{snippets}/getCommentsByPentestId/path-parameters.adoc[]
|
||||
|
||||
==== Response example
|
||||
|
||||
include::{snippets}/getCommentsByPentestId/http-response.adoc[]
|
||||
|
||||
==== Response structure
|
||||
|
||||
include::{snippets}/getCommentsByPentestId/response-fields.adoc[]
|
||||
|
||||
=== Save comment
|
||||
|
||||
To save a comment, call the POST request /pentests/+{pentestId}+/comment
|
||||
|
||||
==== Request example
|
||||
|
||||
include::{snippets}/saveCommentByPentestId/http-request.adoc[]
|
||||
|
||||
==== Request structure
|
||||
|
||||
include::{snippets}/saveCommentByPentestId/path-parameters.adoc[]
|
||||
|
||||
==== Response example
|
||||
|
||||
include::{snippets}/saveCommentByPentestId/http-response.adoc[]
|
||||
|
||||
==== Response structure
|
||||
|
||||
include::{snippets}/saveCommentByPentestId/response-fields.adoc[]
|
||||
|
||||
== Change History
|
||||
|
||||
|===
|
||||
|Date |Change
|
||||
|2022-12-22
|
||||
|Added GET, POST endpoint for Comment
|
||||
|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
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.securityc4po.api.comment
|
||||
|
||||
import com.securityc4po.api.ResponseBody
|
||||
import com.securityc4po.api.finding.FindingRequestBody
|
||||
import org.springframework.data.mongodb.core.index.Indexed
|
||||
import java.util.*
|
||||
|
||||
|
@ -10,3 +12,55 @@ data class Comment (
|
|||
val description: String,
|
||||
val relatedFindings: List<String>? = emptyList()
|
||||
)
|
||||
|
||||
fun buildComment(body: CommentRequestBody, commentEntity: CommentEntity): Comment {
|
||||
return Comment(
|
||||
id = commentEntity.data.id,
|
||||
title = body.title,
|
||||
description = body.description,
|
||||
relatedFindings = body.relatedFindings
|
||||
)
|
||||
}
|
||||
|
||||
data class CommentRequestBody(
|
||||
val title: String,
|
||||
val description: String,
|
||||
val relatedFindings: List<String>? = emptyList()
|
||||
)
|
||||
|
||||
fun Comment.toCommentResponseBody(): ResponseBody {
|
||||
return mapOf(
|
||||
"id" to id,
|
||||
"title" to title,
|
||||
"description" to description,
|
||||
"relatedFindings" to relatedFindings
|
||||
)
|
||||
}
|
||||
|
||||
fun Comment.toCommentDeleteResponseBody(): ResponseBody {
|
||||
return mapOf(
|
||||
"id" to id
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a [FindingRequestBody] is valid
|
||||
*
|
||||
* @return Boolean describing if the body is valid
|
||||
*/
|
||||
fun CommentRequestBody.isValid(): Boolean {
|
||||
return when {
|
||||
this.title.isBlank() -> false
|
||||
this.description.isBlank() -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
fun CommentRequestBody.toComment(): Comment {
|
||||
return Comment(
|
||||
id = UUID.randomUUID().toString(),
|
||||
title = this.title,
|
||||
description = this.description,
|
||||
relatedFindings = this.relatedFindings
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package com.securityc4po.api.comment
|
||||
|
||||
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.finding.toFindingResponseBody
|
||||
import com.securityc4po.api.pentest.PentestService
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.http.ResponseEntity.noContent
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.kotlin.core.publisher.switchIfEmpty
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/pentests")
|
||||
@CrossOrigin(
|
||||
origins = [],
|
||||
allowCredentials = "false",
|
||||
allowedHeaders = ["*"],
|
||||
methods = [RequestMethod.GET, RequestMethod.DELETE, RequestMethod.POST, RequestMethod.PATCH]
|
||||
)
|
||||
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION)
|
||||
class CommentController(private val pentestService: PentestService, private val commentService: CommentService) {
|
||||
|
||||
var logger = getLoggerFor<CommentController>()
|
||||
|
||||
@GetMapping("/{pentestId}/comments")
|
||||
fun getComments(@PathVariable(value = "pentestId") pentestId: String): Mono<ResponseEntity<List<ResponseBody>>> {
|
||||
return this.pentestService.getCommentIdsByPentestId(pentestId).flatMap { commentIds: List<String> ->
|
||||
this.commentService.getCommentsByIds(commentIds).map { commentList ->
|
||||
commentList.map { it.toCommentResponseBody() }
|
||||
}
|
||||
}.map {
|
||||
if (it.isEmpty()) noContent().build()
|
||||
else ResponseEntity.ok(it)
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{pentestId}/comment")
|
||||
fun saveComment(
|
||||
@PathVariable(value = "pentestId") pentestId: String,
|
||||
@RequestBody body: CommentRequestBody
|
||||
): Mono<ResponseEntity<ResponseBody>> {
|
||||
return this.commentService.saveComment(pentestId, body).map {
|
||||
ResponseEntity.accepted().body(it.toCommentResponseBody())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.securityc4po.api.comment
|
||||
|
||||
import org.springframework.data.mongodb.repository.DeleteQuery
|
||||
import org.springframework.data.mongodb.repository.Query
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@Repository
|
||||
interface CommentRepository : ReactiveMongoRepository<CommentEntity, String> {
|
||||
|
||||
@Query("{'data._id' : ?0}")
|
||||
fun findCommentById(id: String): Mono<CommentEntity>
|
||||
|
||||
@Query("{'data._id' :{\$in: ?0 }}")
|
||||
fun findCommentsByIds(id: List<String>): Flux<CommentEntity>
|
||||
|
||||
@DeleteQuery("{'data._id' : ?0}")
|
||||
fun deleteCommentById(id: String): Mono<Long>
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package com.securityc4po.api.comment
|
||||
|
||||
import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
|
||||
import com.securityc4po.api.configuration.MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION
|
||||
import com.securityc4po.api.configuration.error.handler.*
|
||||
import com.securityc4po.api.configuration.error.handler.EntityNotFoundException
|
||||
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.finding.*
|
||||
import com.securityc4po.api.pentest.PentestService
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.kotlin.core.publisher.switchIfEmpty
|
||||
|
||||
@Service
|
||||
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
|
||||
class CommentService(private val commentRepository: CommentRepository, private val pentestService: PentestService) {
|
||||
|
||||
var logger = getLoggerFor<CommentService>()
|
||||
|
||||
/**
|
||||
* Save [Finding]
|
||||
*
|
||||
* @throws [InvalidModelException] if the [Finding] is invalid
|
||||
* @throws [TransactionInterruptedException] if the [Finding] could not be stored
|
||||
* @return saved [Finding]
|
||||
*/
|
||||
fun saveComment(pentestId: String, body: CommentRequestBody): Mono<Comment> {
|
||||
validate(
|
||||
require = body.isValid(),
|
||||
logging = { logger.warn("Comment not valid.") },
|
||||
mappedException = InvalidModelException(
|
||||
"Comment not valid.", Errorcode.CommentInvalid
|
||||
)
|
||||
)
|
||||
val comment = body.toComment()
|
||||
val commentEntity = CommentEntity(comment)
|
||||
return commentRepository.insert(commentEntity).flatMap { newCommentEntity: CommentEntity ->
|
||||
val finding = newCommentEntity.toComment()
|
||||
// After successfully saving finding add id to pentest
|
||||
pentestService.updatePentestComment(pentestId, comment.id).onErrorMap {
|
||||
TransactionInterruptedException(
|
||||
"Pentest could not be updated in Database.",
|
||||
Errorcode.PentestInsertionFailed
|
||||
)
|
||||
}.map {
|
||||
finding
|
||||
}
|
||||
}.doOnError {
|
||||
throw wrappedException(
|
||||
logging = { logger.warn("Comment could not be stored in Database. Thrown exception: ", it) },
|
||||
mappedException = TransactionInterruptedException(
|
||||
"Comment could not be stored.",
|
||||
Errorcode.CommentInsertionFailed
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ enum class Errorcode(val code: Int) {
|
|||
PentestNotFound(1003),
|
||||
FindingsNotFound(1004),
|
||||
FindingNotFound(1005),
|
||||
CommentsNotFound(1006),
|
||||
CommentNotFound(1007),
|
||||
|
||||
// 2XXX Already Changed
|
||||
ProjectAlreadyChanged(2001),
|
||||
|
@ -20,6 +22,7 @@ enum class Errorcode(val code: Int) {
|
|||
TokenWithoutField(3004),
|
||||
UserIdIsEmpty(3005),
|
||||
FindingInvalid(3006),
|
||||
CommentInvalid(3007),
|
||||
|
||||
// 4XXX Unauthorized
|
||||
ProjectAdjustmentNotAuthorized(4000),
|
||||
|
@ -39,4 +42,7 @@ enum class Errorcode(val code: Int) {
|
|||
PentestInsertionFailed(6007),
|
||||
ProjectPentestInsertionFailed(6008),
|
||||
FindingInsertionFailed(6009),
|
||||
FindingDeletionFailed(6010),
|
||||
CommentInsertionFailed(6011),
|
||||
CommentDeletionFailed(6012),
|
||||
}
|
|
@ -52,6 +52,12 @@ fun Finding.toFindingResponseBody(): ResponseBody {
|
|||
)
|
||||
}
|
||||
|
||||
fun Finding.toFindingDeleteResponseBody(): ResponseBody {
|
||||
return mapOf(
|
||||
"id" to id
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a [FindingRequestBody] is valid
|
||||
*
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.securityc4po.api.finding
|
||||
|
||||
import org.springframework.data.mongodb.repository.DeleteQuery
|
||||
import org.springframework.data.mongodb.repository.Query
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
@ -14,4 +15,7 @@ interface FindingRepository : ReactiveMongoRepository<FindingEntity, String> {
|
|||
|
||||
@Query("{'data._id' :{\$in: ?0 }}")
|
||||
fun findFindingsByIds(id: List<String>): Flux<FindingEntity>
|
||||
|
||||
@DeleteQuery("{'data._id' : ?0}")
|
||||
fun deleteFindingById(id: String): Mono<Long>
|
||||
}
|
||||
|
|
|
@ -124,4 +124,32 @@ class FindingService(private val findingRepository: FindingRepository, private v
|
|||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFindingByPentestAndFindingId(pentestId: String, findingId: String): Mono<Finding> {
|
||||
return findingRepository.findFindingById(findingId).switchIfEmpty {
|
||||
logger.info("Finding with id $findingId not found. Deletion not necessary.")
|
||||
Mono.empty()
|
||||
}.flatMap { findingEntity: FindingEntity ->
|
||||
val finding = findingEntity.toFinding()
|
||||
findingRepository.deleteFindingById(findingId).flatMap {
|
||||
// After successfully deleting finding remove its id from pentest
|
||||
pentestService.removeFindingFromPentest(pentestId, findingId).onErrorMap {
|
||||
TransactionInterruptedException(
|
||||
"Pentest could not be updated in Database.",
|
||||
Errorcode.PentestInsertionFailed
|
||||
)
|
||||
}.map {
|
||||
finding
|
||||
}
|
||||
}.doOnError {
|
||||
throw wrappedException(
|
||||
logging = { logger.warn("Finding could not be deleted from Database. Thrown exception: ", it) },
|
||||
mappedException = TransactionInterruptedException(
|
||||
"Finding could not be deleted.",
|
||||
Errorcode.FindingDeletionFailed
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ data class Pentest(
|
|||
val refNumber: String,
|
||||
val status: PentestStatus,
|
||||
var findingIds: List<String> = emptyList(),
|
||||
val commentIds: List<String> = emptyList()
|
||||
var commentIds: List<String> = emptyList()
|
||||
)
|
||||
|
||||
fun buildPentest(body: PentestRequestBody, pentestEntity: PentestEntity): Pentest {
|
||||
|
|
|
@ -6,11 +6,13 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
|||
import com.securityc4po.api.ResponseBody
|
||||
import com.securityc4po.api.finding.FindingRequestBody
|
||||
import com.securityc4po.api.finding.FindingService
|
||||
import com.securityc4po.api.finding.toFindingDeleteResponseBody
|
||||
import com.securityc4po.api.finding.toFindingResponseBody
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.http.ResponseEntity.noContent
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.kotlin.core.publisher.switchIfEmpty
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/pentests")
|
||||
|
@ -77,7 +79,7 @@ class PentestController(private val pentestService: PentestService, private val
|
|||
findingList.map { it.toFindingResponseBody() }
|
||||
}
|
||||
}.map {
|
||||
if (it.isEmpty()) ResponseEntity.noContent().build()
|
||||
if (it.isEmpty()) noContent().build()
|
||||
else ResponseEntity.ok(it)
|
||||
}
|
||||
}
|
||||
|
@ -108,4 +110,16 @@ class PentestController(private val pentestService: PentestService, private val
|
|||
ResponseEntity.accepted().body(it.toFindingResponseBody())
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{pentestId}/finding/{findingId}")
|
||||
fun deleteFinding(
|
||||
@PathVariable(value = "pentestId") pentestId: String,
|
||||
@PathVariable(value = "findingId") findingId: String
|
||||
): Mono<ResponseEntity<ResponseBody>> {
|
||||
return this.findingService.deleteFindingByPentestAndFindingId(pentestId, findingId).map {
|
||||
ResponseEntity.ok().body(it.toFindingDeleteResponseBody())
|
||||
}.switchIfEmpty {
|
||||
Mono.just(noContent().build<ResponseBody>())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -154,6 +154,40 @@ class PentestService(private val pentestRepository: PentestRepository, private v
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update [Pentest] for Finding
|
||||
*
|
||||
* @throws [InvalidModelException] if the [Pentest] is invalid
|
||||
* @throws [TransactionInterruptedException] if the [Pentest] could not be updated
|
||||
* @return updated [Pentest]
|
||||
*/
|
||||
fun removeFindingFromPentest(pentestId: String, findingId: String): Mono<Pentest> {
|
||||
return pentestRepository.findPentestById(pentestId).switchIfEmpty {
|
||||
logger.warn("Pentest with id $pentestId not found. Updating not possible.")
|
||||
val msg = "Pentest with id $pentestId not found."
|
||||
val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
|
||||
throw ex
|
||||
}.flatMap { currentPentestEntity: PentestEntity ->
|
||||
if (currentPentestEntity.data.findingIds.find { pentestData -> pentestData == findingId } != null) {
|
||||
val findingIds = currentPentestEntity.data.findingIds.toMutableList()
|
||||
findingIds.remove(findingId)
|
||||
currentPentestEntity.data.findingIds = findingIds.toList()
|
||||
}
|
||||
currentPentestEntity.lastModified = Instant.now()
|
||||
this.pentestRepository.save(currentPentestEntity).map {
|
||||
it.toPentest()
|
||||
}.doOnError {
|
||||
throw wrappedException(
|
||||
logging = { logger.warn("Pentest could not be updated in Database. Thrown exception: ", it) },
|
||||
mappedException = TransactionInterruptedException(
|
||||
"Pentest could not be updated.",
|
||||
Errorcode.PentestInsertionFailed
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all [Finding]Id's by pentestId
|
||||
*
|
||||
|
@ -167,4 +201,50 @@ class PentestService(private val pentestRepository: PentestRepository, private v
|
|||
throw ex
|
||||
}.map { pentestEntity -> pentestEntity.data.findingIds }
|
||||
}
|
||||
|
||||
/**
|
||||
* Update [Pentest] for Comment
|
||||
*
|
||||
* @throws [InvalidModelException] if the [Pentest] is invalid
|
||||
* @throws [TransactionInterruptedException] if the [Pentest] could not be updated
|
||||
* @return updated [Pentest]
|
||||
*/
|
||||
fun updatePentestComment(pentestId: String, commentId: String): Mono<Pentest> {
|
||||
return pentestRepository.findPentestById(pentestId).switchIfEmpty {
|
||||
logger.warn("Pentest with id $pentestId not found. Updating not possible.")
|
||||
val msg = "Pentest with id $pentestId not found."
|
||||
val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
|
||||
throw ex
|
||||
}.flatMap { currentPentestEntity: PentestEntity ->
|
||||
if (currentPentestEntity.data.commentIds.find { pentestData -> pentestData == commentId } == null) {
|
||||
currentPentestEntity.data.commentIds += commentId
|
||||
}
|
||||
currentPentestEntity.lastModified = Instant.now()
|
||||
this.pentestRepository.save(currentPentestEntity).map {
|
||||
it.toPentest()
|
||||
}.doOnError {
|
||||
throw wrappedException(
|
||||
logging = { logger.warn("Pentest could not be updated in Database. Thrown exception: ", it) },
|
||||
mappedException = TransactionInterruptedException(
|
||||
"Pentest could not be updated.",
|
||||
Errorcode.PentestInsertionFailed
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all [Comment]Id's by pentestId
|
||||
*
|
||||
* @return list of [String]
|
||||
*/
|
||||
fun getCommentIdsByPentestId(pentestId: String): Mono<List<String>> {
|
||||
return this.pentestRepository.findPentestById(pentestId).switchIfEmpty {
|
||||
logger.warn("Pentest with id $pentestId not found. Collecting comments not possible.")
|
||||
val msg = "Pentest with id $pentestId not found."
|
||||
val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound)
|
||||
throw ex
|
||||
}.map { pentestEntity -> pentestEntity.data.commentIds }
|
||||
}
|
||||
}
|
|
@ -14,6 +14,4 @@ interface ProjectRepository: ReactiveMongoRepository<ProjectEntity, String> {
|
|||
|
||||
@DeleteQuery("{'data._id' : ?0}")
|
||||
fun deleteProjectById(id: String): Mono<Long>
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
package com.securityc4po.api.comment
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.github.tomakehurst.wiremock.common.Json
|
||||
import com.securityc4po.api.BaseDocumentationIntTest
|
||||
import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR
|
||||
import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
|
||||
import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC
|
||||
import com.securityc4po.api.pentest.Pentest
|
||||
import com.securityc4po.api.pentest.PentestCategory
|
||||
import com.securityc4po.api.pentest.PentestEntity
|
||||
import com.securityc4po.api.pentest.PentestStatus
|
||||
import com.securityc4po.api.project.Project
|
||||
import com.securityc4po.api.project.ProjectEntity
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.data.mongodb.core.MongoTemplate
|
||||
import org.springframework.data.mongodb.core.query.Query
|
||||
import org.springframework.restdocs.operation.preprocess.Preprocessors
|
||||
import org.springframework.restdocs.payload.JsonFieldType
|
||||
import org.springframework.restdocs.payload.PayloadDocumentation
|
||||
import org.springframework.restdocs.request.RequestDocumentation
|
||||
import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@SuppressFBWarnings(
|
||||
SIC_INNER_SHOULD_BE_STATIC,
|
||||
NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
|
||||
RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
|
||||
)
|
||||
class CommentControllerDocumentationTest : BaseDocumentationIntTest() {
|
||||
|
||||
@Autowired
|
||||
lateinit var mongoTemplate: MongoTemplate
|
||||
var mapper = ObjectMapper()
|
||||
|
||||
@BeforeEach
|
||||
fun init() {
|
||||
configureAdminToken()
|
||||
persistBasicTestScenario()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun destroy() {
|
||||
cleanUp()
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class GetComments {
|
||||
@Test
|
||||
fun getCommentsByPentestId() {
|
||||
val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002"
|
||||
webTestClient.get()
|
||||
.uri("/pentests/{pentestId}/comments", pentestTwoId)
|
||||
.header("Authorization", "Bearer $tokenAdmin")
|
||||
.exchange()
|
||||
.expectStatus().isOk
|
||||
.expectHeader().doesNotExist("")
|
||||
.expectBody().json(Json.write(getCommentsResponse()))
|
||||
.consumeWith(
|
||||
WebTestClientRestDocumentation.document(
|
||||
"{methodName}",
|
||||
Preprocessors.preprocessRequest(
|
||||
Preprocessors.prettyPrint(),
|
||||
Preprocessors.modifyUris().removePort(),
|
||||
Preprocessors.removeHeaders("Host", "Content-Length")
|
||||
),
|
||||
Preprocessors.preprocessResponse(
|
||||
Preprocessors.prettyPrint()
|
||||
),
|
||||
RequestDocumentation.relaxedPathParameters(
|
||||
RequestDocumentation.parameterWithName("pentestId").description("The id of the pentest you want to get the comments for")
|
||||
),
|
||||
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()
|
||||
)
|
||||
|
||||
private fun getCommentsResponse() = listOf(
|
||||
commentOne.toCommentResponseBody()
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class SaveComment {
|
||||
@Test
|
||||
fun saveCommentByPentestId() {
|
||||
val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002"
|
||||
webTestClient.post()
|
||||
.uri("/pentests/{pentestId}/comment", pentestTwoId)
|
||||
.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("pentestId").description("The id of the pentest you want to save the comment for")
|
||||
),
|
||||
PayloadDocumentation.relaxedResponseFields(
|
||||
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
|
||||
.description("The id of the saved comment"),
|
||||
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
|
||||
.description("The title of the comment"),
|
||||
PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
|
||||
.description("The description of the comment"),
|
||||
PayloadDocumentation.fieldWithPath("relatedFindings").type(JsonFieldType.ARRAY)
|
||||
.description("List of related findings of the comment")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val commentBody = CommentRequestBody(
|
||||
title = "Found another Bug",
|
||||
description = "Another OTG-INFO-002 Bug",
|
||||
relatedFindings = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun persistBasicTestScenario() {
|
||||
// setup test data
|
||||
// Project
|
||||
val projectOne = Project(
|
||||
id = "d2e126ba-f608-11ec-b939-0242ac120025",
|
||||
client = "E Corp",
|
||||
title = "Some Mock API (v1.0) Scanning",
|
||||
createdAt = "2021-01-10T18:05:00Z",
|
||||
tester = "Novatester",
|
||||
projectPentests = emptyList(),
|
||||
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
|
||||
)
|
||||
// Pentests
|
||||
val pentestOne = Pentest(
|
||||
id = "9c8af320-f608-11ec-b939-0242ac120002",
|
||||
projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
|
||||
category = PentestCategory.INFORMATION_GATHERING,
|
||||
refNumber = "OTG-INFO-001",
|
||||
status = PentestStatus.NOT_STARTED,
|
||||
findingIds = emptyList(),
|
||||
commentIds = emptyList()
|
||||
)
|
||||
val pentestTwo = Pentest(
|
||||
id = "43fbc63c-f624-11ec-b939-0242ac120002",
|
||||
projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
|
||||
category = PentestCategory.INFORMATION_GATHERING,
|
||||
refNumber = "OTG-INFO-002",
|
||||
status = PentestStatus.IN_PROGRESS,
|
||||
findingIds = emptyList(),
|
||||
commentIds = listOf("ab62d365-1b1d-4da1-89bc-5496616e220f")
|
||||
)
|
||||
val pentestThree = Pentest(
|
||||
id = "74eae112-f62c-11ec-b939-0242ac120002",
|
||||
projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
|
||||
category = PentestCategory.AUTHENTICATION_TESTING,
|
||||
refNumber = "OTG-AUTHN-001",
|
||||
status = PentestStatus.COMPLETED,
|
||||
findingIds = emptyList(),
|
||||
commentIds = emptyList()
|
||||
)
|
||||
// Comment
|
||||
val commentOne = Comment(
|
||||
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
|
||||
title = "Found Bug",
|
||||
description = "OTG-INFO-002 Bug",
|
||||
relatedFindings = emptyList()
|
||||
)
|
||||
// persist test data in database
|
||||
mongoTemplate.save(ProjectEntity(projectOne))
|
||||
mongoTemplate.save(PentestEntity(pentestOne))
|
||||
mongoTemplate.save(PentestEntity(pentestTwo))
|
||||
mongoTemplate.save(PentestEntity(pentestThree))
|
||||
mongoTemplate.save(CommentEntity(commentOne))
|
||||
}
|
||||
|
||||
private fun configureAdminToken() {
|
||||
tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local")
|
||||
}
|
||||
|
||||
private fun cleanUp() {
|
||||
mongoTemplate.findAllAndRemove(Query(), ProjectEntity::class.java)
|
||||
mongoTemplate.findAllAndRemove(Query(), PentestEntity::class.java)
|
||||
mongoTemplate.findAllAndRemove(Query(), CommentEntity::class.java)
|
||||
|
||||
tokenAdmin = "n/a"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
package com.securityc4po.api.comment
|
||||
|
||||
import com.github.tomakehurst.wiremock.common.Json
|
||||
import com.securityc4po.api.BaseIntTest
|
||||
import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR
|
||||
import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
|
||||
import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC
|
||||
import com.securityc4po.api.pentest.Pentest
|
||||
import com.securityc4po.api.pentest.PentestCategory
|
||||
import com.securityc4po.api.pentest.PentestEntity
|
||||
import com.securityc4po.api.pentest.PentestStatus
|
||||
import com.securityc4po.api.project.Project
|
||||
import com.securityc4po.api.project.ProjectEntity
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.web.server.LocalServerPort
|
||||
import org.springframework.data.mongodb.core.MongoTemplate
|
||||
import org.springframework.data.mongodb.core.query.Query
|
||||
import org.springframework.test.web.reactive.server.WebTestClient
|
||||
import reactor.core.publisher.Mono
|
||||
import java.time.Duration
|
||||
|
||||
@SuppressFBWarnings(
|
||||
SIC_INNER_SHOULD_BE_STATIC,
|
||||
NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
|
||||
RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
|
||||
)
|
||||
class CommentControllerIntegrationTest: BaseIntTest() {
|
||||
|
||||
@LocalServerPort
|
||||
private var port = 0
|
||||
|
||||
@Autowired
|
||||
lateinit var mongoTemplate: MongoTemplate
|
||||
|
||||
@Autowired
|
||||
private lateinit var webTestClient: WebTestClient
|
||||
|
||||
@BeforeEach
|
||||
fun setupWebClient() {
|
||||
webTestClient = WebTestClient.bindToServer()
|
||||
.baseUrl("http://localhost:$port")
|
||||
.responseTimeout(Duration.ofMillis(10000))
|
||||
.build()
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun init() {
|
||||
configureAdminToken()
|
||||
persistBasicTestScenario()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun destroy() {
|
||||
cleanUp()
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class GetComments {
|
||||
@Test
|
||||
fun `requesting comments by pentestId successfully`() {
|
||||
val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002"
|
||||
webTestClient.get()
|
||||
.uri("/pentests/{pentestId}/comments", pentestTwoId)
|
||||
.header("Authorization", "Bearer $tokenAdmin")
|
||||
.exchange()
|
||||
.expectStatus().isOk
|
||||
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
|
||||
.expectBody().json(Json.write(getComments()))
|
||||
}
|
||||
|
||||
private val commentOne = Comment(
|
||||
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
|
||||
title = "Found Bug",
|
||||
description = "OTG-INFO-002 Bug",
|
||||
relatedFindings = emptyList()
|
||||
)
|
||||
|
||||
private fun getComments() = listOf(
|
||||
commentOne.toCommentResponseBody()
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class SaveComment {
|
||||
@Test
|
||||
fun `save comment successfully`() {
|
||||
val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002"
|
||||
webTestClient.post()
|
||||
.uri("/pentests/{pentestId}/comment", pentestTwoId)
|
||||
.header("Authorization", "Bearer $tokenAdmin")
|
||||
.body(Mono.just(commentBody), CommentRequestBody::class.java)
|
||||
.exchange()
|
||||
.expectStatus().isAccepted
|
||||
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
|
||||
.expectBody()
|
||||
.jsonPath("$.title").isEqualTo("Found another Bug")
|
||||
.jsonPath("$.description").isEqualTo("Another OTG-INFO-002 Bug")
|
||||
.jsonPath("$.relatedFindings").isEmpty
|
||||
}
|
||||
|
||||
private val commentBody = CommentRequestBody(
|
||||
title = "Found another Bug",
|
||||
description = "Another OTG-INFO-002 Bug",
|
||||
relatedFindings = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun persistBasicTestScenario() {
|
||||
// setup test data
|
||||
// project
|
||||
val projectOne = Project(
|
||||
id = "d2e126ba-f608-11ec-b939-0242ac120025",
|
||||
client = "E Corp",
|
||||
title = "Some Mock API (v1.0) Scanning",
|
||||
createdAt = "2021-01-10T18:05:00Z",
|
||||
tester = "Novatester",
|
||||
createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032"
|
||||
)
|
||||
// pentests
|
||||
val pentestOne = Pentest(
|
||||
id = "9c8af320-f608-11ec-b939-0242ac120002",
|
||||
projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
|
||||
category = PentestCategory.INFORMATION_GATHERING,
|
||||
refNumber = "OTG-INFO-001",
|
||||
status = PentestStatus.NOT_STARTED,
|
||||
findingIds = emptyList(),
|
||||
commentIds = emptyList()
|
||||
)
|
||||
val pentestTwo = Pentest(
|
||||
id = "43fbc63c-f624-11ec-b939-0242ac120002",
|
||||
projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
|
||||
category = PentestCategory.INFORMATION_GATHERING,
|
||||
refNumber = "OTG-INFO-002",
|
||||
status = PentestStatus.IN_PROGRESS,
|
||||
findingIds = emptyList(),
|
||||
commentIds = listOf("ab62d365-1b1d-4da1-89bc-5496616e220f")
|
||||
)
|
||||
val pentestThree = Pentest(
|
||||
id = "16vbc63c-f624-11ec-b939-0242ac120002",
|
||||
projectId = "d2e126ba-f608-11ec-b939-0242ac120025",
|
||||
category = PentestCategory.AUTHENTICATION_TESTING,
|
||||
refNumber = "OTG-AUTHN-001",
|
||||
status = PentestStatus.COMPLETED,
|
||||
findingIds = emptyList(),
|
||||
commentIds = emptyList()
|
||||
)
|
||||
// Comment
|
||||
val commentOne = Comment(
|
||||
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
|
||||
title = "Found Bug",
|
||||
description = "OTG-INFO-002 Bug",
|
||||
relatedFindings = emptyList()
|
||||
)
|
||||
// persist test data in database
|
||||
mongoTemplate.save(ProjectEntity(projectOne))
|
||||
mongoTemplate.save(PentestEntity(pentestOne))
|
||||
mongoTemplate.save(PentestEntity(pentestTwo))
|
||||
mongoTemplate.save(PentestEntity(pentestThree))
|
||||
mongoTemplate.save(CommentEntity(commentOne))
|
||||
}
|
||||
|
||||
private fun configureAdminToken() {
|
||||
tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local")
|
||||
}
|
||||
|
||||
private fun cleanUp() {
|
||||
mongoTemplate.findAllAndRemove(Query(), ProjectEntity::class.java)
|
||||
mongoTemplate.findAllAndRemove(Query(), PentestEntity::class.java)
|
||||
mongoTemplate.findAllAndRemove(Query(), CommentEntity::class.java)
|
||||
|
||||
tokenAdmin = "n/a"
|
||||
}
|
||||
}
|
|
@ -261,7 +261,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
|
|||
),
|
||||
PayloadDocumentation.relaxedResponseFields(
|
||||
PayloadDocumentation.fieldWithPath("[].id").type(JsonFieldType.STRING)
|
||||
.description("The id of the requested pentest"),
|
||||
.description("The id of the requested findings"),
|
||||
PayloadDocumentation.fieldWithPath("[].severity").type(JsonFieldType.STRING)
|
||||
.description("The severity of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING)
|
||||
|
@ -325,7 +325,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
|
|||
),
|
||||
PayloadDocumentation.relaxedResponseFields(
|
||||
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
|
||||
.description("The id of the requested pentest"),
|
||||
.description("The id of the requested finding"),
|
||||
PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING)
|
||||
.description("The severity of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
|
||||
|
@ -386,11 +386,11 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
|
|||
),
|
||||
PayloadDocumentation.relaxedResponseFields(
|
||||
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
|
||||
.description("The id of the requested pentest"),
|
||||
.description("The id of the saved finding"),
|
||||
PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING)
|
||||
.description("The severity of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
|
||||
.description("The title of the requested finding"),
|
||||
.description("The title of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
|
||||
.description("The description number of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING)
|
||||
|
@ -446,7 +446,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
|
|||
),
|
||||
PayloadDocumentation.relaxedResponseFields(
|
||||
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
|
||||
.description("The id of the requested pentest"),
|
||||
.description("The id of the updated finding"),
|
||||
PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING)
|
||||
.description("The severity of the finding"),
|
||||
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
|
||||
|
@ -477,6 +477,43 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class DeleteFinding {
|
||||
@Test
|
||||
fun deleteFindingByPentestAndFindingId() {
|
||||
val pentestId = "43fbc63c-f624-11ec-b939-0242ac120002"
|
||||
val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
|
||||
webTestClient.delete()
|
||||
.uri("/pentests/{pentestId}/finding/{findingId}", pentestId, findingId)
|
||||
.header("Authorization", "Bearer $tokenAdmin")
|
||||
.exchange()
|
||||
.expectStatus().isOk
|
||||
.expectHeader().doesNotExist("")
|
||||
.expectBody()
|
||||
.consumeWith(
|
||||
WebTestClientRestDocumentation.document(
|
||||
"{methodName}",
|
||||
Preprocessors.preprocessRequest(
|
||||
Preprocessors.prettyPrint(),
|
||||
Preprocessors.modifyUris().removePort(),
|
||||
Preprocessors.removeHeaders("Host", "Content-Length")
|
||||
),
|
||||
Preprocessors.preprocessResponse(
|
||||
Preprocessors.prettyPrint()
|
||||
),
|
||||
RequestDocumentation.relaxedPathParameters(
|
||||
RequestDocumentation.parameterWithName("pentestId").description("The id of the pentest you want to remove the finidng from"),
|
||||
RequestDocumentation.parameterWithName("findingId").description("The id of the finding you want to delete")
|
||||
),
|
||||
PayloadDocumentation.relaxedResponseFields(
|
||||
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
|
||||
.description("The id of the finding you deleted")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun persistBasicTestScenario() {
|
||||
// setup test data
|
||||
// Project
|
||||
|
@ -517,7 +554,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
|
|||
findingIds = emptyList(),
|
||||
commentIds = emptyList()
|
||||
)
|
||||
// Findings
|
||||
// Finding
|
||||
val findingOne = Finding(
|
||||
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
|
||||
severity = Severity.LOW,
|
||||
|
|
|
@ -281,6 +281,23 @@ class PentestControllerIntTest : BaseIntTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class DeleteFinding {
|
||||
@Test
|
||||
fun `delete finding successfully`() {
|
||||
val pentestId = "43fbc63c-f624-11ec-b939-0242ac120002"
|
||||
val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f"
|
||||
webTestClient.delete()
|
||||
.uri("/pentests/{pentestId}/finding/{findingId}", pentestId, findingId)
|
||||
.header("Authorization", "Bearer $tokenAdmin")
|
||||
.exchange()
|
||||
.expectStatus().isOk
|
||||
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
|
||||
.expectBody()
|
||||
.jsonPath("$.id").isEqualTo("ab62d365-1b1d-4da1-89bc-5496616e220f")
|
||||
}
|
||||
}
|
||||
|
||||
private fun persistBasicTestScenario() {
|
||||
// setup test data
|
||||
// project
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
[{
|
||||
"_id": {
|
||||
"$oid": "63a4530b9bb2aa2c3bc77b52"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1671713547508"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "89703b19-16c7-49e5-8e33-0c706313e5fe",
|
||||
"title": "Test Comment",
|
||||
"description": "Test Comment Description",
|
||||
"relatedFindings": []
|
||||
},
|
||||
"_class": "com.securityc4po.api.comment.CommentEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "63a453e4377c3f53f86d27d8"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1671713764781"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "df516de6-ca5e-44a6-ac50-db89bb17aac3",
|
||||
"title": "New Test",
|
||||
"description": "New Test",
|
||||
"relatedFindings": [
|
||||
"0bda8950-94fa-4ec6-8fa7-e09f5a8cd3e8",
|
||||
"4ddb84f6-068c-4319-a8ee-1000008bb75a"
|
||||
]
|
||||
},
|
||||
"_class": "com.securityc4po.api.comment.CommentEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "63a454b5377c3f53f86d27d9"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1671713973541"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "e55e943b-6a48-4d84-8d72-b48d7d9de5b7",
|
||||
"title": "Wow another one?",
|
||||
"description": "Epic!",
|
||||
"relatedFindings": [
|
||||
"5e22d38f-a4f6-4809-84ea-a803b5f1f9fc"
|
||||
]
|
||||
},
|
||||
"_class": "com.securityc4po.api.comment.CommentEntity"
|
||||
}]
|
|
@ -4,7 +4,7 @@
|
|||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1668425376074"
|
||||
"$numberLong": "1671713973546"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
|
@ -14,9 +14,22 @@
|
|||
"refNumber": "OTG-INFO-001",
|
||||
"status": "IN_PROGRESS",
|
||||
"findingIds": [
|
||||
"ef31449d-71ec-4736-952f-8b20e53117d5"
|
||||
"ef31449d-71ec-4736-952f-8b20e53117d5",
|
||||
"0bda8950-94fa-4ec6-8fa7-e09f5a8cd3e8",
|
||||
"58f63b4e-97fb-4fe8-8527-7996896089d2",
|
||||
"72886128-b2d9-4a92-bbfe-b54373441321",
|
||||
"4ddb84f6-068c-4319-a8ee-1000008bb75a",
|
||||
"42831151-51fd-4348-b829-6b18ddd14fe1",
|
||||
"559cd0ac-9e64-41f9-892a-4c8a9dd30357",
|
||||
"5e22d38f-a4f6-4809-84ea-a803b5f1f9fc",
|
||||
"0bfa7511-fe33-4ab5-9af2-d4ed70c1b350",
|
||||
"70e413b9-d736-40d2-b7d6-236768b1230c"
|
||||
],
|
||||
"commentIds": []
|
||||
"commentIds": [
|
||||
"89703b19-16c7-49e5-8e33-0c706313e5fe",
|
||||
"df516de6-ca5e-44a6-ac50-db89bb17aac3",
|
||||
"e55e943b-6a48-4d84-8d72-b48d7d9de5b7"
|
||||
]
|
||||
},
|
||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||
},{
|
||||
|
@ -1387,4 +1400,202 @@
|
|||
"commentIds": []
|
||||
},
|
||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "6374dd53e0136563b96187c1"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1668603470508"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "617cc86d-aaa5-4144-93b6-1799e913ba21",
|
||||
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
|
||||
"category": "INFORMATION_GATHERING",
|
||||
"refNumber": "OTG-INFO-001",
|
||||
"status": "OPEN",
|
||||
"findingIds": [],
|
||||
"commentIds": []
|
||||
},
|
||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "6374de60e0136563b96187c2"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1668603488283"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "58c47263-47b8-4fd5-804d-092dad0a0cbb",
|
||||
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
|
||||
"category": "CONFIGURATION_AND_DEPLOY_MANAGEMENT_TESTING",
|
||||
"refNumber": "OTG-CONFIG-001",
|
||||
"status": "IN_PROGRESS",
|
||||
"findingIds": [],
|
||||
"commentIds": []
|
||||
},
|
||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "6374e698e0136563b96187c3"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1668605592100"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "34ccdaf4-0449-478c-b621-80c3e6417256",
|
||||
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
|
||||
"category": "AUTHORIZATION_TESTING",
|
||||
"refNumber": "OTG-AUTHZ-001",
|
||||
"status": "IN_PROGRESS",
|
||||
"findingIds": [],
|
||||
"commentIds": []
|
||||
},
|
||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "6374e77fe0136563b96187c4"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1668605823136"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "f1c3b1be-e509-48ac-aefd-5ef406f4adaf",
|
||||
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
|
||||
"category": "SESSION_MANAGEMENT_TESTING",
|
||||
"refNumber": "OTG-SESS-001",
|
||||
"status": "IN_PROGRESS",
|
||||
"findingIds": [],
|
||||
"commentIds": []
|
||||
},
|
||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "6374e7d8e0136563b96187c5"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1668605912794"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "a276d02c-28b4-4354-9fda-98280809a17c",
|
||||
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
|
||||
"category": "INPUT_VALIDATION_TESTING",
|
||||
"refNumber": "OTG-INPVAL-001",
|
||||
"status": "IN_PROGRESS",
|
||||
"findingIds": [],
|
||||
"commentIds": []
|
||||
},
|
||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "6374e931e0136563b96187c6"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1668606263295"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "fd26e674-9eea-478b-a82c-36c6cf8e37f9",
|
||||
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
|
||||
"category": "CRYPTOGRAPHY",
|
||||
"refNumber": "OTG-CRYPST-001",
|
||||
"status": "IN_PROGRESS",
|
||||
"findingIds": [],
|
||||
"commentIds": []
|
||||
},
|
||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "6374ec24e0136563b96187c7"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1668607029078"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "29bc612f-c093-40f8-8ebd-e03c9775194d",
|
||||
"projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746",
|
||||
"category": "BUSINESS_LOGIC_TESTING",
|
||||
"refNumber": "OTG-BUSLOGIC-001",
|
||||
"status": "IN_PROGRESS",
|
||||
"findingIds": [
|
||||
"672d9f87-fb3d-4fc5-8c6f-cadf97661ca5"
|
||||
],
|
||||
"commentIds": []
|
||||
},
|
||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "6376047e0687d905ca60af1c"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1668679299852"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "33c90b03-e475-407f-b58f-64833aa58480",
|
||||
"projectId": "ecece991-d3e0-4089-84ef-730bac197fbf",
|
||||
"category": "INPUT_VALIDATION_TESTING",
|
||||
"refNumber": "OTG-INPVAL-005",
|
||||
"status": "IN_PROGRESS",
|
||||
"findingIds": [
|
||||
"bddf810b-f20e-473e-a63d-34fcba7e48ef"
|
||||
],
|
||||
"commentIds": []
|
||||
},
|
||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "637611070687d905ca60af1f"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1668682319332"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "4ed11922-0b11-4134-a616-0da781d9ad77",
|
||||
"projectId": "e86984d1-6ed4-4cb7-aa17-d8d4d7c23f61",
|
||||
"category": "CLIENT_SIDE_TESTING",
|
||||
"refNumber": "OTG-CLIENT-001",
|
||||
"status": "IN_PROGRESS",
|
||||
"findingIds": [
|
||||
"d7c95af7-5434-4768-b62c-5b11f9396276"
|
||||
],
|
||||
"commentIds": []
|
||||
},
|
||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||
},{
|
||||
"_id": {
|
||||
"$oid": "63776766fcdda12bf2e51eb1"
|
||||
},
|
||||
"lastModified": {
|
||||
"$date": {
|
||||
"$numberLong": "1670579952464"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"_id": "52c9ce82-b4f7-48d9-b92d-4895d04ffdf8",
|
||||
"projectId": "9217fd68-3fad-478b-a900-b05ed549bef2",
|
||||
"category": "CLIENT_SIDE_TESTING",
|
||||
"refNumber": "OTG-CLIENT-001",
|
||||
"status": "IN_PROGRESS",
|
||||
"findingIds": [
|
||||
"cb33fad4-7965-4654-a9f9-f007edaca35c"
|
||||
],
|
||||
"commentIds": []
|
||||
},
|
||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||
}]
|
Loading…
Reference in New Issue