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 {NotificationServiceMock} from '@shared/services/notification.service.mock';
|
||||||
import {MockComponent} from 'ng-mocks';
|
import {MockComponent} from 'ng-mocks';
|
||||||
import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component';
|
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 = {
|
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
|
||||||
selectedProject: {
|
selectedProject: {
|
||||||
|
@ -74,7 +78,9 @@ describe('PentestCommentsComponent', () => {
|
||||||
NgxsModule.forRoot([ProjectState])
|
NgxsModule.forRoot([ProjectState])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: NotificationService, useValue: new NotificationServiceMock()}
|
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
||||||
|
{provide: DialogService, useClass: DialogServiceMock},
|
||||||
|
{provide: CommentDialogService, useClass: CommentDialogServiceMock},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
|
|
@ -3,15 +3,25 @@ import {BehaviorSubject, Observable} from 'rxjs';
|
||||||
import {Pentest} from '@shared/models/pentest.model';
|
import {Pentest} from '@shared/models/pentest.model';
|
||||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||||
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
|
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
|
||||||
import {PentestService} from '@shared/services/pentest.service';
|
import {NotificationService, PopupType} from '@shared/services/notification.service';
|
||||||
import {NotificationService} from '@shared/services/notification.service';
|
|
||||||
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
||||||
import {filter, tap} from 'rxjs/operators';
|
import {filter, mergeMap, tap} from 'rxjs/operators';
|
||||||
import {Comment, CommentEntry, transformCommentsToObjectiveEntries} from '@shared/models/comment.model';
|
import {
|
||||||
|
Comment,
|
||||||
|
CommentDialogBody,
|
||||||
|
CommentEntry,
|
||||||
|
transformCommentsToObjectiveEntries,
|
||||||
|
transformCommentToRequestBody
|
||||||
|
} from '@shared/models/comment.model';
|
||||||
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
|
import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined';
|
||||||
import {ProjectState} from '@shared/stores/project-state/project-state';
|
import {ProjectState} from '@shared/stores/project-state/project-state';
|
||||||
import {Store} from '@ngxs/store';
|
import {Store} from '@ngxs/store';
|
||||||
import {PentestStatus} from '@shared/models/pentest-status.model';
|
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()
|
@UntilDestroy()
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -42,10 +52,12 @@ export class PentestCommentsComponent implements OnInit {
|
||||||
expandedGetter: (node: CommentEntry) => !!node.expanded,
|
expandedGetter: (node: CommentEntry) => !!node.expanded,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private readonly pentestService: PentestService,
|
constructor(private readonly commentService: CommentService,
|
||||||
private dataSourceBuilder: NbTreeGridDataSourceBuilder<CommentEntry>,
|
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);
|
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +76,7 @@ export class PentestCommentsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCommentsData(): void {
|
loadCommentsData(): void {
|
||||||
this.pentestService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '')
|
this.commentService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '')
|
||||||
.pipe(
|
.pipe(
|
||||||
untilDestroyed(this),
|
untilDestroyed(this),
|
||||||
/*filter(isNotNullOrUndefined),*/
|
/*filter(isNotNullOrUndefined),*/
|
||||||
|
@ -90,15 +102,47 @@ export class PentestCommentsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickAddComment(): void {
|
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 {
|
onClickEditComment(commentEntry): void {
|
||||||
console.info('Coming soon..');
|
console.info('Coming soon..', commentEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickDeleteComment(comment): void {
|
onClickDeleteComment(commentEntry): void {
|
||||||
console.info('Coming soon..');
|
console.info('Coming soon..', commentEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML only
|
// HTML only
|
||||||
|
|
|
@ -20,6 +20,8 @@ import {Category} from '@shared/models/category.model';
|
||||||
import {PentestStatus} from '@shared/models/pentest-status.model';
|
import {PentestStatus} from '@shared/models/pentest-status.model';
|
||||||
import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service';
|
import {FindingDialogService} from '@shared/modules/finding-dialog/service/finding-dialog.service';
|
||||||
import {FindingDialogServiceMock} from '@shared/modules/finding-dialog/service/finding-dialog.service.mock';
|
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 = {
|
const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
|
||||||
selectedProject: {
|
selectedProject: {
|
||||||
|
@ -77,6 +79,7 @@ describe('PentestFindingsComponent', () => {
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
||||||
|
{provide: DialogService, useClass: DialogServiceMock},
|
||||||
{provide: FindingDialogService, useClass: FindingDialogServiceMock},
|
{provide: FindingDialogService, useClass: FindingDialogServiceMock},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {PentestService} from '@shared/services/pentest.service';
|
||||||
import {BehaviorSubject, Observable} from 'rxjs';
|
import {BehaviorSubject, Observable} from 'rxjs';
|
||||||
import {Pentest} from '@shared/models/pentest.model';
|
import {Pentest} from '@shared/models/pentest.model';
|
||||||
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
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 {NotificationService, PopupType} from '@shared/services/notification.service';
|
||||||
import {
|
import {
|
||||||
Finding,
|
Finding,
|
||||||
|
@ -21,6 +21,7 @@ import {PentestStatus} from '@shared/models/pentest-status.model';
|
||||||
import {Store} from '@ngxs/store';
|
import {Store} from '@ngxs/store';
|
||||||
import {UpdatePentestFindings} from '@shared/stores/project-state/project-state.actions';
|
import {UpdatePentestFindings} from '@shared/stores/project-state/project-state.actions';
|
||||||
import {ProjectState} from '@shared/stores/project-state/project-state';
|
import {ProjectState} from '@shared/stores/project-state/project-state';
|
||||||
|
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||||
|
|
||||||
@UntilDestroy()
|
@UntilDestroy()
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -33,6 +34,7 @@ export class PentestFindingsComponent implements OnInit {
|
||||||
constructor(private readonly pentestService: PentestService,
|
constructor(private readonly pentestService: PentestService,
|
||||||
private dataSourceBuilder: NbTreeGridDataSourceBuilder<FindingEntry>,
|
private dataSourceBuilder: NbTreeGridDataSourceBuilder<FindingEntry>,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
|
private dialogService: DialogService,
|
||||||
private findingDialogService: FindingDialogService,
|
private findingDialogService: FindingDialogService,
|
||||||
private store: Store) {
|
private store: Store) {
|
||||||
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
|
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
|
||||||
|
@ -180,7 +182,32 @@ export class PentestFindingsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickDeleteFinding(findingEntry): void {
|
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> {
|
isLoading(): Observable<boolean> {
|
||||||
|
|
|
@ -3,18 +3,19 @@ import {CommonModule} from '@angular/common';
|
||||||
import {RouterModule} from '@angular/router';
|
import {RouterModule} from '@angular/router';
|
||||||
import {PentestComponent} from './pentest.component';
|
import {PentestComponent} from './pentest.component';
|
||||||
import {NbButtonModule, NbCardModule, NbLayoutModule, NbSelectModule, NbTabsetModule, NbTreeGridModule} from '@nebular/theme';
|
import {NbButtonModule, NbCardModule, NbLayoutModule, NbSelectModule, NbTabsetModule, NbTreeGridModule} from '@nebular/theme';
|
||||||
import { PentestHeaderComponent } from './pentest-header/pentest-header.component';
|
import {PentestHeaderComponent} from './pentest-header/pentest-header.component';
|
||||||
import { PentestContentComponent } from './pentest-content/pentest-content.component';
|
import {PentestContentComponent} from './pentest-content/pentest-content.component';
|
||||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||||
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {StatusTagModule} from '@shared/widgets/status-tag/status-tag.module';
|
import {StatusTagModule} from '@shared/widgets/status-tag/status-tag.module';
|
||||||
import { PentestInfoComponent } from './pentest-content/pentest-info/pentest-info.component';
|
import {PentestInfoComponent} from './pentest-content/pentest-info/pentest-info.component';
|
||||||
import { PentestFindingsComponent } from './pentest-content/pentest-findings/pentest-findings.component';
|
import {PentestFindingsComponent} from './pentest-content/pentest-findings/pentest-findings.component';
|
||||||
import { PentestCommentsComponent } from './pentest-content/pentest-comments/pentest-comments.component';
|
import {PentestCommentsComponent} from './pentest-content/pentest-comments/pentest-comments.component';
|
||||||
import {CommonAppModule} from '../common-app.module';
|
import {CommonAppModule} from '../common-app.module';
|
||||||
import {SeverityTagModule} from '@shared/widgets/severity-tag/severity-tag.module';
|
import {SeverityTagModule} from '@shared/widgets/severity-tag/severity-tag.module';
|
||||||
import {FindingDialogModule} from '@shared/modules/finding-dialog/finding-dialog.module';
|
import {FindingDialogModule} from '@shared/modules/finding-dialog/finding-dialog.module';
|
||||||
|
import {CommentDialogModule} from '@shared/modules/comment-dialog/comment-dialog.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -25,26 +26,28 @@ import {FindingDialogModule} from '@shared/modules/finding-dialog/finding-dialog
|
||||||
PentestFindingsComponent,
|
PentestFindingsComponent,
|
||||||
PentestCommentsComponent
|
PentestCommentsComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
CommonAppModule,
|
CommonAppModule,
|
||||||
RouterModule.forChild([{
|
RouterModule.forChild([{
|
||||||
path: '',
|
path: '',
|
||||||
component: PentestComponent
|
component: PentestComponent
|
||||||
}]),
|
}]),
|
||||||
NbLayoutModule,
|
NbLayoutModule,
|
||||||
NbCardModule,
|
NbCardModule,
|
||||||
FlexLayoutModule,
|
FlexLayoutModule,
|
||||||
FontAwesomeModule,
|
FontAwesomeModule,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
NbButtonModule,
|
NbButtonModule,
|
||||||
StatusTagModule,
|
StatusTagModule,
|
||||||
NbTabsetModule,
|
NbTabsetModule,
|
||||||
NbTreeGridModule,
|
NbTreeGridModule,
|
||||||
SeverityTagModule,
|
SeverityTagModule,
|
||||||
FindingDialogModule,
|
NbSelectModule,
|
||||||
NbSelectModule
|
// Dialog Modules
|
||||||
]
|
FindingDialogModule,
|
||||||
|
CommentDialogModule,
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class PentestModule {
|
export class PentestModule {
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"action.exit": "Beenden",
|
"action.exit": "Beenden",
|
||||||
"action.update": "Speichern",
|
"action.update": "Speichern",
|
||||||
"action.export": "Exportieren",
|
"action.export": "Exportieren",
|
||||||
|
"action.reset": "Zurücksetzen",
|
||||||
"action.yes": "Ja",
|
"action.yes": "Ja",
|
||||||
"action.no": "Nein",
|
"action.no": "Nein",
|
||||||
"username": "Nutzername",
|
"username": "Nutzername",
|
||||||
|
@ -148,10 +149,31 @@
|
||||||
"commentId": "Kommentar Id",
|
"commentId": "Kommentar Id",
|
||||||
"title": "Titel",
|
"title": "Titel",
|
||||||
"description": "Beschreibung",
|
"description": "Beschreibung",
|
||||||
"relatedFindings": "Verwandte Funde",
|
"relatedFindings": "Verbundene Funde",
|
||||||
"add": "Kommentar hinzufügen",
|
"add": "Kommentar hinzufügen",
|
||||||
"no.relatedFindings": "Nicht verbunden mit Fund",
|
"add.finding": "Fund hinzufügen",
|
||||||
"no.comments": "Keine Kommentare verfügbar",
|
"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": {
|
"popup": {
|
||||||
"not.found": "Keine Kommentare gefunden",
|
"not.found": "Keine Kommentare gefunden",
|
||||||
"save.success": "Kommentar erfolgreich gespeichert",
|
"save.success": "Kommentar erfolgreich gespeichert",
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"action.exit": "Exit",
|
"action.exit": "Exit",
|
||||||
"action.update": "Update",
|
"action.update": "Update",
|
||||||
"action.export": "Export",
|
"action.export": "Export",
|
||||||
|
"action.reset": "Reset",
|
||||||
"action.yes": "Yes",
|
"action.yes": "Yes",
|
||||||
"action.no": "No",
|
"action.no": "No",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
|
@ -150,8 +151,29 @@
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"relatedFindings": "Related Findings",
|
"relatedFindings": "Related Findings",
|
||||||
"add": "Add Comment",
|
"add": "Add Comment",
|
||||||
|
"add.finding": "Add related finding",
|
||||||
"no.comments": "No comments available",
|
"no.comments": "No comments available",
|
||||||
"no.relatedFindings": "Not related to finding",
|
"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": {
|
"popup": {
|
||||||
"not.found": "No comment found",
|
"not.found": "No comment found",
|
||||||
"save.success": "Comment saved successfully",
|
"save.success": "Comment saved successfully",
|
||||||
|
|
|
@ -42,3 +42,30 @@ export function transformCommentsToObjectiveEntries(findings: Comment[]): Commen
|
||||||
});
|
});
|
||||||
return findingEntries;
|
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,168 +4,167 @@
|
||||||
</nb-card-header>
|
</nb-card-header>
|
||||||
<nb-card-body>
|
<nb-card-body>
|
||||||
<form *ngIf="formArray" [formGroup]="findingFormGroup">
|
<form *ngIf="formArray" [formGroup]="findingFormGroup">
|
||||||
<div>
|
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||||
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
<!-- Form Text Layout -->
|
||||||
<!-- Form Text Layout -->
|
<div fxLayout="column" fxFlex="50" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||||
<div fxLayout="column" fxFlex="50" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
<!-- Title Form Field -->
|
||||||
<!-- Title Form Field -->
|
<nb-form-field class="finding-form-field">
|
||||||
<nb-form-field class="finding-form-field">
|
<label for="{{formArray[0].fieldName}}" class="label">
|
||||||
<label for="{{formArray[0].fieldName}}" class="label">
|
{{formArray[0].labelKey | translate}}
|
||||||
{{formArray[0].labelKey | translate}}
|
</label>
|
||||||
</label>
|
<input formControlName="{{formArray[0].fieldName}}"
|
||||||
<input formControlName="{{formArray[0].fieldName}}"
|
type="formText" required fullWidth
|
||||||
type="formText" required fullWidth
|
id="{{formArray[0].fieldName}}" nbInput
|
||||||
id="{{formArray[0].fieldName}}" nbInput
|
class="form-field form-text"
|
||||||
class="form-field form-text"
|
[status]="findingFormGroup.get(formArray[0].fieldName).dirty ? (findingFormGroup.get(formArray[0].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
[status]="findingFormGroup.get(formArray[0].fieldName).dirty ? (findingFormGroup.get(formArray[0].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
placeholder="{{formArray[0].placeholder | translate}} *">
|
||||||
placeholder="{{formArray[0].placeholder | translate}} *">
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
<ng-template ngFor let-error [ngForOf]="formArray[0].errors"
|
||||||
<ng-template ngFor let-error [ngForOf]="formArray[0].errors"
|
*ngIf="findingFormGroup.get(formArray[0].fieldName).dirty">
|
||||||
*ngIf="findingFormGroup.get(formArray[0].fieldName).dirty">
|
|
||||||
<span class="error-text"
|
<span class="error-text"
|
||||||
*ngIf="findingFormGroup.get(formArray[0].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
*ngIf="findingFormGroup.get(formArray[0].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||||
{{error.translationKey | translate}}
|
{{error.translationKey | translate}}
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</nb-form-field>
|
</nb-form-field>
|
||||||
<!-- Description Form Field -->
|
<!-- Description Form Field -->
|
||||||
<nb-form-field class="finding-form-field">
|
<nb-form-field class="finding-form-field">
|
||||||
<label for="{{formArray[2].fieldName}}" class="label">
|
<label for="{{formArray[2].fieldName}}" class="label">
|
||||||
{{formArray[2].labelKey | translate}}
|
{{formArray[2].labelKey | translate}}
|
||||||
</label>
|
</label>
|
||||||
<textarea formControlName="{{formArray[2].fieldName}}"
|
<textarea formControlName="{{formArray[2].fieldName}}"
|
||||||
type="formText" required fullWidth
|
type="formText" required fullWidth
|
||||||
id="{{formArray[2].fieldName}}" nbInput
|
id="{{formArray[2].fieldName}}" nbInput
|
||||||
class="form-field form-text"
|
class="form-field form-text"
|
||||||
[status]="findingFormGroup.get(formArray[2].fieldName).dirty ? (findingFormGroup.get(formArray[2].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
[status]="findingFormGroup.get(formArray[2].fieldName).dirty ? (findingFormGroup.get(formArray[2].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
placeholder="{{formArray[2].placeholder | translate}} *">
|
placeholder="{{formArray[2].placeholder | translate}} *">
|
||||||
</textarea>
|
</textarea>
|
||||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
<ng-template ngFor let-error [ngForOf]="formArray[2].errors"
|
<ng-template ngFor let-error [ngForOf]="formArray[2].errors"
|
||||||
*ngIf="findingFormGroup.get(formArray[2].fieldName).dirty">
|
*ngIf="findingFormGroup.get(formArray[2].fieldName).dirty">
|
||||||
<span class="error-text"
|
<span class="error-text"
|
||||||
*ngIf="findingFormGroup.get(formArray[2].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
*ngIf="findingFormGroup.get(formArray[2].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||||
{{error.translationKey | translate}}
|
{{error.translationKey | translate}}
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</nb-form-field>
|
</nb-form-field>
|
||||||
<!-- Impact Form Field -->
|
<!-- Impact Form Field -->
|
||||||
<nb-form-field class="finding-form-field">
|
<nb-form-field class="finding-form-field">
|
||||||
<label for="{{formArray[3].fieldName}}" class="label">
|
<label for="{{formArray[3].fieldName}}" class="label">
|
||||||
{{formArray[3].labelKey | translate}}
|
{{formArray[3].labelKey | translate}}
|
||||||
</label>
|
</label>
|
||||||
<textarea formControlName="{{formArray[3].fieldName}}"
|
<textarea formControlName="{{formArray[3].fieldName}}"
|
||||||
type="formText" required fullWidth
|
type="formText" required fullWidth
|
||||||
id="{{formArray[3].fieldName}}" nbInput
|
id="{{formArray[3].fieldName}}" nbInput
|
||||||
class="form-field form-text"
|
class="form-field form-text"
|
||||||
[status]="findingFormGroup.get(formArray[3].fieldName).dirty ? (findingFormGroup.get(formArray[3].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
[status]="findingFormGroup.get(formArray[3].fieldName).dirty ? (findingFormGroup.get(formArray[3].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
placeholder="{{formArray[3].placeholder | translate}} *">
|
placeholder="{{formArray[3].placeholder | translate}} *">
|
||||||
</textarea>
|
</textarea>
|
||||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
<ng-template ngFor let-error [ngForOf]="formArray[3].errors"
|
<ng-template ngFor let-error [ngForOf]="formArray[3].errors"
|
||||||
*ngIf="findingFormGroup.get(formArray[3].fieldName).dirty">
|
*ngIf="findingFormGroup.get(formArray[3].fieldName).dirty">
|
||||||
<span class="error-text"
|
<span class="error-text"
|
||||||
*ngIf="findingFormGroup.get(formArray[3].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
*ngIf="findingFormGroup.get(formArray[3].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||||
{{error.translationKey | translate}}
|
{{error.translationKey | translate}}
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</nb-form-field>
|
</nb-form-field>
|
||||||
</div>
|
|
||||||
<!-- Severity Layout -->
|
|
||||||
<!-- Severity Form Field -->
|
|
||||||
<div fxFlex class="severity-dialog">
|
|
||||||
<label for="{{formArray[1].fieldName}}" class="label">
|
|
||||||
{{formArray[1].labelKey | translate}}
|
|
||||||
</label>
|
|
||||||
<nb-select class="severities" placeholder="{{formArray[1].placeholder | translate}} *"
|
|
||||||
type="severity-select"
|
|
||||||
[(selected)]="formArray[1].controlsConfig[0].value"
|
|
||||||
shape="round" status="{{getSeverityFillStatus(formArray[1].controlsConfig[0].value)}}" filled>
|
|
||||||
<nb-option *ngFor="let severity of severityTexts" [value]="severity.value">
|
|
||||||
{{ severity.translationText | translate }}
|
|
||||||
</nb-option>
|
|
||||||
</nb-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Affected URLs Layout -->
|
<!-- Severity Layout -->
|
||||||
<!-- Affected URLs Form Field -->
|
<!-- Severity Form Field -->
|
||||||
<nb-form-field class="finding-form-field">
|
<div fxFlex class="severity-dialog">
|
||||||
<label for="{{formArray[4].fieldName}}" class="label">
|
<label for="{{formArray[1].fieldName}}" class="label">
|
||||||
{{formArray[4].labelKey | translate}}
|
{{formArray[1].labelKey | translate}}
|
||||||
</label>
|
</label>
|
||||||
<input formControlName="{{formArray[4].fieldName}}"
|
<nb-select class="severities" placeholder="{{formArray[1].placeholder | translate}} *"
|
||||||
type="text"
|
type="severity-select"
|
||||||
id="{{formArray[4].fieldName}}"
|
[(selected)]="formArray[1].controlsConfig[0].value"
|
||||||
nbTagInput fullWidth
|
shape="round" status="{{getSeverityFillStatus(formArray[1].controlsConfig[0].value)}}" filled>
|
||||||
shape="rectangle"
|
<nb-option *ngFor="let severity of severityTexts" [value]="severity.value">
|
||||||
(tagAdd)="onAffectedUrlAdd()"
|
{{ severity.translationText | translate }}
|
||||||
class="form-field additionalUrl"
|
</nb-option>
|
||||||
[status]="findingFormGroup.get(formArray[4].fieldName).dirty ? (findingFormGroup.get(formArray[4].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
</nb-select>
|
||||||
placeholder="{{formArray[4].placeholder | translate}}">
|
</div>
|
||||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
</div>
|
||||||
<ng-template ngFor let-error [ngForOf]="formArray[4].errors"
|
<!-- Affected URLs Layout -->
|
||||||
*ngIf="findingFormGroup.get(formArray[4].fieldName).dirty">
|
<!-- Affected URLs Form Field -->
|
||||||
|
<nb-form-field class="finding-form-field">
|
||||||
|
<label for="{{formArray[4].fieldName}}" class="label">
|
||||||
|
{{formArray[4].labelKey | translate}}
|
||||||
|
</label>
|
||||||
|
<input formControlName="{{formArray[4].fieldName}}"
|
||||||
|
type="text"
|
||||||
|
id="{{formArray[4].fieldName}}"
|
||||||
|
nbTagInput fullWidth
|
||||||
|
shape="rectangle"
|
||||||
|
(tagAdd)="onAffectedUrlAdd()"
|
||||||
|
class="form-field additionalUrl"
|
||||||
|
[status]="findingFormGroup.get(formArray[4].fieldName).dirty ? (findingFormGroup.get(formArray[4].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{formArray[4].placeholder | translate}}">
|
||||||
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
|
<ng-template ngFor let-error [ngForOf]="formArray[4].errors"
|
||||||
|
*ngIf="findingFormGroup.get(formArray[4].fieldName).dirty">
|
||||||
<span class="error-text"
|
<span class="error-text"
|
||||||
*ngIf="findingFormGroup.get(formArray[4].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
*ngIf="findingFormGroup.get(formArray[4].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||||
{{error.translationKey | translate}}
|
{{error.translationKey | translate}}
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</nb-form-field>
|
</nb-form-field>
|
||||||
<!-- Add Affected URLs Button -->
|
<!-- Add Affected URLs Button -->
|
||||||
<button nbButton status="primary" size="small" class="add-url-button"
|
<button nbButton status="primary" size="small" class="add-url-button"
|
||||||
(click)="onAffectedUrlAdd()">
|
(click)="onAffectedUrlAdd()">
|
||||||
<fa-icon [icon]="fa.faPlus" class="new-url-icon"></fa-icon>
|
<fa-icon [icon]="fa.faPlus" class="new-url-icon"></fa-icon>
|
||||||
<span> {{ 'finding.add.url' | translate }} </span>
|
<span> {{ 'finding.add.url' | translate }} </span>
|
||||||
</button>
|
</button>
|
||||||
<!---->
|
<!---->
|
||||||
<nb-tag-list (tagRemove)="onAffectedUrlTagRemove($event)" class="url-tag-list">
|
<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"
|
||||||
</nb-tag-list>
|
[text]="url"></nb-tag>
|
||||||
<!-- Additional Text Layout -->
|
</nb-tag-list>
|
||||||
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
<!-- Additional Text Layout -->
|
||||||
<!-- Reproduction Form Field -->
|
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start start">
|
||||||
<nb-form-field fxFlex="50" class="finding-form-field">
|
<!-- Reproduction Form Field -->
|
||||||
<label for="{{formArray[5].fieldName}}" class="label">
|
<nb-form-field fxFlex="50" class="finding-form-field">
|
||||||
{{formArray[5].labelKey | translate}}
|
<label for="{{formArray[5].fieldName}}" class="label">
|
||||||
</label>
|
{{formArray[5].labelKey | translate}}
|
||||||
<textarea formControlName="{{formArray[5].fieldName}}"
|
</label>
|
||||||
type="text" required fullWidth
|
<textarea formControlName="{{formArray[5].fieldName}}"
|
||||||
id="{{formArray[5].fieldName}}" nbInput
|
type="text" required fullWidth
|
||||||
class="form-field form-textarea"
|
id="{{formArray[5].fieldName}}" nbInput
|
||||||
[status]="findingFormGroup.get(formArray[5].fieldName).dirty ? (findingFormGroup.get(formArray[5].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
class="form-field form-textarea"
|
||||||
placeholder="{{formArray[5].placeholder | translate}} *">
|
[status]="findingFormGroup.get(formArray[5].fieldName).dirty ? (findingFormGroup.get(formArray[5].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
|
placeholder="{{formArray[5].placeholder | translate}} *">
|
||||||
</textarea>
|
</textarea>
|
||||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
<ng-template ngFor let-error [ngForOf]="formArray[5].errors"
|
<ng-template ngFor let-error [ngForOf]="formArray[5].errors"
|
||||||
*ngIf="findingFormGroup.get(formArray[5].fieldName).dirty">
|
*ngIf="findingFormGroup.get(formArray[5].fieldName).dirty">
|
||||||
<span class="error-text"
|
<span class="error-text"
|
||||||
*ngIf="findingFormGroup.get(formArray[5].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
*ngIf="findingFormGroup.get(formArray[5].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||||
{{error.translationKey | translate}}
|
{{error.translationKey | translate}}
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</nb-form-field>
|
</nb-form-field>
|
||||||
<!-- Mitigation Form Field -->
|
<!-- Mitigation Form Field -->
|
||||||
<nb-form-field fxFlex class="finding-form-field">
|
<nb-form-field fxFlex class="finding-form-field">
|
||||||
<label for="{{formArray[6].fieldName}}" class="label">
|
<label for="{{formArray[6].fieldName}}" class="label">
|
||||||
{{formArray[6].labelKey | translate}}
|
{{formArray[6].labelKey | translate}}
|
||||||
</label>
|
</label>
|
||||||
<textarea formControlName="{{formArray[6].fieldName}}"
|
<textarea formControlName="{{formArray[6].fieldName}}"
|
||||||
type="text" fullWidth
|
type="text" fullWidth
|
||||||
id="{{formArray[6].fieldName}}" nbInput
|
id="{{formArray[6].fieldName}}" nbInput
|
||||||
class="form-field form-textarea"
|
class="form-field form-textarea"
|
||||||
[status]="findingFormGroup.get(formArray[6].fieldName).dirty ? (findingFormGroup.get(formArray[6].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
[status]="findingFormGroup.get(formArray[6].fieldName).dirty ? (findingFormGroup.get(formArray[6].fieldName).invalid ? 'danger' : 'basic') : 'basic'"
|
||||||
placeholder="{{formArray[6].placeholder | translate}}">
|
placeholder="{{formArray[6].placeholder | translate}}">
|
||||||
</textarea>
|
</textarea>
|
||||||
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
<!-- FIXME: when the bug (https://github.com/angular/components/issues/7739) is fixed -->
|
||||||
<ng-template ngFor let-error [ngForOf]="formArray[6].errors"
|
<ng-template ngFor let-error [ngForOf]="formArray[6].errors"
|
||||||
*ngIf="findingFormGroup.get(formArray[6].fieldName).dirty">
|
*ngIf="findingFormGroup.get(formArray[6].fieldName).dirty">
|
||||||
<span class="error-text"
|
<span class="error-text"
|
||||||
*ngIf="findingFormGroup.get(formArray[6].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
*ngIf="findingFormGroup.get(formArray[6].fieldName)?.hasError(error.errorCode) && error.errorCode === 'required'">
|
||||||
{{error.translationKey | translate}}
|
{{error.translationKey | translate}}
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</nb-form-field>
|
</nb-form-field>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</nb-card-body>
|
</nb-card-body>
|
||||||
|
|
|
@ -96,13 +96,13 @@ describe('FindingDialogComponent', () => {
|
||||||
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
{provide: NotificationService, useValue: new NotificationServiceMock()},
|
||||||
{provide: DialogService, useClass: DialogServiceMock},
|
{provide: DialogService, useClass: DialogServiceMock},
|
||||||
{provide: NbDialogRef, useValue: dialogSpy},
|
{provide: NbDialogRef, useValue: dialogSpy},
|
||||||
{provide: NB_DIALOG_CONFIG, useValue: mockedFindingDialogData}
|
{provide: NB_DIALOG_CONFIG, useValue: mockedCommentDialogData}
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedFindingDialogData});
|
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedCommentDialogData});
|
||||||
fixture = TestBed.createComponent(FindingDialogComponent);
|
fixture = TestBed.createComponent(FindingDialogComponent);
|
||||||
store = TestBed.inject(Store);
|
store = TestBed.inject(Store);
|
||||||
store.reset({
|
store.reset({
|
||||||
|
@ -137,7 +137,7 @@ export const mockFinding: Finding = {
|
||||||
mitigation: 'Mitigation Test'
|
mitigation: 'Mitigation Test'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mockedFindingDialogData = {
|
export const mockedCommentDialogData = {
|
||||||
form: {
|
form: {
|
||||||
findingTitle: {
|
findingTitle: {
|
||||||
fieldName: 'findingTitle',
|
fieldName: 'findingTitle',
|
||||||
|
|
|
@ -6,7 +6,6 @@ import deepEqual from 'deep-equal';
|
||||||
import {UntilDestroy} from '@ngneat/until-destroy';
|
import {UntilDestroy} from '@ngneat/until-destroy';
|
||||||
import {Severity} from '@shared/models/severity.enum';
|
import {Severity} from '@shared/models/severity.enum';
|
||||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||||
import {BehaviorSubject} from 'rxjs';
|
|
||||||
|
|
||||||
@UntilDestroy()
|
@UntilDestroy()
|
||||||
@Component({
|
@Component({
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import {NgModule} from '@angular/core';
|
import {NgModule} from '@angular/core';
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component';
|
import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component';
|
||||||
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
|
||||||
import {
|
import {
|
||||||
NbButtonModule,
|
NbButtonModule,
|
||||||
NbCardModule,
|
NbCardModule,
|
||||||
NbDialogModule,
|
|
||||||
NbDialogService,
|
|
||||||
NbFormFieldModule,
|
NbFormFieldModule,
|
||||||
NbInputModule,
|
NbInputModule,
|
||||||
NbSelectModule,
|
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
|
* Delete Finding
|
||||||
* @param pentestId the id of the project
|
* @param pentestId the id of the pentest
|
||||||
|
* @param findingId the id of the finding
|
||||||
*/
|
*/
|
||||||
public getCommentsByPentestId(pentestId: string): Observable<Comment[]> {
|
public deleteFindingByPentestAndFindingId(pentestId: string, findingId: string): Observable<string> {
|
||||||
return this.http.get<Comment[]>(`${this.apiBaseURL}/${pentestId}/comments`);
|
return this.http.delete<string>(`${this.apiBaseURL}/${pentestId}/finding/${findingId}`);
|
||||||
// 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: [],
|
|
||||||
}
|
|
||||||
]);*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,3 +40,10 @@ export class UpdatePentestFindings {
|
||||||
constructor(public findingId: string) {
|
constructor(public findingId: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UpdatePentestComments {
|
||||||
|
static readonly type = '[ProjectState] UpdatePentestComments';
|
||||||
|
|
||||||
|
constructor(public commentId: string) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
ChangeCategory,
|
ChangeCategory,
|
||||||
ChangePentest,
|
ChangePentest,
|
||||||
ChangeProject,
|
ChangeProject,
|
||||||
InitProjectState,
|
InitProjectState, UpdatePentestComments,
|
||||||
UpdatePentestFindings
|
UpdatePentestFindings
|
||||||
} from '@shared/stores/project-state/project-state.actions';
|
} from '@shared/stores/project-state/project-state.actions';
|
||||||
import {Category} from '@shared/models/category.model';
|
import {Category} from '@shared/models/category.model';
|
||||||
|
@ -108,4 +108,26 @@ export class ProjectState {
|
||||||
selectedPentest: stateSelectedPentest
|
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": []
|
"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[]
|
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
|
== Change History
|
||||||
|
|
||||||
|===
|
|===
|
||||||
|Date |Change
|
|Date |Change
|
||||||
|
|2022-12-22
|
||||||
|
|Added GET, POST endpoint for Comment
|
||||||
|2022-12-09
|
|2022-12-09
|
||||||
|
|Added DELETE endpoint for Finding
|
||||||
|
|2022-12-08
|
||||||
|Added GET and PATCH endpoint for Finding
|
|Added GET and PATCH endpoint for Finding
|
||||||
|2022-12-02
|
|2022-12-02
|
||||||
|Added GET and POST endpoint for Findings
|
|Added GET and POST endpoint for Findings
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package com.securityc4po.api.comment
|
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 org.springframework.data.mongodb.core.index.Indexed
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -10,3 +12,55 @@ data class Comment (
|
||||||
val description: String,
|
val description: String,
|
||||||
val relatedFindings: List<String>? = emptyList()
|
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),
|
PentestNotFound(1003),
|
||||||
FindingsNotFound(1004),
|
FindingsNotFound(1004),
|
||||||
FindingNotFound(1005),
|
FindingNotFound(1005),
|
||||||
|
CommentsNotFound(1006),
|
||||||
|
CommentNotFound(1007),
|
||||||
|
|
||||||
// 2XXX Already Changed
|
// 2XXX Already Changed
|
||||||
ProjectAlreadyChanged(2001),
|
ProjectAlreadyChanged(2001),
|
||||||
|
@ -20,6 +22,7 @@ enum class Errorcode(val code: Int) {
|
||||||
TokenWithoutField(3004),
|
TokenWithoutField(3004),
|
||||||
UserIdIsEmpty(3005),
|
UserIdIsEmpty(3005),
|
||||||
FindingInvalid(3006),
|
FindingInvalid(3006),
|
||||||
|
CommentInvalid(3007),
|
||||||
|
|
||||||
// 4XXX Unauthorized
|
// 4XXX Unauthorized
|
||||||
ProjectAdjustmentNotAuthorized(4000),
|
ProjectAdjustmentNotAuthorized(4000),
|
||||||
|
@ -39,4 +42,7 @@ enum class Errorcode(val code: Int) {
|
||||||
PentestInsertionFailed(6007),
|
PentestInsertionFailed(6007),
|
||||||
ProjectPentestInsertionFailed(6008),
|
ProjectPentestInsertionFailed(6008),
|
||||||
FindingInsertionFailed(6009),
|
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
|
* Validates if a [FindingRequestBody] is valid
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.securityc4po.api.finding
|
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.Query
|
||||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
|
@ -14,4 +15,7 @@ interface FindingRepository : ReactiveMongoRepository<FindingEntity, String> {
|
||||||
|
|
||||||
@Query("{'data._id' :{\$in: ?0 }}")
|
@Query("{'data._id' :{\$in: ?0 }}")
|
||||||
fun findFindingsByIds(id: List<String>): Flux<FindingEntity>
|
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
|
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 refNumber: String,
|
||||||
val status: PentestStatus,
|
val status: PentestStatus,
|
||||||
var findingIds: List<String> = emptyList(),
|
var findingIds: List<String> = emptyList(),
|
||||||
val commentIds: List<String> = emptyList()
|
var commentIds: List<String> = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
fun buildPentest(body: PentestRequestBody, pentestEntity: PentestEntity): Pentest {
|
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.ResponseBody
|
||||||
import com.securityc4po.api.finding.FindingRequestBody
|
import com.securityc4po.api.finding.FindingRequestBody
|
||||||
import com.securityc4po.api.finding.FindingService
|
import com.securityc4po.api.finding.FindingService
|
||||||
|
import com.securityc4po.api.finding.toFindingDeleteResponseBody
|
||||||
import com.securityc4po.api.finding.toFindingResponseBody
|
import com.securityc4po.api.finding.toFindingResponseBody
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.http.ResponseEntity.noContent
|
import org.springframework.http.ResponseEntity.noContent
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.kotlin.core.publisher.switchIfEmpty
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/pentests")
|
@RequestMapping("/pentests")
|
||||||
|
@ -77,7 +79,7 @@ class PentestController(private val pentestService: PentestService, private val
|
||||||
findingList.map { it.toFindingResponseBody() }
|
findingList.map { it.toFindingResponseBody() }
|
||||||
}
|
}
|
||||||
}.map {
|
}.map {
|
||||||
if (it.isEmpty()) ResponseEntity.noContent().build()
|
if (it.isEmpty()) noContent().build()
|
||||||
else ResponseEntity.ok(it)
|
else ResponseEntity.ok(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,4 +110,16 @@ class PentestController(private val pentestService: PentestService, private val
|
||||||
ResponseEntity.accepted().body(it.toFindingResponseBody())
|
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
|
* Get all [Finding]Id's by pentestId
|
||||||
*
|
*
|
||||||
|
@ -167,4 +201,50 @@ class PentestService(private val pentestRepository: PentestRepository, private v
|
||||||
throw ex
|
throw ex
|
||||||
}.map { pentestEntity -> pentestEntity.data.findingIds }
|
}.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}")
|
@DeleteQuery("{'data._id' : ?0}")
|
||||||
fun deleteProjectById(id: String): Mono<Long>
|
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.relaxedResponseFields(
|
||||||
PayloadDocumentation.fieldWithPath("[].id").type(JsonFieldType.STRING)
|
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)
|
PayloadDocumentation.fieldWithPath("[].severity").type(JsonFieldType.STRING)
|
||||||
.description("The severity of the finding"),
|
.description("The severity of the finding"),
|
||||||
PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING)
|
PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING)
|
||||||
|
@ -325,7 +325,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
|
||||||
),
|
),
|
||||||
PayloadDocumentation.relaxedResponseFields(
|
PayloadDocumentation.relaxedResponseFields(
|
||||||
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
|
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)
|
PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING)
|
||||||
.description("The severity of the finding"),
|
.description("The severity of the finding"),
|
||||||
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
|
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
|
||||||
|
@ -386,11 +386,11 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
|
||||||
),
|
),
|
||||||
PayloadDocumentation.relaxedResponseFields(
|
PayloadDocumentation.relaxedResponseFields(
|
||||||
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
|
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)
|
PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING)
|
||||||
.description("The severity of the finding"),
|
.description("The severity of the finding"),
|
||||||
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
|
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)
|
PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING)
|
||||||
.description("The description number of the finding"),
|
.description("The description number of the finding"),
|
||||||
PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING)
|
PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING)
|
||||||
|
@ -446,7 +446,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
|
||||||
),
|
),
|
||||||
PayloadDocumentation.relaxedResponseFields(
|
PayloadDocumentation.relaxedResponseFields(
|
||||||
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
|
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)
|
PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING)
|
||||||
.description("The severity of the finding"),
|
.description("The severity of the finding"),
|
||||||
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
|
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() {
|
private fun persistBasicTestScenario() {
|
||||||
// setup test data
|
// setup test data
|
||||||
// Project
|
// Project
|
||||||
|
@ -517,7 +554,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
|
||||||
findingIds = emptyList(),
|
findingIds = emptyList(),
|
||||||
commentIds = emptyList()
|
commentIds = emptyList()
|
||||||
)
|
)
|
||||||
// Findings
|
// Finding
|
||||||
val findingOne = Finding(
|
val findingOne = Finding(
|
||||||
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
|
id = "ab62d365-1b1d-4da1-89bc-5496616e220f",
|
||||||
severity = Severity.LOW,
|
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() {
|
private fun persistBasicTestScenario() {
|
||||||
// setup test data
|
// setup test data
|
||||||
// project
|
// 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": {
|
"lastModified": {
|
||||||
"$date": {
|
"$date": {
|
||||||
"$numberLong": "1668425376074"
|
"$numberLong": "1671713973546"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
|
@ -14,9 +14,22 @@
|
||||||
"refNumber": "OTG-INFO-001",
|
"refNumber": "OTG-INFO-001",
|
||||||
"status": "IN_PROGRESS",
|
"status": "IN_PROGRESS",
|
||||||
"findingIds": [
|
"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"
|
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
||||||
},{
|
},{
|
||||||
|
@ -1387,4 +1400,202 @@
|
||||||
"commentIds": []
|
"commentIds": []
|
||||||
},
|
},
|
||||||
"_class": "com.securityc4po.api.pentest.PentestEntity"
|
"_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