diff --git a/security-c4po-angular/src/app/objective-overview/objective-table/objective-table.component.ts b/security-c4po-angular/src/app/objective-overview/objective-table/objective-table.component.ts index ac3a136..57618de 100644 --- a/security-c4po-angular/src/app/objective-overview/objective-table/objective-table.component.ts +++ b/security-c4po-angular/src/app/objective-overview/objective-table/objective-table.component.ts @@ -3,12 +3,11 @@ import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@neb import {Pentest, ObjectiveEntry, transformPentestsToObjectiveEntries} from '@shared/models/pentest.model'; import {PentestService} from '@shared/services/pentest.service'; import {Store} from '@ngxs/store'; -import {PROJECT_STATE_NAME, ProjectState} from '@shared/stores/project-state/project-state'; +import {ProjectState} from '@shared/stores/project-state/project-state'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; import {catchError, switchMap, tap} from 'rxjs/operators'; import {BehaviorSubject, Observable, of} from 'rxjs'; import {getTitleKeyForRefNumber} from '@shared/functions/categories/get-title-key-for-ref-number.function'; -import {Route} from '@shared/models/route.enum'; import {Router} from '@angular/router'; import {ChangePentest} from '@shared/stores/project-state/project-state.actions'; @@ -25,6 +24,7 @@ export class ObjectiveTableComponent implements OnInit { dataSource: NbTreeGridDataSource; private data: ObjectiveEntry[] = []; + private pentests$: BehaviorSubject = new BehaviorSubject([]); getters: NbGetters = { dataGetter: (node: ObjectiveEntry) => node, @@ -53,6 +53,8 @@ export class ObjectiveTableComponent implements OnInit { untilDestroyed(this) ).subscribe({ next: (pentests: Pentest[]) => { + // ToDo: Change assignement here + this.pentests$.next(pentests); this.data = transformPentestsToObjectiveEntries(pentests); this.dataSource.setData(this.data, this.getters); this.loading$.next(false); @@ -64,7 +66,7 @@ export class ObjectiveTableComponent implements OnInit { }); } - selectPentest(pentest: Pentest): void { + selectPentest(selectedPentest: Pentest): void { /* ToDo: Include again after fixing pentest route this.router.navigate([Route.PENTEST]) .then( @@ -74,7 +76,8 @@ export class ObjectiveTableComponent implements OnInit { }) ).finally(); */ - this.store.dispatch(new ChangePentest(pentest)); + const statePentest = this.pentests$.getValue().find(pentest => pentest.refNumber === selectedPentest.refNumber); + this.store.dispatch(new ChangePentest(statePentest)); } // HTML only 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 ebabf09..c6998ef 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 @@ -40,8 +40,8 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { refNumber: 'OTF-001', childEntries: [], status: PentestStatus.NOT_STARTED, - findingsIds: [], - commentsIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'] + findingIds: [], + commentIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'] }, }; 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 98dc6c9..d28ee14 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 @@ -4,7 +4,7 @@ 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, PopupType} from '@shared/services/notification.service'; +import {NotificationService} 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'; @@ -63,8 +63,9 @@ export class PentestCommentsComponent implements OnInit { this.loading$.next(false); }, error: err => { - console.log(err); - this.notificationService.showPopup('comment.popup.not.found', PopupType.FAILURE); + console.error(err); + // ToDo: Implement again after proper lazy loading and routing + // this.notificationService.showPopup('comment.popup.not.found', PopupType.FAILURE); this.loading$.next(false); } }); diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-content.component.html b/security-c4po-angular/src/app/pentest/pentest-content/pentest-content.component.html index 511a1be..02d263c 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-content.component.html +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-content.component.html @@ -27,14 +27,27 @@ - +
+ +
+ + + + diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-content.component.spec.ts b/security-c4po-angular/src/app/pentest/pentest-content/pentest-content.component.spec.ts index 99335e7..422fe3a 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-content.component.spec.ts +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-content.component.spec.ts @@ -11,6 +11,8 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {HttpClientTestingModule} from '@angular/common/http/testing'; import {Category} from '@shared/models/category.model'; import {PentestStatus} from '@shared/models/pentest-status.model'; +import {NotificationService} from '@shared/services/notification.service'; +import {NotificationServiceMock} from '@shared/services/notification.service.mock'; const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { selectedProject: { @@ -33,8 +35,8 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { refNumber: 'OTF-001', childEntries: [], status: PentestStatus.NOT_STARTED, - findingsIds: [], - commentsIds: [] + findingIds: [], + commentIds: [] }, }; @@ -60,6 +62,9 @@ describe('PentestContentComponent', () => { }), RouterTestingModule.withRoutes([]), NgxsModule.forRoot([ProjectState]) + ], + providers: [ + {provide: NotificationService, useValue: new NotificationServiceMock()} ] }) .compileComponents(); diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-content.component.ts b/security-c4po-angular/src/app/pentest/pentest-content/pentest-content.component.ts index 8e921d3..307ba0a 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-content.component.ts +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-content.component.ts @@ -1,14 +1,17 @@ import {Component, OnInit} from '@angular/core'; import * as FA from '@fortawesome/free-solid-svg-icons'; -import {BehaviorSubject} from 'rxjs'; -import {Store} from '@ngxs/store'; +import {BehaviorSubject, Observable} from 'rxjs'; +import {Select, Store} from '@ngxs/store'; import {ProjectState} from '@shared/stores/project-state/project-state'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; -import {Pentest} from '@shared/models/pentest.model'; +import {Pentest, transformPentestToRequestBody} from '@shared/models/pentest.model'; import {PentestStatus} from '@shared/models/pentest-status.model'; import {StatusText} from '@shared/widgets/status-tag/status-tag.component'; import {PentestService} from '@shared/services/pentest.service'; import {NotificationService, PopupType} from '@shared/services/notification.service'; +import {Project} from '@shared/models/project.model'; +import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined'; +import {filter} from 'rxjs/operators'; @UntilDestroy() @Component({ @@ -20,6 +23,10 @@ export class PentestContentComponent implements OnInit { // HTML only readonly fa = FA; + @Select(ProjectState.project) + selectedProject$: Observable; + selectedProjectId: string; + pentest$: BehaviorSubject = new BehaviorSubject(null); pentestChanged$: BehaviorSubject = new BehaviorSubject(false); currentNumberOfFindings$: BehaviorSubject = new BehaviorSubject(0); @@ -45,16 +52,29 @@ export class PentestContentComponent implements OnInit { } ngOnInit(): void { + this.selectedProject$.pipe( + filter(isNotNullOrUndefined), + untilDestroyed(this) + ).subscribe({ + next: (project) => { + this.selectedProjectId = project.id; + }, + error: (err) => { + console.error(err); + } + }); + this.store.selectOnce(ProjectState.pentest).pipe( untilDestroyed(this) ).subscribe({ next: (selectedPentest: Pentest) => { + console.warn(selectedPentest); this.pentest$.next(selectedPentest); this.currentStatus = selectedPentest.status; this.initialPentestStatus = selectedPentest.status; - const findings = selectedPentest.findingsIds ? selectedPentest.findingsIds.length : 0; + const findings = selectedPentest.findingIds ? selectedPentest.findingIds.length : 0; this.currentNumberOfFindings$.next(findings); - const comments = selectedPentest.commentsIds ? selectedPentest.commentsIds.length : 0; + const comments = selectedPentest.commentIds ? selectedPentest.commentIds.length : 0; this.currentNumberOfComments$.next(comments); }, error: err => { @@ -65,10 +85,11 @@ export class PentestContentComponent implements OnInit { onClickSavePentest(): void { this.pentest$.next({...this.pentest$.getValue(), status: this.currentStatus}); - console.warn('Updated Pentest: ', this.pentest$.getValue()); - this.pentestService.savePentest(this.pentest$.getValue()) + this.pentestService.savePentest(this.selectedProjectId, transformPentestToRequestBody(this.pentest$.getValue())) .subscribe({ next: (pentest: Pentest) => { + this.pentest$.next(pentest); + this.initialPentestStatus = pentest.status; this.notificationService.showPopup('pentest.popup.save.success', PopupType.SUCCESS); }, error: err => { @@ -78,6 +99,22 @@ export class PentestContentComponent implements OnInit { }); } + onClickUpdatePentest(): void { + this.pentest$.next({...this.pentest$.getValue(), status: this.currentStatus}); + this.pentestService.updatePentest(transformPentestToRequestBody(this.pentest$.getValue())) + .subscribe({ + next: (pentest: Pentest) => { + this.pentest$.next(pentest); + this.initialPentestStatus = pentest.status; + this.notificationService.showPopup('pentest.popup.update.success', PopupType.SUCCESS); + }, + error: err => { + console.log(err); + this.notificationService.showPopup('pentest.popup.update.failed', PopupType.FAILURE); + } + }); + } + /** * @return true if initial pentest Status is different from current pentest status */ diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.spec.ts b/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.spec.ts index 3451b34..b538936 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.spec.ts +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.spec.ts @@ -42,8 +42,8 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { refNumber: 'OTF-001', childEntries: [], status: PentestStatus.NOT_STARTED, - findingsIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'], - commentsIds: [] + findingIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'], + commentIds: [] }, }; diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.ts b/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.ts index 51ad3a2..656c5a2 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.ts +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.ts @@ -65,8 +65,9 @@ export class PentestFindingsComponent implements OnInit { this.loading$.next(false); }, error: err => { - console.log(err); - this.notificationService.showPopup('findings.popup.not.found', PopupType.FAILURE); + console.error(err); + // ToDo: Implement again after proper lazy loading and routing + // this.notificationService.showPopup('findings.popup.not.found', PopupType.FAILURE); this.loading$.next(false); } }); @@ -94,8 +95,8 @@ export class PentestFindingsComponent implements OnInit { this.loadFindingsData(); this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS); }, - error: error => { - console.error(error); + error: err => { + console.error(err); this.notificationService.showPopup('finding.popup.save.failed', PopupType.FAILURE); } }); diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-info/pentest-info.component.spec.ts b/security-c4po-angular/src/app/pentest/pentest-content/pentest-info/pentest-info.component.spec.ts index 21371ca..9b19381 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-info/pentest-info.component.spec.ts +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-info/pentest-info.component.spec.ts @@ -51,8 +51,8 @@ describe('PentestInfoComponent', () => { refNumber: 'OTF-001', childEntries: [], status: PentestStatus.NOT_STARTED, - findingsIds: [], - commentsIds: [] + findingIds: [], + commentIds: [] }); fixture.detectChanges(); }); diff --git a/security-c4po-angular/src/app/pentest/pentest-header/pentest-header.component.spec.ts b/security-c4po-angular/src/app/pentest/pentest-header/pentest-header.component.spec.ts index b83dad9..6ff228b 100644 --- a/security-c4po-angular/src/app/pentest/pentest-header/pentest-header.component.spec.ts +++ b/security-c4po-angular/src/app/pentest/pentest-header/pentest-header.component.spec.ts @@ -33,8 +33,8 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { refNumber: 'OTF-001', childEntries: [], status: PentestStatus.NOT_STARTED, - findingsIds: [], - commentsIds: [] + findingIds: [], + commentIds: [] }, }; diff --git a/security-c4po-angular/src/app/project-overview/project-overview.component.html b/security-c4po-angular/src/app/project-overview/project-overview.component.html index a23ea4f..42effdd 100644 --- a/security-c4po-angular/src/app/project-overview/project-overview.component.html +++ b/security-c4po-angular/src/app/project-overview/project-overview.component.html @@ -36,13 +36,11 @@
- - {{'popup.info' | translate}} {{'global.no.progress' | translate}} diff --git a/security-c4po-angular/src/shared/models/pentest.model.ts b/security-c4po-angular/src/shared/models/pentest.model.ts index 40ff95e..9f36c16 100644 --- a/security-c4po-angular/src/shared/models/pentest.model.ts +++ b/security-c4po-angular/src/shared/models/pentest.model.ts @@ -1,28 +1,31 @@ -import {v4 as UUID} from 'uuid'; import {PentestStatus} from '@shared/models/pentest-status.model'; import {Category} from '@shared/models/category.model'; +import {v4 as UUID} from 'uuid'; export class Pentest { id?: string; + projectId?: string; category: Category; refNumber: string; childEntries?: Pentest[]; status: PentestStatus; - findingsIds?: Array; - commentsIds?: Array; + findingIds?: Array; + commentIds?: Array; constructor(category: Category, refNumber: string, status: PentestStatus, id?: string, + projectId?: string, findingsIds?: Array, commentsIds?: Array) { this.id = id ? id : UUID(); + this.projectId = projectId ? projectId : ''; this.category = category; this.refNumber = refNumber; this.status = status; - this.findingsIds = findingsIds ? findingsIds : []; - this.commentsIds = commentsIds ? commentsIds : []; + this.findingIds = findingsIds ? findingsIds : []; + this.commentIds = commentsIds ? commentsIds : []; } } @@ -35,13 +38,31 @@ export interface ObjectiveEntry { expanded?: boolean; } +export function transformPentestToRequestBody(pentest: Pentest): Pentest { + const transformedPentest = { + ...pentest, + projectId: pentest.projectId, + category: typeof pentest.category === 'number' ? Category[pentest.category] : pentest.category, + refNumber: pentest.refNumber, + status: pentest.status, + findingIds: pentest.findingIds ? pentest.findingIds : [], + commentIds: pentest.commentIds ? pentest.commentIds : [], + /* Remove Table Entry Object Properties */ + childEntries: undefined, + kind: undefined, + findings: undefined, + expanded: undefined, + } as unknown as Pentest; + return transformedPentest; +} + export function transformPentestsToObjectiveEntries(pentests: Pentest[]): ObjectiveEntry[] { const objectiveEntries: ObjectiveEntry[] = []; pentests.forEach((value: Pentest) => { objectiveEntries.push({ refNumber: value.refNumber, status: value.status, - findings: value.findingsIds ? value.findingsIds.length : 0, + findings: value.findingIds ? value.findingIds.length : 0, kind: value.childEntries ? 'dir' : 'cell', childEntries: value.childEntries ? value.childEntries : null, expanded: !!value.childEntries 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 8e218a5..b743bc3 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 @@ -52,8 +52,8 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = { refNumber: 'OTF-001', childEntries: [], status: PentestStatus.NOT_STARTED, - findingsIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'], - commentsIds: [] + findingIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'], + commentIds: [] }, }; diff --git a/security-c4po-angular/src/shared/services/pentest.service.ts b/security-c4po-angular/src/shared/services/pentest.service.ts index b363761..e42a519 100644 --- a/security-c4po-angular/src/shared/services/pentest.service.ts +++ b/security-c4po-angular/src/shared/services/pentest.service.ts @@ -11,6 +11,7 @@ import {getTempPentestsForCategory} from '@shared/functions/categories/get-temp- import {Finding, FindingDialogBody} from '@shared/models/finding.model'; import {Severity} from '@shared/models/severity.enum'; import {Comment} from '@shared/models/comment.model'; +import {v4 as UUID} from 'uuid'; @Injectable({ providedIn: 'root' @@ -29,17 +30,26 @@ export class PentestService { * @param category the categories of which the pentests should be requested */ public loadPentests(category: Category): Observable { - return this.store.selectOnce(ProjectState.project).pipe( + return this.store.select(ProjectState.project).pipe( switchMap(project => this.getPentestByProjectIdAndCategory(project.id, category)), catchError(_ => of(null)), - map(response => { - let pentests = response; - if (!pentests) { - pentests = getTempPentestsForCategory(category); - // tslint:disable-next-line:no-console - console.info('Initial pentest data loaded.'); + map((response: Pentest[]) => { + // ToDo: Improve performance by only loading templates when not all pentests of category got returned + // Load template pentest + const templatePentests = getTempPentestsForCategory(category); + // The pentests that get returned to the component + let completePentests: Pentest[] = response; + // Add pentest template to complete pentests if not included in request + if (completePentests) { + templatePentests.forEach((templatePentest: Pentest) => { + if (!completePentests.map(it => it.refNumber).includes(templatePentest.refNumber)) { + completePentests.push(templatePentest); + } + }); + } else { + completePentests = templatePentests; } - return pentests; + return completePentests; }) ); } @@ -58,8 +68,16 @@ export class PentestService { * Save Pentest * @param pentest the information of the Pentest */ - public savePentest(pentest: Pentest): Observable { - return this.http.post(`${this.apiBaseURL}/${pentest.id}`, pentest); + public savePentest(projectId: string, pentest: Pentest): Observable { + return this.http.post(`${this.apiBaseURL}/${projectId}`, pentest); + } + + /** + * Update Pentest + * @param pentest the information of the Pentest + */ + public updatePentest(pentest: Pentest): Observable { + return this.http.patch(`${this.apiBaseURL}/${pentest.id}`, pentest); } /** 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 f6f3310..2e0ef30 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 @@ -77,7 +77,7 @@ export class ProjectState { changePentest(ctx: StateContext, {pentest}: ChangePentest): void { const state = ctx.getState(); ctx.patchState({ - selectedPentest: pentest + selectedPentest: {...pentest, projectId: state.selectedProject.id} }); } } diff --git a/security-c4po-api/security-c4po-api.postman_collection.json b/security-c4po-api/security-c4po-api.postman_collection.json index c9492c7..3c2f2c0 100644 --- a/security-c4po-api/security-c4po-api.postman_collection.json +++ b/security-c4po-api/security-c4po-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "58adc500-c0c6-47f3-b268-5fcc16e0944d", + "_postman_id": "6f244dd9-5264-497a-9ea4-1ae73e172624", "name": "security-c4po-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "5225213" @@ -266,7 +266,7 @@ "bearer": [ { "key": "token", - "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NjAxNDI5NjMsImlhdCI6MTY2MDE0MjY2MywianRpIjoiNzk2YzY5NzYtZjBlYS00ZTM0LTk2MTItMjI5ZmE0ODgzOTM0IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiIyYWU1MmQyYy01MjA5LTQzMjEtOWY5OS0wMTQ2YjRkMmNkY2YiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.EO5CC1VXZzybIx-lndq3b61TZpWOnYDI4F2CUFuxj5ECxrlIfm_tlv0TbErDTX311YsA_nhzNHYSaffRzx0OkmmUKSyyH8k9aPRKXUTUmY7Y9PLv3UCKEmAFHAnJkr5kZV08g3UMYG2blpryYBg82abEVMxeMUbh-T4M-Z9dcgQyiZ4nyNMUs1bbfH_2kAtqfEXmP_9eZ42Kwa2ixFWFZDcvOp775bjkYcGvwSnHqmyBXivONzTxyPN6Ug7uFCvMTbeo10ctgOFfXJUZfoxRt-hCspTPJR8C4TzIK41fiy19uRpGjeezG5Ghwy9upXsomunwB4knTAn1otmj4afIxw", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2Njc5OTM4NzYsImlhdCI6MTY2Nzk5MzU3NiwianRpIjoiNTdhOWRiYTYtYzExYy00NGQzLWIzNzItNTQ1MmZjYTk5OTc3IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI5NDY5OTJmNy03MDJhLTQ1NzYtYWI5Yi03MGM5Yzk1MzkwOTIiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.r9EHKziADP6FYV2dfUszNB3Mrm6HwJc5pFWzx-bZ83HGGZ7NRCMkmHhLjAZUdnLcQYDikxzg88KXOM5H9i_0RXtQTgwhYfhuAiOelJTQ8a4YHq0t5vNbG9XmIymtGU5wdiTMM0Z8Dz85sxB9dAl5uKDCh5Eo3gA3r42kJ4reFzU_ldjYGZf7J0yskgGv_JCn9MXYWW7Zp0StegE_XMF1Fl3yWE67uxHOd_fOQExbmGohP9fSmzjAaMfvCt3XtqP2oi9BXuV04zbvqP7-9r2yt58vpyQbRPy-xRgxTIU0wwmnDavKVoji2e8rNaSEuvr_Tu_PJ69uUzBu36vpA4aMxQ", "type": "string" }, { @@ -300,6 +300,78 @@ } }, "response": [] + }, + { + "name": "savePentest", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NjgxNzYyNDYsImlhdCI6MTY2ODE3NTk0NiwianRpIjoiNTFmZWE5YjYtMGY3OS00M2QxLWI1YmItNmEyOTRhMjQyZDIxIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI1NjU5ZDIyMS0yODdiLTQ1ZjktODUzMS00M2I3ZGNhOTExMmUiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.D30yLd7T0Qu4GgEYFbaVQXXqNrC_xJeMqaoBZxh4O0KR_hjn7Udsgdkrb8cs4kQinDasOiLaFBABKSF5pQNXJS_yeRsVW-D4_pBY1yD52_rKwHCjNFRkj1ads0CF4h8tHrJhXcBLQKYB9T0F5hu6q5dsP33q1ej25vJm6yoOu2U33TpqLlOOufNLyGJrtdDzpD9BYsAECLboO3X-KneNfYH_Xl7ECXT3hSMnagFchkQ_sDUuurnyBqg-2-sBGFhBgVgb-ku_aiSeZvRvRY9vPPRIyze6r-bgRM28cgjZvjFtMTjiJeFtwnjcEbAOquX2CRqDo7H3GfJGXrqGHrg8tw", + "type": "string" + }, + { + "key": "undefined", + "type": "any" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"category\": \"INFORMATION_GATHERING\",\n \"refNumber\": \"OTG-INFO-001\",\n \"status\": \"IN_PROGRESS\",\n \"projectId\": \"5a4f126c-9471-43b8-80b9-6eb02b7c35d0\",\n \"findingIds\": [],\n \"commentIds\": []\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8443/pentests/5a4f126c-9471-43b8-80b9-6eb02b7c35d0", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8443", + "path": [ + "pentests", + "5a4f126c-9471-43b8-80b9-6eb02b7c35d0" + ] + } + }, + "response": [] + }, + { + "name": "updatePentest", + "request": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": \"11601f51-bc17-47fd-847d-0c53df5405b5\",\n \"category\": \"INFORMATION_GATHERING\",\n \"refNumber\": \"OTG-INFO-001\",\n \"status\": \"COMPLETED\",\n \"projectId\": \"5a4f126c-9471-43b8-80b9-6eb02b7c35d0\",\n \"findingIds\": [],\n \"commentIds\": []\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8443/pentests/pentestId", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8443", + "path": [ + "pentests", + "pentestId" + ] + } + }, + "response": [] } ] }, 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 6d60e48..b1c0769 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 @@ -34,4 +34,5 @@ enum class Errorcode(val code: Int) { PentestFetchingFailed(6005), ProjectInsertionFailed(6006), PentestInsertionFailed(6007), + ProjectPentestInsertionFailed(6008), } \ 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 55ac055..2f66019 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 @@ -9,22 +9,68 @@ data class Pentest( val id: String = UUID.randomUUID().toString(), val projectId: String, val category: PentestCategory, - val title: String, val refNumber: String, val status: PentestStatus, val findingIds: List = emptyList(), val commentIds: List = emptyList() ) +fun buildPentest(body: PentestRequestBody, pentestEntity: PentestEntity): Pentest { + return Pentest( + id = pentestEntity.data.id, + projectId = body.projectId, + category = PentestCategory.valueOf(body.category), + refNumber = body.refNumber, + status = PentestStatus.valueOf(body.status), + findingIds = body.findingIds, + commentIds = body.commentIds + ) +} + fun Pentest.toPentestResponseBody(): ResponseBody { return mapOf( "id" to id, "projectId" to projectId, "category" to category, - "title" to title, "refNumber" to refNumber, "status" to status, "findingIds" to findingIds, "commentIds" to commentIds ) } + +data class PentestRequestBody( + val projectId: String, + val refNumber: String, + val category: String, + val status: String, + val findingIds: List, + val commentIds: List +) + +/** + * Validates if a [PentestRequestBody] is valid + * + * @return Boolean describing if the body is valid + */ +fun PentestRequestBody.isValid(): Boolean { + return when { + this.projectId.isBlank() -> false + this.refNumber.isBlank() -> false + this.category.isBlank() -> false + this.status.isBlank() -> false + else -> true + } +} + +fun PentestRequestBody.toPentest(): Pentest { + return Pentest( + id = UUID.randomUUID().toString(), + projectId = this.projectId, + category = PentestCategory.valueOf(this.category), + refNumber = this.refNumber, + status = PentestStatus.valueOf(this.status), + findingIds = this.findingIds, + commentIds = this.commentIds + ) +} 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 ee10f02..a06f2e7 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 @@ -15,7 +15,7 @@ import reactor.core.publisher.Mono origins = [], allowCredentials = "false", allowedHeaders = ["*"], - methods = [RequestMethod.GET] + methods = [RequestMethod.GET, RequestMethod.DELETE, RequestMethod.POST, RequestMethod.PATCH] ) @SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION) @@ -28,7 +28,7 @@ class PentestController(private val pentestService: PentestService) { @RequestParam("projectId") projectId: String, @RequestParam("category") category: String ): Mono>> { - return pentestService.getPentests(projectId, PentestCategory.valueOf(category)).map { pentestList -> + return pentestService.getPentestsForCategory(projectId, PentestCategory.valueOf(category)).map { pentestList -> pentestList.map { it.toPentestResponseBody() } @@ -37,4 +37,36 @@ class PentestController(private val pentestService: PentestService) { else ResponseEntity.ok(it) } } + + /* Todo: Add API + @GetMapping + fun getPentestById( + @RequestParam("pentestId") pentestId: String + ): Mono>> { + return pentestService.getPentest(pentestId).map { + ResponseEntity.ok(it) + } + }*/ + + // ToDo: Add Documentation & Tests + @PostMapping("/{projectId}") + fun savePentest( + @PathVariable(value = "projectId") projectId: String, + @RequestBody body: PentestRequestBody + ): Mono> { + return this.pentestService.savePentest(projectId, body).map { + ResponseEntity.accepted().body(it.toPentestResponseBody()) + } + } + + // ToDo: Add Documentation & Tests + @PatchMapping("/{pentestId}") + fun updatePentest( + @PathVariable(value = "pentestId") pentestId: String, + @RequestBody body: PentestRequestBody + ): Mono> { + return this.pentestService.updatePentest(pentestId, body).map { + ResponseEntity.accepted().body(it.toPentestResponseBody()) + } + } } diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestEntity.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestEntity.kt index e1c55c5..ed7af1c 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestEntity.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestEntity.kt @@ -16,7 +16,6 @@ fun PentestEntity.toPentest(): Pentest { this.data.id, this.data.projectId, this.data.category, - this.data.title, this.data.refNumber, this.data.status, this.data.findingIds, diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestRepository.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestRepository.kt index 59e9e3b..5650256 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestRepository.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestRepository.kt @@ -4,6 +4,7 @@ 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 PentestRepository : ReactiveMongoRepository { @@ -11,4 +12,6 @@ interface PentestRepository : ReactiveMongoRepository { @Query("{'data.projectId': ?0, 'data.category': ?1}") fun findPentestByProjectIdAndCategory(projectId: String, category: PentestCategory): Flux + @Query("{'data._id' : ?0}") + fun findPentestById(id: String): Mono } \ 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 f0f05e5..203d2d1 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 @@ -2,14 +2,20 @@ package com.securityc4po.api.pentest 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.InvalidModelException +import com.securityc4po.api.configuration.error.handler.TransactionInterruptedException import com.securityc4po.api.extensions.getLoggerFor +import com.securityc4po.api.project.* import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import org.springframework.stereotype.Service import reactor.core.publisher.Mono +import reactor.kotlin.core.publisher.switchIfEmpty +import java.time.Instant @Service @SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION) -class PentestService(private val pentestRepository: PentestRepository) { +class PentestService(private val pentestRepository: PentestRepository, private val projectService: ProjectService) { var logger = getLoggerFor() @@ -18,9 +24,97 @@ class PentestService(private val pentestRepository: PentestRepository) { * * @return list of [Pentest] */ - fun getPentests(projectId: String, category: PentestCategory): Mono> { + fun getPentestsForCategory(projectId: String, category: PentestCategory): Mono> { return pentestRepository.findPentestByProjectIdAndCategory(projectId, category).collectList().map { it.map { pentestEntity -> pentestEntity.toPentest() } } } + + /** + * Save [Pentest] + * + * @throws [InvalidModelException] if the [Pentest] is invalid + * @throws [TransactionInterruptedException] if the [Pentest] could not be stored + * @return saved [Pentest] + */ + fun savePentest(projectId: String, body: PentestRequestBody): Mono { + validate( + require = body.isValid(), + logging = { logger.warn("Pentest not valid.") }, + mappedException = InvalidModelException( + "Pentest not valid.", Errorcode.PentestInvalid + ) + ) + val pentest = body.toPentest() + val pentestEntity = PentestEntity(pentest) + return pentestRepository.insert(pentestEntity).flatMap { newPentestEntity: PentestEntity -> + val pentest = newPentestEntity.toPentest() + // After successfully saving pentest add id and status to project + val projectPentest = ProjectPentest(pentestId = pentest.id, status = pentest.status) + projectService.updateProjectTestingProgress(projectId, projectPentest).onErrorMap { + TransactionInterruptedException( + "Project Pentests could not be updated in Database.", + Errorcode.ProjectPentestInsertionFailed + ) + }.map { + pentest + } + }.doOnError { + throw wrappedException( + logging = { logger.warn("Pentest could not be stored in Database. Thrown exception: ", it) }, + mappedException = TransactionInterruptedException( + "Pentest could not be stored.", + Errorcode.PentestInsertionFailed + ) + ) + } + } + + /** + * Update [Pentest] + * + * @throws [InvalidModelException] if the [Pentest] is invalid + * @throws [TransactionInterruptedException] if the [Pentest] could not be updated + * @return updated [Pentest] + */ + fun updatePentest(pentestId: String, body: PentestRequestBody): Mono { + validate( + require = body.isValid(), + logging = { logger.warn("Pentest not valid.") }, + mappedException = InvalidModelException( + "Pentest not valid.", Errorcode.PentestInvalid + ) + ) + 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 -> + currentPentestEntity.lastModified = Instant.now() + currentPentestEntity.data = buildPentest(body, currentPentestEntity) + pentestRepository.save(currentPentestEntity).flatMap {newPentestEntity: PentestEntity -> + val pentest = newPentestEntity.toPentest() + // After successfully saving pentest add id and status to project + val projectPentest = ProjectPentest(pentestId = pentest.id, status = pentest.status) + projectService.updateProjectTestingProgress(body.projectId, projectPentest).onErrorMap { + TransactionInterruptedException( + "Project Pentest could not be updated in Database.", + Errorcode.ProjectPentestInsertionFailed + ) + }.map { + return@map newPentestEntity.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 + ) + ) + } + } + + } } \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestStatus.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestStatus.kt index 22f67e0..bf32eae 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestStatus.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestStatus.kt @@ -2,6 +2,7 @@ package com.securityc4po.api.pentest enum class PentestStatus { NOT_STARTED, + DISABLED, OPEN, IN_PROGRESS, COMPLETED diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/Project.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/Project.kt index 5d90708..20b94d9 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/Project.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/Project.kt @@ -3,10 +3,9 @@ package com.securityc4po.api.project import com.fasterxml.jackson.annotation.JsonFormat import com.securityc4po.api.ResponseBody import com.securityc4po.api.pentest.PentestStatus -import org.springframework.beans.factory.annotation.Value import org.springframework.data.mongodb.core.index.Indexed +import java.math.BigDecimal import java.math.RoundingMode -import java.text.DecimalFormat import java.time.Instant import java.util.UUID @@ -18,11 +17,11 @@ data class Project( @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssZ") val createdAt: String = Instant.now().toString(), val tester: String? = null, - val projectPentests: List = emptyList(), + var projectPentests: List = emptyList(), val createdBy: String ) -fun buildProject(body: ProjectRequestBody, projectEntity: ProjectEntity): Project{ +fun buildProject(body: ProjectRequestBody, projectEntity: ProjectEntity): Project { return Project( id = projectEntity.data.id, client = body.client, @@ -36,14 +35,14 @@ fun buildProject(body: ProjectRequestBody, projectEntity: ProjectEntity): Projec fun Project.toProjectResponseBody(): ResponseBody { return mapOf( - "id" to id, - "client" to client, - "title" to title, - "createdAt" to createdAt, - "tester" to tester, - /* ToDo: Calculate percentage in BE type: float */ - "testingProgress" to calculateProgress(), - "createdBy" to createdBy + "id" to id, + "client" to client, + "title" to title, + "createdAt" to createdAt, + "tester" to tester, + /* ToDo: Calculate percentage in BE type: float */ + "testingProgress" to calculateProgress(), + "createdBy" to createdBy ) } @@ -54,32 +53,32 @@ fun Project.toProjectDeleteResponseBody(): ResponseBody { } - -fun Project.calculateProgress(): Float { +fun Project.calculateProgress(): BigDecimal { // Total number of pentests listet in the OWASP testing guide // https://owasp.org/www-project-web-security-testing-guide/assets/archive/OWASP_Testing_Guide_v4.pdf // @Value("\${owasp.web.pentests}") // lateinit var TOTALPENTESTS: Int - val TOTALPENTESTS = 95 + val TOTALPENTESTS = 95.0 return if (projectPentests.isEmpty()) - 0F + BigDecimal.ZERO else { - var completedPentests = 0 + var completedPentests = 0.0 projectPentests.forEach { projectPentest -> + println(projectPentest.toString()) if (projectPentest.status == PentestStatus.COMPLETED) { - completedPentests++ + completedPentests += 1.0 + } else if (projectPentest.status != PentestStatus.NOT_STARTED) { + completedPentests += 0.5 } } - val df = DecimalFormat("#.##") - df.roundingMode = RoundingMode.DOWN - val progress = completedPentests / TOTALPENTESTS - df.format(progress).toFloat() + val progress = (completedPentests * 100) / TOTALPENTESTS + BigDecimal(progress).setScale(2, RoundingMode.HALF_UP) } } data class ProjectOverview( - val projects: List + val projects: List ) data class ProjectRequestBody( @@ -111,6 +110,5 @@ fun ProjectRequestBody.toProject(): Project { tester = this.tester, // ToDo: Should be changed to SUB from Token after adding AUTH Header createdBy = UUID.randomUUID().toString() - -) + ) } diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectPentest.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectPentest.kt index ca860d6..29f35e4 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectPentest.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectPentest.kt @@ -4,5 +4,5 @@ import com.securityc4po.api.pentest.PentestStatus data class ProjectPentest( val pentestId: String, - val status: PentestStatus + var status: PentestStatus ) \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt index 44870c3..838cf57 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt @@ -125,15 +125,39 @@ class ProjectService(private val projectRepository: ProjectRepository) { /** - * Update testing progress of [Project] + * Update testing progress for specific Pentest of [Project] * - * @throws [TransactionInterruptedException] if the [Project] could not be updated - * @return updated list of [ProjectPentest]s + * @throws [TransactionInterruptedException] if the [Project] Pentest could not be updated + * @return updated [Project] */ fun updateProjectTestingProgress( projectId: String, - projectPentests: ProjectPentest - )/*: Mono>*/ { - // ToDo: update Project Entity with progress + projectPentest: ProjectPentest + ): Mono { + return this.projectRepository.findProjectById(projectId).switchIfEmpty { + logger.warn("Project with id $projectId not found. Updating not possible.") + val msg = "Project with id $projectId not found." + val ex = EntityNotFoundException(msg, Errorcode.ProjectNotFound) + throw ex + }.flatMap {projectEntity: ProjectEntity -> + val currentProjectPentestStatus = projectEntity.data.projectPentests.find { projectPentestData -> projectPentestData.pentestId == projectPentest.pentestId } + if (currentProjectPentestStatus !== null) { + projectEntity.data.projectPentests.find { data -> data.pentestId == projectPentest.pentestId }!!.status = projectPentest.status + } else { + projectEntity.data.projectPentests += projectPentest + } + projectEntity.lastModified = Instant.now() + this.projectRepository.save(projectEntity).map { + it.toProject() + }.doOnError { + throw wrappedException( + logging = { logger.warn("Project Pentests could not be updated in Database. Thrown exception: ", it) }, + mappedException = TransactionInterruptedException( + "Project could not be updated.", + Errorcode.ProjectInsertionFailed + ) + ) + } + } } } 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 048afe9..67646cb 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 @@ -75,8 +75,6 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { .description("The id of the project of the requested pentest"), PayloadDocumentation.fieldWithPath("[].category").type(JsonFieldType.STRING) .description("The category of the requested pentest"), - PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING) - .description("The title of the requested pentest"), PayloadDocumentation.fieldWithPath("[].refNumber").type(JsonFieldType.STRING) .description("The reference number of the requested pentest according to the current OWASP Testing Guide"), PayloadDocumentation.fieldWithPath("[].status").type(JsonFieldType.STRING) @@ -94,7 +92,6 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { id = "9c8af320-f608-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002", category = PentestCategory.INFORMATION_GATHERING, - title = "Search engine discovery/reconnaissance", refNumber = "OTG-INFO-001", status = PentestStatus.NOT_STARTED, findingIds = emptyList(), @@ -104,7 +101,6 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { id = "43fbc63c-f624-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002", category = PentestCategory.INFORMATION_GATHERING, - title = "Fingerprint Web Server", refNumber = "OTG-INFO-002", status = PentestStatus.IN_PROGRESS, findingIds = emptyList(), @@ -123,7 +119,6 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { id = "9c8af320-f608-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002", category = PentestCategory.INFORMATION_GATHERING, - title = "Search engine discovery/reconnaissance", refNumber = "OTG-INFO-001", status = PentestStatus.NOT_STARTED, findingIds = emptyList(), @@ -133,7 +128,6 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { id = "43fbc63c-f624-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002", category = PentestCategory.INFORMATION_GATHERING, - title = "Fingerprint Web Server", refNumber = "OTG-INFO-002", status = PentestStatus.IN_PROGRESS, findingIds = emptyList(), @@ -143,7 +137,6 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { id = "74eae112-f62c-11ec-b939-0242ac120002", projectId = "6fad3474-fc29-49f9-bd37-e039e9e60c18", category = PentestCategory.AUTHENTICATION_TESTING, - title = "Testing for Credentials Transported over an Encrypted Channel", refNumber = "OTG-AUTHN-001", status = PentestStatus.COMPLETED, findingIds = emptyList(), diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/PentestControllerIntTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/PentestControllerIntTest.kt index e8c8dac..3f86895 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/PentestControllerIntTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/PentestControllerIntTest.kt @@ -69,7 +69,6 @@ class PentestControllerIntTest : BaseIntTest() { id = "9c8af320-f608-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002", category = PentestCategory.INFORMATION_GATHERING, - title = "Search engine discovery/reconnaissance", refNumber = "OTG-INFO-001", status = PentestStatus.NOT_STARTED, findingIds = emptyList(), @@ -79,7 +78,6 @@ class PentestControllerIntTest : BaseIntTest() { id = "43fbc63c-f624-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002", category = PentestCategory.INFORMATION_GATHERING, - title = "Fingerprint Web Server", refNumber = "OTG-INFO-002", status = PentestStatus.IN_PROGRESS, findingIds = emptyList(), @@ -98,7 +96,6 @@ class PentestControllerIntTest : BaseIntTest() { id = "9c8af320-f608-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002", category = PentestCategory.INFORMATION_GATHERING, - title = "Search engine discovery/reconnaissance", refNumber = "OTG-INFO-001", status = PentestStatus.NOT_STARTED, findingIds = emptyList(), @@ -108,7 +105,6 @@ class PentestControllerIntTest : BaseIntTest() { id = "43fbc63c-f624-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002", category = PentestCategory.INFORMATION_GATHERING, - title = "Fingerprint Web Server", refNumber = "OTG-INFO-002", status = PentestStatus.IN_PROGRESS, findingIds = emptyList(), @@ -118,7 +114,6 @@ class PentestControllerIntTest : BaseIntTest() { id = "74eae112-f62c-11ec-b939-0242ac120002", projectId = "6fad3474-fc29-49f9-bd37-e039e9e60c18", category = PentestCategory.AUTHENTICATION_TESTING, - title = "Testing for Credentials Transported over an Encrypted Channel", refNumber = "OTG-AUTHN-001", status = PentestStatus.COMPLETED, findingIds = emptyList(), diff --git a/security-c4po-api/src/test/resources/collections/pentests.json b/security-c4po-api/src/test/resources/collections/pentests.json new file mode 100644 index 0000000..a23ba31 --- /dev/null +++ b/security-c4po-api/src/test/resources/collections/pentests.json @@ -0,0 +1,20 @@ +[{ + "_id": { + "$oid": "636e332392b4c57eb1693c4f" + }, + "lastModified": { + "$date": { + "$numberLong": "1668176064712" + } + }, + "data": { + "_id": "11601f51-bc17-47fd-847d-0c53df5405b5", + "projectId": "5a4f126c-9471-43b8-80b9-6eb02b7c35d0", + "category": "INFORMATION_GATHERING", + "refNumber": "OTG-INFO-001", + "status": "IN_PROGRESS", + "findingIds": [], + "commentIds": [] + }, + "_class": "com.securityc4po.api.pentest.PentestEntity" +}] \ No newline at end of file diff --git a/security-c4po-api/src/test/resources/collections/projects.json b/security-c4po-api/src/test/resources/collections/projects.json index c0e96b3..f82c12a 100644 --- a/security-c4po-api/src/test/resources/collections/projects.json +++ b/security-c4po-api/src/test/resources/collections/projects.json @@ -4,16 +4,21 @@ }, "lastModified": { "$date": { - "$numberLong": "1660142860140" + "$numberLong": "1668176064717" } }, "data": { "_id": "5a4f126c-9471-43b8-80b9-6eb02b7c35d0", - "client": "E Corp", - "title": "Some Mock API (v1.0) Scanning", + "client": "Dio Stonemask Inc.", + "title": "log4jj bizarre adventure", "createdAt": "2022-08-10T14:47:40.140406Z", - "tester": "Novatester", - "projectPentests": [], + "tester": "Jojo", + "projectPentests": [ + { + "pentestId": "11601f51-bc17-47fd-847d-0c53df5405b5", + "status": "IN_PROGRESS" + } + ], "createdBy": "3c4ae87f-0d56-4634-a824-b4883c403c8a" }, "_class": "com.securityc4po.api.project.ProjectEntity" @@ -38,21 +43,21 @@ "_class": "com.securityc4po.api.project.ProjectEntity" },{ "_id": { - "$oid": "62f3c5427acde34f740ba739" + "$oid": "62ff7534ac2b4d14d86215c4" }, "lastModified": { "$date": { - "$numberLong": "1660142914204" + "$numberLong": "1660908852340" } }, "data": { - "_id": "1120bfa1-0d2b-4e42-a209-0289a1256266", + "_id": "195809ed-9722-4ad5-a84b-0099a9a01652", "client": "Novatec", - "title": "Openspace log4J", - "createdAt": "2022-08-10T14:48:34.204234Z", - "tester": "mhg", + "title": "log4j pentest", + "createdAt": "2022-08-19T11:34:12.339990Z", + "tester": "Stipe", "projectPentests": [], - "createdBy": "5a4a8032-0726-4851-a105-9f079c3989b9" + "createdBy": "7fe49c8d-fee3-47e0-9224-94e0ac7436c6" }, "_class": "com.securityc4po.api.project.ProjectEntity" }] \ No newline at end of file