From 46f79dcf89c3805a8914e1d32ae26034536961fd Mon Sep 17 00:00:00 2001 From: Marcel Haag Date: Fri, 16 Dec 2022 13:48:45 +0100 Subject: [PATCH] feat: As a user I want to add a comment via dialog --- .../pentest-comments.component.spec.ts | 8 +- .../pentest-comments.component.ts | 70 ++++- .../src/app/pentest/pentest.module.ts | 53 ++-- .../src/assets/i18n/de-DE.json | 26 +- .../src/assets/i18n/en-US.json | 22 ++ .../src/shared/models/comment.model.ts | 27 ++ .../comment-dialog.component.html | 80 ++++++ .../comment-dialog.component.scss | 65 +++++ .../comment-dialog.component.spec.ts | 188 +++++++++++++ .../comment-dialog.component.ts | 170 +++++++++++ .../comment-dialog/comment-dialog.module.ts | 39 +++ .../service/comment-dialog.service.mock.ts | 18 ++ .../service/comment-dialog.service.spec.ts | 30 ++ .../service/comment-dialog.service.ts | 103 +++++++ .../finding-dialog.component.html | 263 +++++++++--------- .../finding-dialog.component.spec.ts | 6 +- .../finding-dialog.component.ts | 1 - .../finding-dialog/finding-dialog.module.ts | 3 - .../shared/services/comment.service.spec.ts | 26 ++ .../src/shared/services/comment.service.ts | 37 +++ .../src/shared/services/pentest.service.ts | 24 -- .../project-state/project-state.actions.ts | 7 + .../stores/project-state/project-state.ts | 24 +- .../security-c4po-api.postman_collection.json | 84 ++++++ .../src/main/asciidoc/SecurityC4PO.adoc | 44 +++ .../com/securityc4po/api/comment/Comment.kt | 54 ++++ .../api/comment/CommentController.kt | 51 ++++ .../api/comment/CommentRepository.kt | 21 ++ .../api/comment/CommentService.kt | 77 +++++ .../configuration/error/handler/Errorcode.kt | 5 + .../com/securityc4po/api/pentest/Pentest.kt | 2 +- .../api/pentest/PentestController.kt | 4 +- .../api/pentest/PentestService.kt | 46 +++ .../CommentControllerDocumentationTest.kt | 218 +++++++++++++++ .../CommentControllerIntegrationTest.kt | 178 ++++++++++++ .../PentestControllerDocumentationTest.kt | 4 +- .../test/resources/collections/comments.json | 54 ++++ .../test/resources/collections/pentests.json | 217 ++++++++++++++- 38 files changed, 2136 insertions(+), 213 deletions(-) create mode 100644 security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.html create mode 100644 security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.scss create mode 100644 security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.spec.ts create mode 100644 security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.ts create mode 100644 security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.module.ts create mode 100644 security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.mock.ts create mode 100644 security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.spec.ts create mode 100644 security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.ts create mode 100644 security-c4po-angular/src/shared/services/comment.service.spec.ts create mode 100644 security-c4po-angular/src/shared/services/comment.service.ts create mode 100644 security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/CommentController.kt create mode 100644 security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/CommentRepository.kt create mode 100644 security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/CommentService.kt create mode 100644 security-c4po-api/src/test/kotlin/com/securityc4po/api/comment/CommentControllerDocumentationTest.kt create mode 100644 security-c4po-api/src/test/kotlin/com/securityc4po/api/comment/CommentControllerIntegrationTest.kt create mode 100644 security-c4po-api/src/test/resources/collections/comments.json diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.spec.ts b/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.spec.ts index c6998ef..fca89fb 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.spec.ts +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.spec.ts @@ -18,6 +18,10 @@ import {NotificationService} from '@shared/services/notification.service'; import {NotificationServiceMock} from '@shared/services/notification.service.mock'; import {MockComponent} from 'ng-mocks'; import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock'; +import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service'; +import {CommentDialogServiceMock} from '@shared/modules/comment-dialog/service/comment-dialog.service.mock'; const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { selectedProject: { @@ -74,7 +78,9 @@ describe('PentestCommentsComponent', () => { NgxsModule.forRoot([ProjectState]) ], providers: [ - {provide: NotificationService, useValue: new NotificationServiceMock()} + {provide: NotificationService, useValue: new NotificationServiceMock()}, + {provide: DialogService, useClass: DialogServiceMock}, + {provide: CommentDialogService, useClass: CommentDialogServiceMock}, ] }) .compileComponents(); diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts b/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts index 6680ac0..630137b 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts @@ -3,15 +3,25 @@ import {BehaviorSubject, Observable} from 'rxjs'; import {Pentest} from '@shared/models/pentest.model'; import * as FA from '@fortawesome/free-solid-svg-icons'; import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme'; -import {PentestService} from '@shared/services/pentest.service'; -import {NotificationService} from '@shared/services/notification.service'; +import {NotificationService, PopupType} from '@shared/services/notification.service'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; -import {filter, tap} from 'rxjs/operators'; -import {Comment, CommentEntry, transformCommentsToObjectiveEntries} from '@shared/models/comment.model'; +import {filter, mergeMap, tap} from 'rxjs/operators'; +import { + Comment, + CommentDialogBody, + CommentEntry, + transformCommentsToObjectiveEntries, + transformCommentToRequestBody +} from '@shared/models/comment.model'; import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined'; import {ProjectState} from '@shared/stores/project-state/project-state'; import {Store} from '@ngxs/store'; import {PentestStatus} from '@shared/models/pentest-status.model'; +import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service'; +import {CommentService} from '@shared/services/comment.service'; +import {UpdatePentestComments, UpdatePentestFindings} from '@shared/stores/project-state/project-state.actions'; @UntilDestroy() @Component({ @@ -42,10 +52,12 @@ export class PentestCommentsComponent implements OnInit { expandedGetter: (node: CommentEntry) => !!node.expanded, }; - constructor(private readonly pentestService: PentestService, + constructor(private readonly commentService: CommentService, private dataSourceBuilder: NbTreeGridDataSourceBuilder, - private store: Store, - private notificationService: NotificationService) { + private notificationService: NotificationService, + private dialogService: DialogService, + private commentDialogService: CommentDialogService, + private store: Store) { this.dataSource = dataSourceBuilder.create(this.data, this.getters); } @@ -64,7 +76,7 @@ export class PentestCommentsComponent implements OnInit { } loadCommentsData(): void { - this.pentestService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '') + this.commentService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '') .pipe( untilDestroyed(this), /*filter(isNotNullOrUndefined),*/ @@ -90,15 +102,47 @@ export class PentestCommentsComponent implements OnInit { } onClickAddComment(): void { - console.info('Coming soon..'); + this.commentDialogService.openCommentDialog( + FindingDialogComponent, + this.pentestInfo$.getValue().findingIds, + null, + { + closeOnEsc: false, + hasScroll: false, + autoFocus: false, + closeOnBackdropClick: false + } + ).pipe( + filter(value => !!value), + tap((value) => console.warn('CommentDialogBody: ', value)), + mergeMap((value: CommentDialogBody) => + this.commentService.saveComment( + this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', + transformCommentToRequestBody(value) + ) + ), + untilDestroyed(this) + ).subscribe({ + next: (newComment: Comment) => { + this.store.dispatch(new UpdatePentestComments(newComment.id)); + this.loadCommentsData(); + // Todo: Fix trans keys + this.notificationService.showPopup('comment.popup.save.success', PopupType.SUCCESS); + }, + error: err => { + console.error(err); + // Todo: Fix trans keys + this.notificationService.showPopup('comment.popup.save.failed', PopupType.FAILURE); + } + }); } - onClickEditComment(comment): void { - console.info('Coming soon..'); + onClickEditComment(commentEntry): void { + console.info('Coming soon..', commentEntry); } - onClickDeleteComment(comment): void { - console.info('Coming soon..'); + onClickDeleteComment(commentEntry): void { + console.info('Coming soon..', commentEntry); } // HTML only diff --git a/security-c4po-angular/src/app/pentest/pentest.module.ts b/security-c4po-angular/src/app/pentest/pentest.module.ts index 6a43fe2..0f244b3 100644 --- a/security-c4po-angular/src/app/pentest/pentest.module.ts +++ b/security-c4po-angular/src/app/pentest/pentest.module.ts @@ -3,18 +3,19 @@ import {CommonModule} from '@angular/common'; import {RouterModule} from '@angular/router'; import {PentestComponent} from './pentest.component'; import {NbButtonModule, NbCardModule, NbLayoutModule, NbSelectModule, NbTabsetModule, NbTreeGridModule} from '@nebular/theme'; -import { PentestHeaderComponent } from './pentest-header/pentest-header.component'; -import { PentestContentComponent } from './pentest-content/pentest-content.component'; +import {PentestHeaderComponent} from './pentest-header/pentest-header.component'; +import {PentestContentComponent} from './pentest-content/pentest-content.component'; import {FlexLayoutModule} from '@angular/flex-layout'; import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; import {TranslateModule} from '@ngx-translate/core'; import {StatusTagModule} from '@shared/widgets/status-tag/status-tag.module'; -import { PentestInfoComponent } from './pentest-content/pentest-info/pentest-info.component'; -import { PentestFindingsComponent } from './pentest-content/pentest-findings/pentest-findings.component'; -import { PentestCommentsComponent } from './pentest-content/pentest-comments/pentest-comments.component'; +import {PentestInfoComponent} from './pentest-content/pentest-info/pentest-info.component'; +import {PentestFindingsComponent} from './pentest-content/pentest-findings/pentest-findings.component'; +import {PentestCommentsComponent} from './pentest-content/pentest-comments/pentest-comments.component'; import {CommonAppModule} from '../common-app.module'; import {SeverityTagModule} from '@shared/widgets/severity-tag/severity-tag.module'; import {FindingDialogModule} from '@shared/modules/finding-dialog/finding-dialog.module'; +import {CommentDialogModule} from '@shared/modules/comment-dialog/comment-dialog.module'; @NgModule({ declarations: [ @@ -25,26 +26,28 @@ import {FindingDialogModule} from '@shared/modules/finding-dialog/finding-dialog PentestFindingsComponent, PentestCommentsComponent ], - imports: [ - CommonModule, - CommonAppModule, - RouterModule.forChild([{ - path: '', - component: PentestComponent - }]), - NbLayoutModule, - NbCardModule, - FlexLayoutModule, - FontAwesomeModule, - TranslateModule, - NbButtonModule, - StatusTagModule, - NbTabsetModule, - NbTreeGridModule, - SeverityTagModule, - FindingDialogModule, - NbSelectModule - ] + imports: [ + CommonModule, + CommonAppModule, + RouterModule.forChild([{ + path: '', + component: PentestComponent + }]), + NbLayoutModule, + NbCardModule, + FlexLayoutModule, + FontAwesomeModule, + TranslateModule, + NbButtonModule, + StatusTagModule, + NbTabsetModule, + NbTreeGridModule, + SeverityTagModule, + NbSelectModule, + // Dialog Modules + FindingDialogModule, + CommentDialogModule, + ] }) export class PentestModule { } diff --git a/security-c4po-angular/src/assets/i18n/de-DE.json b/security-c4po-angular/src/assets/i18n/de-DE.json index 06fcb76..eb25746 100644 --- a/security-c4po-angular/src/assets/i18n/de-DE.json +++ b/security-c4po-angular/src/assets/i18n/de-DE.json @@ -10,6 +10,7 @@ "action.exit": "Beenden", "action.update": "Speichern", "action.export": "Exportieren", + "action.reset": "Zurücksetzen", "action.yes": "Ja", "action.no": "Nein", "username": "Nutzername", @@ -148,10 +149,31 @@ "commentId": "Kommentar Id", "title": "Titel", "description": "Beschreibung", - "relatedFindings": "Verwandte Funde", + "relatedFindings": "Verbundene Funde", "add": "Kommentar hinzufügen", - "no.relatedFindings": "Nicht verbunden mit Fund", + "add.finding": "Fund hinzufügen", "no.comments": "Keine Kommentare verfügbar", + "no.relatedFindings": "Nicht verbunden mit Fund", + "relatedFindingsPlaceholder": "Fund auswählen", + "noFindingsInObjectivePlaceholder": "Objective hat keine Befunde, auf die es sich beziehen könnte.", + "create": { + "header": "Neuen Kommentar erstellen" + }, + "edit": { + "header": "Kommentar editieren" + }, + "delete": { + "title": "Kommentar löschen", + "key": "Möchten Sie den Kommentar \"{{name}}\" unwiderruflich löschen?" + }, + "title.label": "Kommentartitel", + "description.label": "Beschreibung des Kommentars", + "relatedFindings.label": "Verbundene Funde", + "validationMessage": { + "titleRequired": "Titel ist erforderlich.", + "descriptionRequired": "Beschreibung ist erforderlich.", + "relatedFindings": "Verwandte Funde erforderlich." + }, "popup": { "not.found": "Keine Kommentare gefunden", "save.success": "Kommentar erfolgreich gespeichert", diff --git a/security-c4po-angular/src/assets/i18n/en-US.json b/security-c4po-angular/src/assets/i18n/en-US.json index 39c0d16..405e4ae 100644 --- a/security-c4po-angular/src/assets/i18n/en-US.json +++ b/security-c4po-angular/src/assets/i18n/en-US.json @@ -10,6 +10,7 @@ "action.exit": "Exit", "action.update": "Update", "action.export": "Export", + "action.reset": "Reset", "action.yes": "Yes", "action.no": "No", "username": "Username", @@ -150,8 +151,29 @@ "description": "Description", "relatedFindings": "Related Findings", "add": "Add Comment", + "add.finding": "Add related finding", "no.comments": "No comments available", "no.relatedFindings": "Not related to finding", + "relatedFindingsPlaceholder": "Select a related finding", + "noFindingsInObjectivePlaceholder": "Objective doesn't have any findings to relate to.", + "create": { + "header": "Create New Comment" + }, + "edit": { + "header": "Edit Comment" + }, + "delete": { + "title": "Delete Comment", + "key": "Do you want to permanently delete the comment \"{{name}}\"?" + }, + "title.label": "Comment Title", + "description.label": "Description of Comment", + "relatedFindings.label": "Related Findings", + "validationMessage": { + "titleRequired": "Title is required.", + "descriptionRequired": "Description is required.", + "relatedFindings": "Related findings required." + }, "popup": { "not.found": "No comment found", "save.success": "Comment saved successfully", diff --git a/security-c4po-angular/src/shared/models/comment.model.ts b/security-c4po-angular/src/shared/models/comment.model.ts index abd9586..3d10c74 100644 --- a/security-c4po-angular/src/shared/models/comment.model.ts +++ b/security-c4po-angular/src/shared/models/comment.model.ts @@ -42,3 +42,30 @@ export function transformCommentsToObjectiveEntries(findings: Comment[]): Commen }); return findingEntries; } + +export function transformCommentToRequestBody(comment: CommentDialogBody | Comment): Comment { + const transformedComment = { + ...comment, + title: comment.title, + description: comment.description, + // Transforms related findings from RelatedFindingOption to list of finding ids + relatedFindings: comment.relatedFindings ? comment.relatedFindings.map(finding => finding.value.id) : [], + /* Remove Table Entry Object Properties */ + childEntries: undefined, + kind: undefined, + findings: undefined, + expanded: undefined, + } as unknown as Comment; + return transformedComment; +} + +export interface CommentDialogBody { + title: string; + description: string; + relatedFindings: Array; +} + +export interface RelatedFindingOption { + id: string; + title: string; +} diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.html b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.html new file mode 100644 index 0000000..fcae7b1 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.html @@ -0,0 +1,80 @@ + + + {{ dialogData?.options[0].headerLabelKey | translate }} + + +
+
+ +
+ + + + + + + + {{error.translationKey | translate}} + + + + + + + + + + + {{error.translationKey | translate}} + + + +
+
+ + + + + + + {{'global.action.reset' | translate}} + + {{finding.title}} + + + +
+
+ + + + +
diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.scss b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.scss new file mode 100644 index 0000000..2076e7f --- /dev/null +++ b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.scss @@ -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); + } +} diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.spec.ts b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.spec.ts new file mode 100644 index 0000000..178b4d1 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.spec.ts @@ -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; + 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 } => { + 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' + }, + ] +}; diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.ts b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.ts new file mode 100644 index 0000000..4ba9e7e --- /dev/null +++ b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.ts @@ -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, + 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; + } +} diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.module.ts b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.module.ts new file mode 100644 index 0000000..dce4c21 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.module.ts @@ -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 { +} diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.mock.ts b/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.mock.ts new file mode 100644 index 0000000..853f3ec --- /dev/null +++ b/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.mock.ts @@ -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 { + + dialog: any; + + openCommentDialog( + componentOrTemplateRef: ComponentType, + findingIds: [], + comment: Comment | undefined, + config: Partial | string>> | undefined): Observable { + return of(undefined); + } +} diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.spec.ts b/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.spec.ts new file mode 100644 index 0000000..285b1e3 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.spec.ts @@ -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(); + }); +}); diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.ts b/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.ts new file mode 100644 index 0000000..b49e009 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.ts @@ -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 | string>>, + commentData?: GenericDialogData + ): Partial | 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, + findingIds: string[], + comment?: Comment, + config?: Partial | string>>): Observable { + let dialogOptions: Partial | 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; + } +} diff --git a/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.html b/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.html index 910c9f4..a118cd6 100644 --- a/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.html +++ b/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.html @@ -4,168 +4,167 @@
-
-
- -
- - - - - - +
+ +
+ + + + + + {{error.translationKey | translate}} - - - - - - - - + + {{error.translationKey | translate}} - - - - - - - - + + {{error.translationKey | translate}} - - -
- - -
- - - - {{ severity.translationText | translate }} - - -
+ +
- - - -
+ + + + + + + {{error.translationKey | translate}} - - - - - - - - - -
- - - - - - + + {{error.translationKey | translate}} - - - - - - - - + + {{error.translationKey | translate}} - - -
+ +
diff --git a/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.spec.ts b/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.spec.ts index b743bc3..ad9dc3c 100644 --- a/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.spec.ts +++ b/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.spec.ts @@ -96,13 +96,13 @@ describe('FindingDialogComponent', () => { {provide: NotificationService, useValue: new NotificationServiceMock()}, {provide: DialogService, useClass: DialogServiceMock}, {provide: NbDialogRef, useValue: dialogSpy}, - {provide: NB_DIALOG_CONFIG, useValue: mockedFindingDialogData} + {provide: NB_DIALOG_CONFIG, useValue: mockedCommentDialogData} ] }).compileComponents(); }); beforeEach(() => { - TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedFindingDialogData}); + TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedCommentDialogData}); fixture = TestBed.createComponent(FindingDialogComponent); store = TestBed.inject(Store); store.reset({ @@ -137,7 +137,7 @@ export const mockFinding: Finding = { mitigation: 'Mitigation Test' }; -export const mockedFindingDialogData = { +export const mockedCommentDialogData = { form: { findingTitle: { fieldName: 'findingTitle', diff --git a/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.ts b/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.ts index 5e41a81..452cc40 100644 --- a/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.ts +++ b/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.ts @@ -6,7 +6,6 @@ import deepEqual from 'deep-equal'; import {UntilDestroy} from '@ngneat/until-destroy'; import {Severity} from '@shared/models/severity.enum'; import * as FA from '@fortawesome/free-solid-svg-icons'; -import {BehaviorSubject} from 'rxjs'; @UntilDestroy() @Component({ diff --git a/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.module.ts b/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.module.ts index 10d5449..c41b13a 100644 --- a/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.module.ts +++ b/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.module.ts @@ -1,12 +1,9 @@ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component'; -import {DialogService} from '@shared/services/dialog-service/dialog.service'; import { NbButtonModule, NbCardModule, - NbDialogModule, - NbDialogService, NbFormFieldModule, NbInputModule, NbSelectModule, diff --git a/security-c4po-angular/src/shared/services/comment.service.spec.ts b/security-c4po-angular/src/shared/services/comment.service.spec.ts new file mode 100644 index 0000000..a5c24fe --- /dev/null +++ b/security-c4po-angular/src/shared/services/comment.service.spec.ts @@ -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(); + }); +}); diff --git a/security-c4po-angular/src/shared/services/comment.service.ts b/security-c4po-angular/src/shared/services/comment.service.ts new file mode 100644 index 0000000..136c32f --- /dev/null +++ b/security-c4po-angular/src/shared/services/comment.service.ts @@ -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 { + return this.http.get(`${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 { + return this.http.post(`${this.apiBaseURL}/${pentestId}/comment`, comment); + } +} diff --git a/security-c4po-angular/src/shared/services/pentest.service.ts b/security-c4po-angular/src/shared/services/pentest.service.ts index 7d45c2f..dbe17d6 100644 --- a/security-c4po-angular/src/shared/services/pentest.service.ts +++ b/security-c4po-angular/src/shared/services/pentest.service.ts @@ -121,28 +121,4 @@ export class PentestService { public deleteFindingByPentestAndFindingId(pentestId: string, findingId: string): Observable { return this.http.delete(`${this.apiBaseURL}/${pentestId}/finding/${findingId}`); } - - /** - * Get Comments for Pentest Id - * @param pentestId the id of the project - */ - public getCommentsByPentestId(pentestId: string): Observable { - return this.http.get(`${this.apiBaseURL}/${pentestId}/comments`); - // return of([]); - /* ToDo: Use mocked Comments? - return of([ - { - id: 'ca96cc19-88ff-4874-8406-dc892620afd2', - title: 'This is a creative title', - description: 'This is a creative description', - relatedFindings: ['ca96cc19-88ff-4874-8406-dc892620afd4'], - }, - { - id: 'ca96cc19-88ff-4874-8406-dc892620afd4', - title: 'This is a creative title', - description: 'This is a creative description', - relatedFindings: [], - } - ]);*/ - } } diff --git a/security-c4po-angular/src/shared/stores/project-state/project-state.actions.ts b/security-c4po-angular/src/shared/stores/project-state/project-state.actions.ts index d97a00d..5b0fd70 100644 --- a/security-c4po-angular/src/shared/stores/project-state/project-state.actions.ts +++ b/security-c4po-angular/src/shared/stores/project-state/project-state.actions.ts @@ -40,3 +40,10 @@ export class UpdatePentestFindings { constructor(public findingId: string) { } } + +export class UpdatePentestComments { + static readonly type = '[ProjectState] UpdatePentestComments'; + + constructor(public commentId: string) { + } +} diff --git a/security-c4po-angular/src/shared/stores/project-state/project-state.ts b/security-c4po-angular/src/shared/stores/project-state/project-state.ts index 07be0d9..709a9ee 100644 --- a/security-c4po-angular/src/shared/stores/project-state/project-state.ts +++ b/security-c4po-angular/src/shared/stores/project-state/project-state.ts @@ -5,7 +5,7 @@ import { ChangeCategory, ChangePentest, ChangeProject, - InitProjectState, + InitProjectState, UpdatePentestComments, UpdatePentestFindings } from '@shared/stores/project-state/project-state.actions'; import {Category} from '@shared/models/category.model'; @@ -108,4 +108,26 @@ export class ProjectState { selectedPentest: stateSelectedPentest }); } + + @Action(UpdatePentestComments) + updatePentestComments(ctx: StateContext, {commentId}: UpdatePentestComments): void { + const state = ctx.getState(); + let stateSelectedPentest: Pentest = state.selectedPentest; + const stateCommentIds: Array = stateSelectedPentest.commentIds || []; + let updatedCommentIds: Array = []; + 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 + }); + } } diff --git a/security-c4po-api/security-c4po-api.postman_collection.json b/security-c4po-api/security-c4po-api.postman_collection.json index 344ebe3..672abf8 100644 --- a/security-c4po-api/security-c4po-api.postman_collection.json +++ b/security-c4po-api/security-c4po-api.postman_collection.json @@ -457,6 +457,90 @@ } ] }, + { + "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": [] + } + ] + }, { "name": "getPentestsByProjectIdAndCategory", "request": { diff --git a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc index f24c9d6..0c685de 100644 --- a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc +++ b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc @@ -255,10 +255,54 @@ include::{snippets}/deleteFindingByPentestAndFindingId/http-response.adoc[] include::{snippets}/deleteFindingByPentestAndFindingId/response-fields.adoc[] +== Comment + +=== Get comments for pentest + +To get comments by pentestId, call the GET request /pentests/+{pentestId}+/comments. + +==== Request example + +include::{snippets}/getCommentsByPentestId/http-request.adoc[] + +==== Request structure + +include::{snippets}/getCommentsByPentestId/path-parameters.adoc[] + +==== Response example + +include::{snippets}/getCommentsByPentestId/http-response.adoc[] + +==== Response structure + +include::{snippets}/getCommentsByPentestId/response-fields.adoc[] + +=== Save comment + +To save a comment, call the POST request /pentests/+{pentestId}+/comment + +==== Request example + +include::{snippets}/saveCommentByPentestId/http-request.adoc[] + +==== Request structure + +include::{snippets}/saveCommentByPentestId/path-parameters.adoc[] + +==== Response example + +include::{snippets}/saveCommentByPentestId/http-response.adoc[] + +==== Response structure + +include::{snippets}/saveCommentByPentestId/response-fields.adoc[] + == Change History |=== |Date |Change +|2022-12-22 +|Added GET, POST endpoint for Comment |2022-12-09 |Added DELETE endpoint for Finding |2022-12-08 diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/Comment.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/Comment.kt index 3775d71..39fedf4 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/Comment.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/Comment.kt @@ -1,5 +1,7 @@ package com.securityc4po.api.comment +import com.securityc4po.api.ResponseBody +import com.securityc4po.api.finding.FindingRequestBody import org.springframework.data.mongodb.core.index.Indexed import java.util.* @@ -10,3 +12,55 @@ data class Comment ( val description: String, val relatedFindings: List? = 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? = 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 + ) +} diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/CommentController.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/CommentController.kt new file mode 100644 index 0000000..e1d5066 --- /dev/null +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/CommentController.kt @@ -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() + + @GetMapping("/{pentestId}/comments") + fun getComments(@PathVariable(value = "pentestId") pentestId: String): Mono>> { + return this.pentestService.getCommentIdsByPentestId(pentestId).flatMap { commentIds: List -> + 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> { + return this.commentService.saveComment(pentestId, body).map { + ResponseEntity.accepted().body(it.toCommentResponseBody()) + } + } +} \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/CommentRepository.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/CommentRepository.kt new file mode 100644 index 0000000..a19ba17 --- /dev/null +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/CommentRepository.kt @@ -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 { + + @Query("{'data._id' : ?0}") + fun findCommentById(id: String): Mono + + @Query("{'data._id' :{\$in: ?0 }}") + fun findCommentsByIds(id: List): Flux + + @DeleteQuery("{'data._id' : ?0}") + fun deleteCommentById(id: String): Mono +} \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/CommentService.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/CommentService.kt new file mode 100644 index 0000000..f76ef5c --- /dev/null +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/comment/CommentService.kt @@ -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() + + /** + * 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 { + 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): Mono> { + 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 + } + } +} \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/Errorcode.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/Errorcode.kt index 6833235..28843e8 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/Errorcode.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/configuration/error/handler/Errorcode.kt @@ -7,6 +7,8 @@ enum class Errorcode(val code: Int) { PentestNotFound(1003), FindingsNotFound(1004), FindingNotFound(1005), + CommentsNotFound(1006), + CommentNotFound(1007), // 2XXX Already Changed ProjectAlreadyChanged(2001), @@ -20,6 +22,7 @@ enum class Errorcode(val code: Int) { TokenWithoutField(3004), UserIdIsEmpty(3005), FindingInvalid(3006), + CommentInvalid(3007), // 4XXX Unauthorized ProjectAdjustmentNotAuthorized(4000), @@ -40,4 +43,6 @@ enum class Errorcode(val code: Int) { ProjectPentestInsertionFailed(6008), FindingInsertionFailed(6009), FindingDeletionFailed(6010), + CommentInsertionFailed(6011), + CommentDeletionFailed(6012), } \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/Pentest.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/Pentest.kt index e5f009a..b59eb6e 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/Pentest.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/Pentest.kt @@ -12,7 +12,7 @@ data class Pentest( val refNumber: String, val status: PentestStatus, var findingIds: List = emptyList(), - val commentIds: List = emptyList() + var commentIds: List = emptyList() ) fun buildPentest(body: PentestRequestBody, pentestEntity: PentestEntity): Pentest { diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestController.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestController.kt index e391b06..9c9cb05 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestController.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestController.kt @@ -79,7 +79,7 @@ class PentestController(private val pentestService: PentestService, private val findingList.map { it.toFindingResponseBody() } } }.map { - if (it.isEmpty()) ResponseEntity.noContent().build() + if (it.isEmpty()) noContent().build() else ResponseEntity.ok(it) } } @@ -119,7 +119,7 @@ class PentestController(private val pentestService: PentestService, private val return this.findingService.deleteFindingByPentestAndFindingId(pentestId, findingId).map { ResponseEntity.ok().body(it.toFindingDeleteResponseBody()) }.switchIfEmpty { - Mono.just(ResponseEntity.noContent().build()) + Mono.just(noContent().build()) } } } \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestService.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestService.kt index b3f97e7..044c0e1 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestService.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestService.kt @@ -201,4 +201,50 @@ class PentestService(private val pentestRepository: PentestRepository, private v throw ex }.map { pentestEntity -> pentestEntity.data.findingIds } } + + /** + * Update [Pentest] for Comment + * + * @throws [InvalidModelException] if the [Pentest] is invalid + * @throws [TransactionInterruptedException] if the [Pentest] could not be updated + * @return updated [Pentest] + */ + fun updatePentestComment(pentestId: String, commentId: String): Mono { + 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> { + 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 } + } } \ No newline at end of file diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/comment/CommentControllerDocumentationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/comment/CommentControllerDocumentationTest.kt new file mode 100644 index 0000000..4853cd7 --- /dev/null +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/comment/CommentControllerDocumentationTest.kt @@ -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" + } +} \ No newline at end of file diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/comment/CommentControllerIntegrationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/comment/CommentControllerIntegrationTest.kt new file mode 100644 index 0000000..d148436 --- /dev/null +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/comment/CommentControllerIntegrationTest.kt @@ -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" + } +} \ No newline at end of file diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/PentestControllerDocumentationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/PentestControllerDocumentationTest.kt index a92db86..d79db95 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/PentestControllerDocumentationTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/PentestControllerDocumentationTest.kt @@ -390,7 +390,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING) .description("The severity of the finding"), PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING) - .description("The title of the requested finding"), + .description("The title of the finding"), PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING) .description("The description number of the finding"), PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING) @@ -554,7 +554,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { findingIds = emptyList(), commentIds = emptyList() ) - // Findings + // Finding val findingOne = Finding( id = "ab62d365-1b1d-4da1-89bc-5496616e220f", severity = Severity.LOW, diff --git a/security-c4po-api/src/test/resources/collections/comments.json b/security-c4po-api/src/test/resources/collections/comments.json new file mode 100644 index 0000000..1873d18 --- /dev/null +++ b/security-c4po-api/src/test/resources/collections/comments.json @@ -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" +}] \ No newline at end of file diff --git a/security-c4po-api/src/test/resources/collections/pentests.json b/security-c4po-api/src/test/resources/collections/pentests.json index f28ee18..e8e4c6d 100644 --- a/security-c4po-api/src/test/resources/collections/pentests.json +++ b/security-c4po-api/src/test/resources/collections/pentests.json @@ -4,7 +4,7 @@ }, "lastModified": { "$date": { - "$numberLong": "1668425376074" + "$numberLong": "1671713973546" } }, "data": { @@ -14,9 +14,22 @@ "refNumber": "OTG-INFO-001", "status": "IN_PROGRESS", "findingIds": [ - "ef31449d-71ec-4736-952f-8b20e53117d5" + "ef31449d-71ec-4736-952f-8b20e53117d5", + "0bda8950-94fa-4ec6-8fa7-e09f5a8cd3e8", + "58f63b4e-97fb-4fe8-8527-7996896089d2", + "72886128-b2d9-4a92-bbfe-b54373441321", + "4ddb84f6-068c-4319-a8ee-1000008bb75a", + "42831151-51fd-4348-b829-6b18ddd14fe1", + "559cd0ac-9e64-41f9-892a-4c8a9dd30357", + "5e22d38f-a4f6-4809-84ea-a803b5f1f9fc", + "0bfa7511-fe33-4ab5-9af2-d4ed70c1b350", + "70e413b9-d736-40d2-b7d6-236768b1230c" ], - "commentIds": [] + "commentIds": [ + "89703b19-16c7-49e5-8e33-0c706313e5fe", + "df516de6-ca5e-44a6-ac50-db89bb17aac3", + "e55e943b-6a48-4d84-8d72-b48d7d9de5b7" + ] }, "_class": "com.securityc4po.api.pentest.PentestEntity" },{ @@ -1387,4 +1400,202 @@ "commentIds": [] }, "_class": "com.securityc4po.api.pentest.PentestEntity" +},{ + "_id": { + "$oid": "6374dd53e0136563b96187c1" + }, + "lastModified": { + "$date": { + "$numberLong": "1668603470508" + } + }, + "data": { + "_id": "617cc86d-aaa5-4144-93b6-1799e913ba21", + "projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746", + "category": "INFORMATION_GATHERING", + "refNumber": "OTG-INFO-001", + "status": "OPEN", + "findingIds": [], + "commentIds": [] + }, + "_class": "com.securityc4po.api.pentest.PentestEntity" +},{ + "_id": { + "$oid": "6374de60e0136563b96187c2" + }, + "lastModified": { + "$date": { + "$numberLong": "1668603488283" + } + }, + "data": { + "_id": "58c47263-47b8-4fd5-804d-092dad0a0cbb", + "projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746", + "category": "CONFIGURATION_AND_DEPLOY_MANAGEMENT_TESTING", + "refNumber": "OTG-CONFIG-001", + "status": "IN_PROGRESS", + "findingIds": [], + "commentIds": [] + }, + "_class": "com.securityc4po.api.pentest.PentestEntity" +},{ + "_id": { + "$oid": "6374e698e0136563b96187c3" + }, + "lastModified": { + "$date": { + "$numberLong": "1668605592100" + } + }, + "data": { + "_id": "34ccdaf4-0449-478c-b621-80c3e6417256", + "projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746", + "category": "AUTHORIZATION_TESTING", + "refNumber": "OTG-AUTHZ-001", + "status": "IN_PROGRESS", + "findingIds": [], + "commentIds": [] + }, + "_class": "com.securityc4po.api.pentest.PentestEntity" +},{ + "_id": { + "$oid": "6374e77fe0136563b96187c4" + }, + "lastModified": { + "$date": { + "$numberLong": "1668605823136" + } + }, + "data": { + "_id": "f1c3b1be-e509-48ac-aefd-5ef406f4adaf", + "projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746", + "category": "SESSION_MANAGEMENT_TESTING", + "refNumber": "OTG-SESS-001", + "status": "IN_PROGRESS", + "findingIds": [], + "commentIds": [] + }, + "_class": "com.securityc4po.api.pentest.PentestEntity" +},{ + "_id": { + "$oid": "6374e7d8e0136563b96187c5" + }, + "lastModified": { + "$date": { + "$numberLong": "1668605912794" + } + }, + "data": { + "_id": "a276d02c-28b4-4354-9fda-98280809a17c", + "projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746", + "category": "INPUT_VALIDATION_TESTING", + "refNumber": "OTG-INPVAL-001", + "status": "IN_PROGRESS", + "findingIds": [], + "commentIds": [] + }, + "_class": "com.securityc4po.api.pentest.PentestEntity" +},{ + "_id": { + "$oid": "6374e931e0136563b96187c6" + }, + "lastModified": { + "$date": { + "$numberLong": "1668606263295" + } + }, + "data": { + "_id": "fd26e674-9eea-478b-a82c-36c6cf8e37f9", + "projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746", + "category": "CRYPTOGRAPHY", + "refNumber": "OTG-CRYPST-001", + "status": "IN_PROGRESS", + "findingIds": [], + "commentIds": [] + }, + "_class": "com.securityc4po.api.pentest.PentestEntity" +},{ + "_id": { + "$oid": "6374ec24e0136563b96187c7" + }, + "lastModified": { + "$date": { + "$numberLong": "1668607029078" + } + }, + "data": { + "_id": "29bc612f-c093-40f8-8ebd-e03c9775194d", + "projectId": "dfc490a8-3ea0-452f-9282-7a3cf68d7746", + "category": "BUSINESS_LOGIC_TESTING", + "refNumber": "OTG-BUSLOGIC-001", + "status": "IN_PROGRESS", + "findingIds": [ + "672d9f87-fb3d-4fc5-8c6f-cadf97661ca5" + ], + "commentIds": [] + }, + "_class": "com.securityc4po.api.pentest.PentestEntity" +},{ + "_id": { + "$oid": "6376047e0687d905ca60af1c" + }, + "lastModified": { + "$date": { + "$numberLong": "1668679299852" + } + }, + "data": { + "_id": "33c90b03-e475-407f-b58f-64833aa58480", + "projectId": "ecece991-d3e0-4089-84ef-730bac197fbf", + "category": "INPUT_VALIDATION_TESTING", + "refNumber": "OTG-INPVAL-005", + "status": "IN_PROGRESS", + "findingIds": [ + "bddf810b-f20e-473e-a63d-34fcba7e48ef" + ], + "commentIds": [] + }, + "_class": "com.securityc4po.api.pentest.PentestEntity" +},{ + "_id": { + "$oid": "637611070687d905ca60af1f" + }, + "lastModified": { + "$date": { + "$numberLong": "1668682319332" + } + }, + "data": { + "_id": "4ed11922-0b11-4134-a616-0da781d9ad77", + "projectId": "e86984d1-6ed4-4cb7-aa17-d8d4d7c23f61", + "category": "CLIENT_SIDE_TESTING", + "refNumber": "OTG-CLIENT-001", + "status": "IN_PROGRESS", + "findingIds": [ + "d7c95af7-5434-4768-b62c-5b11f9396276" + ], + "commentIds": [] + }, + "_class": "com.securityc4po.api.pentest.PentestEntity" +},{ + "_id": { + "$oid": "63776766fcdda12bf2e51eb1" + }, + "lastModified": { + "$date": { + "$numberLong": "1670579952464" + } + }, + "data": { + "_id": "52c9ce82-b4f7-48d9-b92d-4895d04ffdf8", + "projectId": "9217fd68-3fad-478b-a900-b05ed549bef2", + "category": "CLIENT_SIDE_TESTING", + "refNumber": "OTG-CLIENT-001", + "status": "IN_PROGRESS", + "findingIds": [ + "cb33fad4-7965-4654-a9f9-f007edaca35c" + ], + "commentIds": [] + }, + "_class": "com.securityc4po.api.pentest.PentestEntity" }] \ No newline at end of file