feat: As a developer, I want to create a pentest

This commit is contained in:
Marcel Haag 2022-11-09 08:20:02 +01:00 committed by Cel
parent de659e3293
commit 84b7c1a07d
30 changed files with 502 additions and 122 deletions

View File

@ -3,12 +3,11 @@ import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@neb
import {Pentest, ObjectiveEntry, transformPentestsToObjectiveEntries} from '@shared/models/pentest.model'; import {Pentest, ObjectiveEntry, transformPentestsToObjectiveEntries} from '@shared/models/pentest.model';
import {PentestService} from '@shared/services/pentest.service'; import {PentestService} from '@shared/services/pentest.service';
import {Store} from '@ngxs/store'; 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 {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {catchError, switchMap, tap} from 'rxjs/operators'; import {catchError, switchMap, tap} from 'rxjs/operators';
import {BehaviorSubject, Observable, of} from 'rxjs'; import {BehaviorSubject, Observable, of} from 'rxjs';
import {getTitleKeyForRefNumber} from '@shared/functions/categories/get-title-key-for-ref-number.function'; 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 {Router} from '@angular/router';
import {ChangePentest} from '@shared/stores/project-state/project-state.actions'; import {ChangePentest} from '@shared/stores/project-state/project-state.actions';
@ -25,6 +24,7 @@ export class ObjectiveTableComponent implements OnInit {
dataSource: NbTreeGridDataSource<ObjectiveEntry>; dataSource: NbTreeGridDataSource<ObjectiveEntry>;
private data: ObjectiveEntry[] = []; private data: ObjectiveEntry[] = [];
private pentests$: BehaviorSubject<Pentest[]> = new BehaviorSubject<Pentest[]>([]);
getters: NbGetters<ObjectiveEntry, ObjectiveEntry> = { getters: NbGetters<ObjectiveEntry, ObjectiveEntry> = {
dataGetter: (node: ObjectiveEntry) => node, dataGetter: (node: ObjectiveEntry) => node,
@ -53,6 +53,8 @@ export class ObjectiveTableComponent implements OnInit {
untilDestroyed(this) untilDestroyed(this)
).subscribe({ ).subscribe({
next: (pentests: Pentest[]) => { next: (pentests: Pentest[]) => {
// ToDo: Change assignement here
this.pentests$.next(pentests);
this.data = transformPentestsToObjectiveEntries(pentests); this.data = transformPentestsToObjectiveEntries(pentests);
this.dataSource.setData(this.data, this.getters); this.dataSource.setData(this.data, this.getters);
this.loading$.next(false); 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 /* ToDo: Include again after fixing pentest route
this.router.navigate([Route.PENTEST]) this.router.navigate([Route.PENTEST])
.then( .then(
@ -74,7 +76,8 @@ export class ObjectiveTableComponent implements OnInit {
}) })
).finally(); ).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 // HTML only

View File

@ -40,8 +40,8 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
refNumber: 'OTF-001', refNumber: 'OTF-001',
childEntries: [], childEntries: [],
status: PentestStatus.NOT_STARTED, status: PentestStatus.NOT_STARTED,
findingsIds: [], findingIds: [],
commentsIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'] commentIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112']
}, },
}; };

View File

@ -4,7 +4,7 @@ import {Pentest} from '@shared/models/pentest.model';
import * as FA from '@fortawesome/free-solid-svg-icons'; import * as FA from '@fortawesome/free-solid-svg-icons';
import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme'; import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme';
import {PentestService} from '@shared/services/pentest.service'; import {PentestService} from '@shared/services/pentest.service';
import {NotificationService, PopupType} from '@shared/services/notification.service'; import {NotificationService} from '@shared/services/notification.service';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {filter, tap} from 'rxjs/operators'; import {filter, tap} from 'rxjs/operators';
import {Comment, CommentEntry, transformCommentsToObjectiveEntries} from '@shared/models/comment.model'; import {Comment, CommentEntry, transformCommentsToObjectiveEntries} from '@shared/models/comment.model';
@ -63,8 +63,9 @@ export class PentestCommentsComponent implements OnInit {
this.loading$.next(false); this.loading$.next(false);
}, },
error: err => { error: err => {
console.log(err); console.error(err);
this.notificationService.showPopup('comment.popup.not.found', PopupType.FAILURE); // ToDo: Implement again after proper lazy loading and routing
// this.notificationService.showPopup('comment.popup.not.found', PopupType.FAILURE);
this.loading$.next(false); this.loading$.next(false);
} }
}); });

View File

@ -27,14 +27,27 @@
</nb-option> </nb-option>
</nb-select> </nb-select>
</div> </div>
<button nbButton <div *ngIf="!pentest$.getValue().id; else updatePentest">
class="save-pentest-button" <button nbButton
status="primary" class="save-pentest-button"
[disabled]="!pentestStatusChanged()" status="primary"
title="{{ 'global.action.save' | translate }}" [disabled]="!pentestStatusChanged()"
(click)="onClickSavePentest()"> title="{{ 'global.action.save' | translate }}"
<span class="exit-element-text"> {{ 'global.action.save' | translate }} </span> (click)="onClickSavePentest()">
</button> <span class="exit-element-text"> {{ 'global.action.save' | translate }} </span>
</button>
</div>
<ng-template #updatePentest>
<button nbButton
class="save-pentest-button"
status="primary"
[disabled]="!pentestStatusChanged()"
title="{{ 'global.action.update' | translate }}"
(click)="onClickUpdatePentest()">
<span class="exit-element-text"> {{ 'global.action.update' | translate }} </span>
</button>
</ng-template>
</div> </div>
</div> </div>

View File

@ -11,6 +11,8 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HttpClientTestingModule} from '@angular/common/http/testing'; import {HttpClientTestingModule} from '@angular/common/http/testing';
import {Category} from '@shared/models/category.model'; import {Category} from '@shared/models/category.model';
import {PentestStatus} from '@shared/models/pentest-status.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 = { const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
selectedProject: { selectedProject: {
@ -33,8 +35,8 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
refNumber: 'OTF-001', refNumber: 'OTF-001',
childEntries: [], childEntries: [],
status: PentestStatus.NOT_STARTED, status: PentestStatus.NOT_STARTED,
findingsIds: [], findingIds: [],
commentsIds: [] commentIds: []
}, },
}; };
@ -60,6 +62,9 @@ describe('PentestContentComponent', () => {
}), }),
RouterTestingModule.withRoutes([]), RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([ProjectState]) NgxsModule.forRoot([ProjectState])
],
providers: [
{provide: NotificationService, useValue: new NotificationServiceMock()}
] ]
}) })
.compileComponents(); .compileComponents();

View File

@ -1,14 +1,17 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import * as FA from '@fortawesome/free-solid-svg-icons'; import * as FA from '@fortawesome/free-solid-svg-icons';
import {BehaviorSubject} from 'rxjs'; import {BehaviorSubject, Observable} from 'rxjs';
import {Store} from '@ngxs/store'; import {Select, Store} from '@ngxs/store';
import {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 {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 {PentestStatus} from '@shared/models/pentest-status.model';
import {StatusText} from '@shared/widgets/status-tag/status-tag.component'; import {StatusText} from '@shared/widgets/status-tag/status-tag.component';
import {PentestService} from '@shared/services/pentest.service'; import {PentestService} from '@shared/services/pentest.service';
import {NotificationService, PopupType} from '@shared/services/notification.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() @UntilDestroy()
@Component({ @Component({
@ -20,6 +23,10 @@ export class PentestContentComponent implements OnInit {
// HTML only // HTML only
readonly fa = FA; readonly fa = FA;
@Select(ProjectState.project)
selectedProject$: Observable<Project>;
selectedProjectId: string;
pentest$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null); pentest$: BehaviorSubject<Pentest> = new BehaviorSubject<Pentest>(null);
pentestChanged$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); pentestChanged$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
currentNumberOfFindings$: BehaviorSubject<number> = new BehaviorSubject<number>(0); currentNumberOfFindings$: BehaviorSubject<number> = new BehaviorSubject<number>(0);
@ -45,16 +52,29 @@ export class PentestContentComponent implements OnInit {
} }
ngOnInit(): void { 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( this.store.selectOnce(ProjectState.pentest).pipe(
untilDestroyed(this) untilDestroyed(this)
).subscribe({ ).subscribe({
next: (selectedPentest: Pentest) => { next: (selectedPentest: Pentest) => {
console.warn(selectedPentest);
this.pentest$.next(selectedPentest); this.pentest$.next(selectedPentest);
this.currentStatus = selectedPentest.status; this.currentStatus = selectedPentest.status;
this.initialPentestStatus = 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); this.currentNumberOfFindings$.next(findings);
const comments = selectedPentest.commentsIds ? selectedPentest.commentsIds.length : 0; const comments = selectedPentest.commentIds ? selectedPentest.commentIds.length : 0;
this.currentNumberOfComments$.next(comments); this.currentNumberOfComments$.next(comments);
}, },
error: err => { error: err => {
@ -65,10 +85,11 @@ export class PentestContentComponent implements OnInit {
onClickSavePentest(): void { onClickSavePentest(): void {
this.pentest$.next({...this.pentest$.getValue(), status: this.currentStatus}); this.pentest$.next({...this.pentest$.getValue(), status: this.currentStatus});
console.warn('Updated Pentest: ', this.pentest$.getValue()); this.pentestService.savePentest(this.selectedProjectId, transformPentestToRequestBody(this.pentest$.getValue()))
this.pentestService.savePentest(this.pentest$.getValue())
.subscribe({ .subscribe({
next: (pentest: Pentest) => { next: (pentest: Pentest) => {
this.pentest$.next(pentest);
this.initialPentestStatus = pentest.status;
this.notificationService.showPopup('pentest.popup.save.success', PopupType.SUCCESS); this.notificationService.showPopup('pentest.popup.save.success', PopupType.SUCCESS);
}, },
error: err => { 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 * @return true if initial pentest Status is different from current pentest status
*/ */

View File

@ -42,8 +42,8 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
refNumber: 'OTF-001', refNumber: 'OTF-001',
childEntries: [], childEntries: [],
status: PentestStatus.NOT_STARTED, status: PentestStatus.NOT_STARTED,
findingsIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'], findingIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'],
commentsIds: [] commentIds: []
}, },
}; };

View File

@ -65,8 +65,9 @@ export class PentestFindingsComponent implements OnInit {
this.loading$.next(false); this.loading$.next(false);
}, },
error: err => { error: err => {
console.log(err); console.error(err);
this.notificationService.showPopup('findings.popup.not.found', PopupType.FAILURE); // ToDo: Implement again after proper lazy loading and routing
// this.notificationService.showPopup('findings.popup.not.found', PopupType.FAILURE);
this.loading$.next(false); this.loading$.next(false);
} }
}); });
@ -94,8 +95,8 @@ export class PentestFindingsComponent implements OnInit {
this.loadFindingsData(); this.loadFindingsData();
this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS); this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS);
}, },
error: error => { error: err => {
console.error(error); console.error(err);
this.notificationService.showPopup('finding.popup.save.failed', PopupType.FAILURE); this.notificationService.showPopup('finding.popup.save.failed', PopupType.FAILURE);
} }
}); });

View File

@ -51,8 +51,8 @@ describe('PentestInfoComponent', () => {
refNumber: 'OTF-001', refNumber: 'OTF-001',
childEntries: [], childEntries: [],
status: PentestStatus.NOT_STARTED, status: PentestStatus.NOT_STARTED,
findingsIds: [], findingIds: [],
commentsIds: [] commentIds: []
}); });
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -33,8 +33,8 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
refNumber: 'OTF-001', refNumber: 'OTF-001',
childEntries: [], childEntries: [],
status: PentestStatus.NOT_STARTED, status: PentestStatus.NOT_STARTED,
findingsIds: [], findingIds: [],
commentsIds: [] commentIds: []
}, },
}; };

View File

@ -36,13 +36,11 @@
<nb-card-footer> <nb-card-footer>
<div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start end"> <div fxLayout="row" fxLayoutGap="1rem" fxLayoutAlign="start end">
<div class="project-progress"> <div class="project-progress">
<nb-progress-bar *ngIf="project.testingProgress > 0; else altProgressBar" <nb-progress-bar *ngIf="project.testingProgress > 0; else altProgressBar"
status="warning" status="warning"
[value]="project.testingProgress" [value]="project.testingProgress"
[displayValue]="true"> [displayValue]="true">
</nb-progress-bar> </nb-progress-bar>
<ng-template #altProgressBar> <ng-template #altProgressBar>
{{'popup.info' | translate}} {{'global.no.progress' | translate}} {{'popup.info' | translate}} {{'global.no.progress' | translate}}
</ng-template> </ng-template>

View File

@ -1,28 +1,31 @@
import {v4 as UUID} from 'uuid';
import {PentestStatus} from '@shared/models/pentest-status.model'; import {PentestStatus} from '@shared/models/pentest-status.model';
import {Category} from '@shared/models/category.model'; import {Category} from '@shared/models/category.model';
import {v4 as UUID} from 'uuid';
export class Pentest { export class Pentest {
id?: string; id?: string;
projectId?: string;
category: Category; category: Category;
refNumber: string; refNumber: string;
childEntries?: Pentest[]; childEntries?: Pentest[];
status: PentestStatus; status: PentestStatus;
findingsIds?: Array<string>; findingIds?: Array<string>;
commentsIds?: Array<string>; commentIds?: Array<string>;
constructor(category: Category, constructor(category: Category,
refNumber: string, refNumber: string,
status: PentestStatus, status: PentestStatus,
id?: string, id?: string,
projectId?: string,
findingsIds?: Array<string>, findingsIds?: Array<string>,
commentsIds?: Array<string>) { commentsIds?: Array<string>) {
this.id = id ? id : UUID(); this.id = id ? id : UUID();
this.projectId = projectId ? projectId : '';
this.category = category; this.category = category;
this.refNumber = refNumber; this.refNumber = refNumber;
this.status = status; this.status = status;
this.findingsIds = findingsIds ? findingsIds : []; this.findingIds = findingsIds ? findingsIds : [];
this.commentsIds = commentsIds ? commentsIds : []; this.commentIds = commentsIds ? commentsIds : [];
} }
} }
@ -35,13 +38,31 @@ export interface ObjectiveEntry {
expanded?: boolean; 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[] { export function transformPentestsToObjectiveEntries(pentests: Pentest[]): ObjectiveEntry[] {
const objectiveEntries: ObjectiveEntry[] = []; const objectiveEntries: ObjectiveEntry[] = [];
pentests.forEach((value: Pentest) => { pentests.forEach((value: Pentest) => {
objectiveEntries.push({ objectiveEntries.push({
refNumber: value.refNumber, refNumber: value.refNumber,
status: value.status, status: value.status,
findings: value.findingsIds ? value.findingsIds.length : 0, findings: value.findingIds ? value.findingIds.length : 0,
kind: value.childEntries ? 'dir' : 'cell', kind: value.childEntries ? 'dir' : 'cell',
childEntries: value.childEntries ? value.childEntries : null, childEntries: value.childEntries ? value.childEntries : null,
expanded: !!value.childEntries expanded: !!value.childEntries

View File

@ -52,8 +52,8 @@ const DESIRED_PROJECT_STATE_SESSION: ProjectStateModel = {
refNumber: 'OTF-001', refNumber: 'OTF-001',
childEntries: [], childEntries: [],
status: PentestStatus.NOT_STARTED, status: PentestStatus.NOT_STARTED,
findingsIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'], findingIds: ['56c47c56-3bcd-45f1-a05b-c197dbd33112'],
commentsIds: [] commentIds: []
}, },
}; };

View File

@ -11,6 +11,7 @@ import {getTempPentestsForCategory} from '@shared/functions/categories/get-temp-
import {Finding, FindingDialogBody} from '@shared/models/finding.model'; import {Finding, FindingDialogBody} from '@shared/models/finding.model';
import {Severity} from '@shared/models/severity.enum'; import {Severity} from '@shared/models/severity.enum';
import {Comment} from '@shared/models/comment.model'; import {Comment} from '@shared/models/comment.model';
import {v4 as UUID} from 'uuid';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -29,17 +30,26 @@ export class PentestService {
* @param category the categories of which the pentests should be requested * @param category the categories of which the pentests should be requested
*/ */
public loadPentests(category: Category): Observable<Pentest[]> { public loadPentests(category: Category): Observable<Pentest[]> {
return this.store.selectOnce(ProjectState.project).pipe( return this.store.select(ProjectState.project).pipe(
switchMap(project => this.getPentestByProjectIdAndCategory(project.id, category)), switchMap(project => this.getPentestByProjectIdAndCategory(project.id, category)),
catchError(_ => of(null)), catchError(_ => of(null)),
map(response => { map((response: Pentest[]) => {
let pentests = response; // ToDo: Improve performance by only loading templates when not all pentests of category got returned
if (!pentests) { // Load template pentest
pentests = getTempPentestsForCategory(category); const templatePentests = getTempPentestsForCategory(category);
// tslint:disable-next-line:no-console // The pentests that get returned to the component
console.info('Initial pentest data loaded.'); 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 * Save Pentest
* @param pentest the information of the Pentest * @param pentest the information of the Pentest
*/ */
public savePentest(pentest: Pentest): Observable<Pentest> { public savePentest(projectId: string, pentest: Pentest): Observable<Pentest> {
return this.http.post<Pentest>(`${this.apiBaseURL}/${pentest.id}`, pentest); return this.http.post<Pentest>(`${this.apiBaseURL}/${projectId}`, pentest);
}
/**
* Update Pentest
* @param pentest the information of the Pentest
*/
public updatePentest(pentest: Pentest): Observable<Pentest> {
return this.http.patch<Pentest>(`${this.apiBaseURL}/${pentest.id}`, pentest);
} }
/** /**

View File

@ -77,7 +77,7 @@ export class ProjectState {
changePentest(ctx: StateContext<ProjectStateModel>, {pentest}: ChangePentest): void { changePentest(ctx: StateContext<ProjectStateModel>, {pentest}: ChangePentest): void {
const state = ctx.getState(); const state = ctx.getState();
ctx.patchState({ ctx.patchState({
selectedPentest: pentest selectedPentest: {...pentest, projectId: state.selectedProject.id}
}); });
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"info": { "info": {
"_postman_id": "58adc500-c0c6-47f3-b268-5fcc16e0944d", "_postman_id": "6f244dd9-5264-497a-9ea4-1ae73e172624",
"name": "security-c4po-api", "name": "security-c4po-api",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "5225213" "_exporter_id": "5225213"
@ -266,7 +266,7 @@
"bearer": [ "bearer": [
{ {
"key": "token", "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" "type": "string"
}, },
{ {
@ -300,6 +300,78 @@
} }
}, },
"response": [] "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": []
} }
] ]
}, },

View File

@ -34,4 +34,5 @@ enum class Errorcode(val code: Int) {
PentestFetchingFailed(6005), PentestFetchingFailed(6005),
ProjectInsertionFailed(6006), ProjectInsertionFailed(6006),
PentestInsertionFailed(6007), PentestInsertionFailed(6007),
ProjectPentestInsertionFailed(6008),
} }

View File

@ -9,22 +9,68 @@ data class Pentest(
val id: String = UUID.randomUUID().toString(), val id: String = UUID.randomUUID().toString(),
val projectId: String, val projectId: String,
val category: PentestCategory, val category: PentestCategory,
val title: String,
val refNumber: String, val refNumber: String,
val status: PentestStatus, val status: PentestStatus,
val findingIds: List<String> = emptyList(), val findingIds: List<String> = emptyList(),
val commentIds: List<String> = emptyList() val commentIds: List<String> = 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 { fun Pentest.toPentestResponseBody(): ResponseBody {
return mapOf( return mapOf(
"id" to id, "id" to id,
"projectId" to projectId, "projectId" to projectId,
"category" to category, "category" to category,
"title" to title,
"refNumber" to refNumber, "refNumber" to refNumber,
"status" to status, "status" to status,
"findingIds" to findingIds, "findingIds" to findingIds,
"commentIds" to commentIds "commentIds" to commentIds
) )
} }
data class PentestRequestBody(
val projectId: String,
val refNumber: String,
val category: String,
val status: String,
val findingIds: List<String>,
val commentIds: List<String>
)
/**
* 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
)
}

View File

@ -15,7 +15,7 @@ import reactor.core.publisher.Mono
origins = [], origins = [],
allowCredentials = "false", allowCredentials = "false",
allowedHeaders = ["*"], allowedHeaders = ["*"],
methods = [RequestMethod.GET] methods = [RequestMethod.GET, RequestMethod.DELETE, RequestMethod.POST, RequestMethod.PATCH]
) )
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION) @SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION)
@ -28,7 +28,7 @@ class PentestController(private val pentestService: PentestService) {
@RequestParam("projectId") projectId: String, @RequestParam("projectId") projectId: String,
@RequestParam("category") category: String @RequestParam("category") category: String
): Mono<ResponseEntity<List<ResponseBody>>> { ): Mono<ResponseEntity<List<ResponseBody>>> {
return pentestService.getPentests(projectId, PentestCategory.valueOf(category)).map { pentestList -> return pentestService.getPentestsForCategory(projectId, PentestCategory.valueOf(category)).map { pentestList ->
pentestList.map { pentestList.map {
it.toPentestResponseBody() it.toPentestResponseBody()
} }
@ -37,4 +37,36 @@ class PentestController(private val pentestService: PentestService) {
else ResponseEntity.ok(it) else ResponseEntity.ok(it)
} }
} }
/* Todo: Add API
@GetMapping
fun getPentestById(
@RequestParam("pentestId") pentestId: String
): Mono<ResponseEntity<List<ResponseBody>>> {
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<ResponseEntity<ResponseBody>> {
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<ResponseEntity<ResponseBody>> {
return this.pentestService.updatePentest(pentestId, body).map {
ResponseEntity.accepted().body(it.toPentestResponseBody())
}
}
} }

View File

@ -16,7 +16,6 @@ fun PentestEntity.toPentest(): Pentest {
this.data.id, this.data.id,
this.data.projectId, this.data.projectId,
this.data.category, this.data.category,
this.data.title,
this.data.refNumber, this.data.refNumber,
this.data.status, this.data.status,
this.data.findingIds, this.data.findingIds,

View File

@ -4,6 +4,7 @@ import org.springframework.data.mongodb.repository.Query
import org.springframework.data.mongodb.repository.ReactiveMongoRepository import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@Repository @Repository
interface PentestRepository : ReactiveMongoRepository<PentestEntity, String> { interface PentestRepository : ReactiveMongoRepository<PentestEntity, String> {
@ -11,4 +12,6 @@ interface PentestRepository : ReactiveMongoRepository<PentestEntity, String> {
@Query("{'data.projectId': ?0, 'data.category': ?1}") @Query("{'data.projectId': ?0, 'data.category': ?1}")
fun findPentestByProjectIdAndCategory(projectId: String, category: PentestCategory): Flux<PentestEntity> fun findPentestByProjectIdAndCategory(projectId: String, category: PentestCategory): Flux<PentestEntity>
@Query("{'data._id' : ?0}")
fun findPentestById(id: String): Mono<PentestEntity>
} }

View File

@ -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.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.configuration.MESSAGE_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.extensions.getLoggerFor
import com.securityc4po.api.project.*
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.switchIfEmpty
import java.time.Instant
@Service @Service
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION) @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<PentestService>() var logger = getLoggerFor<PentestService>()
@ -18,9 +24,97 @@ class PentestService(private val pentestRepository: PentestRepository) {
* *
* @return list of [Pentest] * @return list of [Pentest]
*/ */
fun getPentests(projectId: String, category: PentestCategory): Mono<List<Pentest>> { fun getPentestsForCategory(projectId: String, category: PentestCategory): Mono<List<Pentest>> {
return pentestRepository.findPentestByProjectIdAndCategory(projectId, category).collectList().map { return pentestRepository.findPentestByProjectIdAndCategory(projectId, category).collectList().map {
it.map { pentestEntity -> pentestEntity.toPentest() } 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<Pentest> {
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<Pentest> {
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
)
)
}
}
}
} }

View File

@ -2,6 +2,7 @@ package com.securityc4po.api.pentest
enum class PentestStatus { enum class PentestStatus {
NOT_STARTED, NOT_STARTED,
DISABLED,
OPEN, OPEN,
IN_PROGRESS, IN_PROGRESS,
COMPLETED COMPLETED

View File

@ -3,10 +3,9 @@ package com.securityc4po.api.project
import com.fasterxml.jackson.annotation.JsonFormat import com.fasterxml.jackson.annotation.JsonFormat
import com.securityc4po.api.ResponseBody import com.securityc4po.api.ResponseBody
import com.securityc4po.api.pentest.PentestStatus import com.securityc4po.api.pentest.PentestStatus
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.mongodb.core.index.Indexed import org.springframework.data.mongodb.core.index.Indexed
import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import java.text.DecimalFormat
import java.time.Instant import java.time.Instant
import java.util.UUID import java.util.UUID
@ -18,11 +17,11 @@ data class Project(
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssZ") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssZ")
val createdAt: String = Instant.now().toString(), val createdAt: String = Instant.now().toString(),
val tester: String? = null, val tester: String? = null,
val projectPentests: List<ProjectPentest> = emptyList(), var projectPentests: List<ProjectPentest> = emptyList(),
val createdBy: String val createdBy: String
) )
fun buildProject(body: ProjectRequestBody, projectEntity: ProjectEntity): Project{ fun buildProject(body: ProjectRequestBody, projectEntity: ProjectEntity): Project {
return Project( return Project(
id = projectEntity.data.id, id = projectEntity.data.id,
client = body.client, client = body.client,
@ -36,14 +35,14 @@ fun buildProject(body: ProjectRequestBody, projectEntity: ProjectEntity): Projec
fun Project.toProjectResponseBody(): ResponseBody { fun Project.toProjectResponseBody(): ResponseBody {
return mapOf( return mapOf(
"id" to id, "id" to id,
"client" to client, "client" to client,
"title" to title, "title" to title,
"createdAt" to createdAt, "createdAt" to createdAt,
"tester" to tester, "tester" to tester,
/* ToDo: Calculate percentage in BE type: float */ /* ToDo: Calculate percentage in BE type: float */
"testingProgress" to calculateProgress(), "testingProgress" to calculateProgress(),
"createdBy" to createdBy "createdBy" to createdBy
) )
} }
@ -54,32 +53,32 @@ fun Project.toProjectDeleteResponseBody(): ResponseBody {
} }
fun Project.calculateProgress(): BigDecimal {
fun Project.calculateProgress(): Float {
// Total number of pentests listet in the OWASP testing guide // 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 // https://owasp.org/www-project-web-security-testing-guide/assets/archive/OWASP_Testing_Guide_v4.pdf
// @Value("\${owasp.web.pentests}") // @Value("\${owasp.web.pentests}")
// lateinit var TOTALPENTESTS: Int // lateinit var TOTALPENTESTS: Int
val TOTALPENTESTS = 95 val TOTALPENTESTS = 95.0
return if (projectPentests.isEmpty()) return if (projectPentests.isEmpty())
0F BigDecimal.ZERO
else { else {
var completedPentests = 0 var completedPentests = 0.0
projectPentests.forEach { projectPentest -> projectPentests.forEach { projectPentest ->
println(projectPentest.toString())
if (projectPentest.status == PentestStatus.COMPLETED) { if (projectPentest.status == PentestStatus.COMPLETED) {
completedPentests++ completedPentests += 1.0
} else if (projectPentest.status != PentestStatus.NOT_STARTED) {
completedPentests += 0.5
} }
} }
val df = DecimalFormat("#.##") val progress = (completedPentests * 100) / TOTALPENTESTS
df.roundingMode = RoundingMode.DOWN BigDecimal(progress).setScale(2, RoundingMode.HALF_UP)
val progress = completedPentests / TOTALPENTESTS
df.format(progress).toFloat()
} }
} }
data class ProjectOverview( data class ProjectOverview(
val projects: List<Project> val projects: List<Project>
) )
data class ProjectRequestBody( data class ProjectRequestBody(
@ -111,6 +110,5 @@ fun ProjectRequestBody.toProject(): Project {
tester = this.tester, tester = this.tester,
// ToDo: Should be changed to SUB from Token after adding AUTH Header // ToDo: Should be changed to SUB from Token after adding AUTH Header
createdBy = UUID.randomUUID().toString() createdBy = UUID.randomUUID().toString()
)
)
} }

View File

@ -4,5 +4,5 @@ import com.securityc4po.api.pentest.PentestStatus
data class ProjectPentest( data class ProjectPentest(
val pentestId: String, val pentestId: String,
val status: PentestStatus var status: PentestStatus
) )

View File

@ -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 * @throws [TransactionInterruptedException] if the [Project] Pentest could not be updated
* @return updated list of [ProjectPentest]s * @return updated [Project]
*/ */
fun updateProjectTestingProgress( fun updateProjectTestingProgress(
projectId: String, projectId: String,
projectPentests: ProjectPentest projectPentest: ProjectPentest
)/*: Mono<List<ProjectPentest>>*/ { ): Mono<Project> {
// ToDo: update Project Entity with progress 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
)
)
}
}
} }
} }

View File

@ -75,8 +75,6 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
.description("The id of the project of the requested pentest"), .description("The id of the project of the requested pentest"),
PayloadDocumentation.fieldWithPath("[].category").type(JsonFieldType.STRING) PayloadDocumentation.fieldWithPath("[].category").type(JsonFieldType.STRING)
.description("The category of the requested pentest"), .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) PayloadDocumentation.fieldWithPath("[].refNumber").type(JsonFieldType.STRING)
.description("The reference number of the requested pentest according to the current OWASP Testing Guide"), .description("The reference number of the requested pentest according to the current OWASP Testing Guide"),
PayloadDocumentation.fieldWithPath("[].status").type(JsonFieldType.STRING) PayloadDocumentation.fieldWithPath("[].status").type(JsonFieldType.STRING)
@ -94,7 +92,6 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
id = "9c8af320-f608-11ec-b939-0242ac120002", id = "9c8af320-f608-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING, category = PentestCategory.INFORMATION_GATHERING,
title = "Search engine discovery/reconnaissance",
refNumber = "OTG-INFO-001", refNumber = "OTG-INFO-001",
status = PentestStatus.NOT_STARTED, status = PentestStatus.NOT_STARTED,
findingIds = emptyList(), findingIds = emptyList(),
@ -104,7 +101,6 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
id = "43fbc63c-f624-11ec-b939-0242ac120002", id = "43fbc63c-f624-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING, category = PentestCategory.INFORMATION_GATHERING,
title = "Fingerprint Web Server",
refNumber = "OTG-INFO-002", refNumber = "OTG-INFO-002",
status = PentestStatus.IN_PROGRESS, status = PentestStatus.IN_PROGRESS,
findingIds = emptyList(), findingIds = emptyList(),
@ -123,7 +119,6 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
id = "9c8af320-f608-11ec-b939-0242ac120002", id = "9c8af320-f608-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING, category = PentestCategory.INFORMATION_GATHERING,
title = "Search engine discovery/reconnaissance",
refNumber = "OTG-INFO-001", refNumber = "OTG-INFO-001",
status = PentestStatus.NOT_STARTED, status = PentestStatus.NOT_STARTED,
findingIds = emptyList(), findingIds = emptyList(),
@ -133,7 +128,6 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
id = "43fbc63c-f624-11ec-b939-0242ac120002", id = "43fbc63c-f624-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING, category = PentestCategory.INFORMATION_GATHERING,
title = "Fingerprint Web Server",
refNumber = "OTG-INFO-002", refNumber = "OTG-INFO-002",
status = PentestStatus.IN_PROGRESS, status = PentestStatus.IN_PROGRESS,
findingIds = emptyList(), findingIds = emptyList(),
@ -143,7 +137,6 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
id = "74eae112-f62c-11ec-b939-0242ac120002", id = "74eae112-f62c-11ec-b939-0242ac120002",
projectId = "6fad3474-fc29-49f9-bd37-e039e9e60c18", projectId = "6fad3474-fc29-49f9-bd37-e039e9e60c18",
category = PentestCategory.AUTHENTICATION_TESTING, category = PentestCategory.AUTHENTICATION_TESTING,
title = "Testing for Credentials Transported over an Encrypted Channel",
refNumber = "OTG-AUTHN-001", refNumber = "OTG-AUTHN-001",
status = PentestStatus.COMPLETED, status = PentestStatus.COMPLETED,
findingIds = emptyList(), findingIds = emptyList(),

View File

@ -69,7 +69,6 @@ class PentestControllerIntTest : BaseIntTest() {
id = "9c8af320-f608-11ec-b939-0242ac120002", id = "9c8af320-f608-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING, category = PentestCategory.INFORMATION_GATHERING,
title = "Search engine discovery/reconnaissance",
refNumber = "OTG-INFO-001", refNumber = "OTG-INFO-001",
status = PentestStatus.NOT_STARTED, status = PentestStatus.NOT_STARTED,
findingIds = emptyList(), findingIds = emptyList(),
@ -79,7 +78,6 @@ class PentestControllerIntTest : BaseIntTest() {
id = "43fbc63c-f624-11ec-b939-0242ac120002", id = "43fbc63c-f624-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING, category = PentestCategory.INFORMATION_GATHERING,
title = "Fingerprint Web Server",
refNumber = "OTG-INFO-002", refNumber = "OTG-INFO-002",
status = PentestStatus.IN_PROGRESS, status = PentestStatus.IN_PROGRESS,
findingIds = emptyList(), findingIds = emptyList(),
@ -98,7 +96,6 @@ class PentestControllerIntTest : BaseIntTest() {
id = "9c8af320-f608-11ec-b939-0242ac120002", id = "9c8af320-f608-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING, category = PentestCategory.INFORMATION_GATHERING,
title = "Search engine discovery/reconnaissance",
refNumber = "OTG-INFO-001", refNumber = "OTG-INFO-001",
status = PentestStatus.NOT_STARTED, status = PentestStatus.NOT_STARTED,
findingIds = emptyList(), findingIds = emptyList(),
@ -108,7 +105,6 @@ class PentestControllerIntTest : BaseIntTest() {
id = "43fbc63c-f624-11ec-b939-0242ac120002", id = "43fbc63c-f624-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002", projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING, category = PentestCategory.INFORMATION_GATHERING,
title = "Fingerprint Web Server",
refNumber = "OTG-INFO-002", refNumber = "OTG-INFO-002",
status = PentestStatus.IN_PROGRESS, status = PentestStatus.IN_PROGRESS,
findingIds = emptyList(), findingIds = emptyList(),
@ -118,7 +114,6 @@ class PentestControllerIntTest : BaseIntTest() {
id = "74eae112-f62c-11ec-b939-0242ac120002", id = "74eae112-f62c-11ec-b939-0242ac120002",
projectId = "6fad3474-fc29-49f9-bd37-e039e9e60c18", projectId = "6fad3474-fc29-49f9-bd37-e039e9e60c18",
category = PentestCategory.AUTHENTICATION_TESTING, category = PentestCategory.AUTHENTICATION_TESTING,
title = "Testing for Credentials Transported over an Encrypted Channel",
refNumber = "OTG-AUTHN-001", refNumber = "OTG-AUTHN-001",
status = PentestStatus.COMPLETED, status = PentestStatus.COMPLETED,
findingIds = emptyList(), findingIds = emptyList(),

View File

@ -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"
}]

View File

@ -4,16 +4,21 @@
}, },
"lastModified": { "lastModified": {
"$date": { "$date": {
"$numberLong": "1660142860140" "$numberLong": "1668176064717"
} }
}, },
"data": { "data": {
"_id": "5a4f126c-9471-43b8-80b9-6eb02b7c35d0", "_id": "5a4f126c-9471-43b8-80b9-6eb02b7c35d0",
"client": "E Corp", "client": "Dio Stonemask Inc.",
"title": "Some Mock API (v1.0) Scanning", "title": "log4jj bizarre adventure",
"createdAt": "2022-08-10T14:47:40.140406Z", "createdAt": "2022-08-10T14:47:40.140406Z",
"tester": "Novatester", "tester": "Jojo",
"projectPentests": [], "projectPentests": [
{
"pentestId": "11601f51-bc17-47fd-847d-0c53df5405b5",
"status": "IN_PROGRESS"
}
],
"createdBy": "3c4ae87f-0d56-4634-a824-b4883c403c8a" "createdBy": "3c4ae87f-0d56-4634-a824-b4883c403c8a"
}, },
"_class": "com.securityc4po.api.project.ProjectEntity" "_class": "com.securityc4po.api.project.ProjectEntity"
@ -38,21 +43,21 @@
"_class": "com.securityc4po.api.project.ProjectEntity" "_class": "com.securityc4po.api.project.ProjectEntity"
},{ },{
"_id": { "_id": {
"$oid": "62f3c5427acde34f740ba739" "$oid": "62ff7534ac2b4d14d86215c4"
}, },
"lastModified": { "lastModified": {
"$date": { "$date": {
"$numberLong": "1660142914204" "$numberLong": "1660908852340"
} }
}, },
"data": { "data": {
"_id": "1120bfa1-0d2b-4e42-a209-0289a1256266", "_id": "195809ed-9722-4ad5-a84b-0099a9a01652",
"client": "Novatec", "client": "Novatec",
"title": "Openspace log4J", "title": "log4j pentest",
"createdAt": "2022-08-10T14:48:34.204234Z", "createdAt": "2022-08-19T11:34:12.339990Z",
"tester": "mhg", "tester": "Stipe",
"projectPentests": [], "projectPentests": [],
"createdBy": "5a4a8032-0726-4851-a105-9f079c3989b9" "createdBy": "7fe49c8d-fee3-47e0-9224-94e0ac7436c6"
}, },
"_class": "com.securityc4po.api.project.ProjectEntity" "_class": "com.securityc4po.api.project.ProjectEntity"
}] }]