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 5bbbbdb..afdc3f6 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 @@ -10,7 +10,7 @@ import { FindingDialogBody, FindingEntry, transformFindingsToObjectiveEntries, - transformFindingToRequestBody + transformFindingToRequestBody, } from '@shared/models/finding.model'; import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme'; import * as FA from '@fortawesome/free-solid-svg-icons'; @@ -111,7 +111,7 @@ export class PentestFindingsComponent implements OnInit { } ).pipe( filter(value => !!value), - /* tap((value) => console.warn('FindingDialogBody: ', value))*/ + /*tap((value) => console.warn('FindingDialogBody: ', value)),*/ mergeMap((value: FindingDialogBody) => this.pentestService.saveFinding( this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', @@ -120,8 +120,8 @@ export class PentestFindingsComponent implements OnInit { ), untilDestroyed(this) ).subscribe({ - next: (finding) => { - this.store.dispatch(new UpdatePentestFindings(finding.id)); + next: (newFinding: Finding) => { + this.store.dispatch(new UpdatePentestFindings(newFinding.id)); this.loadFindingsData(); this.notificationService.showPopup('finding.popup.save.success', PopupType.SUCCESS); }, @@ -132,12 +132,55 @@ export class PentestFindingsComponent implements OnInit { }); } - onClickEditFinding(finding): void { - console.info('Coming soon..'); + onClickEditFinding(findingEntry): void { + this.pentestService.getFindingById(findingEntry.data.findingId).pipe( + filter(isNotNullOrUndefined), + untilDestroyed(this) + ).subscribe({ + next: (existingFinding: Finding) => { + if (existingFinding) { + this.findingDialogService.openFindingDialog( + FindingDialogComponent, + existingFinding, + { + closeOnEsc: false, + hasScroll: false, + autoFocus: false, + closeOnBackdropClick: false + } + ).pipe( + filter(value => !!value), + /*tap((value) => console.warn('FindingDialogBody: ', value)),*/ + mergeMap((value: FindingDialogBody) => + this.pentestService.updateFinding( + findingEntry.data.findingId, + transformFindingToRequestBody(value) + ) + ), + untilDestroyed(this) + ).subscribe({ + next: (updatedFinding: Finding) => { + this.store.dispatch(new UpdatePentestFindings(updatedFinding.id)); + this.loadFindingsData(); + this.notificationService.showPopup('finding.popup.update.success', PopupType.SUCCESS); + }, + error: err => { + console.error(err); + this.notificationService.showPopup('finding.popup.update.failed', PopupType.FAILURE); + } + }); + } else { + this.notificationService.showPopup('finding.popup.not.available', PopupType.FAILURE); + } + }, + error: err => { + console.error(err); + } + }); } - onClickDeleteFinding(finding): void { - console.info('Coming soon..'); + onClickDeleteFinding(findingEntry): void { + console.info('Coming soon..', findingEntry.data.findingId); } isLoading(): Observable { diff --git a/security-c4po-angular/src/assets/i18n/de-DE.json b/security-c4po-angular/src/assets/i18n/de-DE.json index 3e4989f..06fcb76 100644 --- a/security-c4po-angular/src/assets/i18n/de-DE.json +++ b/security-c4po-angular/src/assets/i18n/de-DE.json @@ -134,7 +134,8 @@ "update.success": "Fund erfolgreich aktualisiert", "update.failed": "Fund konnte nicht aktualisiert werden", "delete.success": "Fund erfolgreich gelöscht", - "delete.failed": "Fund konnte nicht gelöscht werden" + "delete.failed": "Fund konnte nicht gelöscht werden", + "not.available": "Fund ist nicht mehr verfügbar" } }, "severities": { diff --git a/security-c4po-angular/src/assets/i18n/en-US.json b/security-c4po-angular/src/assets/i18n/en-US.json index 3e96ba9..39c0d16 100644 --- a/security-c4po-angular/src/assets/i18n/en-US.json +++ b/security-c4po-angular/src/assets/i18n/en-US.json @@ -134,7 +134,8 @@ "update.success": "Finding updated successfully", "update.failed": "Finding could not be updated", "delete.success": "Finding deleted successfully", - "delete.failed": "Finding could not be deleted" + "delete.failed": "Finding could not be deleted", + "not.available": "Finding is not available anymore" } }, "severities": { diff --git a/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.ts b/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.ts index 1dd1006..5e41a81 100644 --- a/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.ts +++ b/security-c4po-angular/src/shared/modules/finding-dialog/finding-dialog.component.ts @@ -6,6 +6,7 @@ import deepEqual from 'deep-equal'; import {UntilDestroy} from '@ngneat/until-destroy'; import {Severity} from '@shared/models/severity.enum'; import * as FA from '@fortawesome/free-solid-svg-icons'; +import {BehaviorSubject} from 'rxjs'; @UntilDestroy() @Component({ @@ -33,6 +34,7 @@ export class FindingDialogComponent implements OnInit { // ToDo: Adjust for edit finding dialog to include existing urls affectedUrls: string[] = []; + initialAffectedUrls: string[] = []; constructor( @Inject(NB_DIALOG_CONFIG) private data: GenericDialogData, @@ -44,6 +46,9 @@ export class FindingDialogComponent implements OnInit { ngOnInit(): void { this.findingFormGroup = this.generateFormCreationFieldArray(); this.dialogData = this.data; + // Resets affected Urls input fields when finding was found in dialog context + // tslint:disable-next-line:no-string-literal + this.findingFormGroup.controls['findingAffectedUrls'].reset(''); } generateFormCreationFieldArray(): FormGroup { @@ -52,6 +57,11 @@ export class FindingDialogComponent implements OnInit { ...accumulator, [currentValue?.fieldName]: currentValue?.controlsConfig }), {}); + // tslint:disable-next-line:no-string-literal + const affectedUrls = this.data.form['findingAffectedUrls'].controlsConfig[0].value; + if (affectedUrls) { + this.renderAffectedUrls(affectedUrls); + } return this.fb.group(config); } @@ -67,6 +77,11 @@ export class FindingDialogComponent implements OnInit { }); } + renderAffectedUrls(affectedUrls: string[]): void { + affectedUrls.forEach(url => this.initialAffectedUrls.push(url)); + affectedUrls.forEach(url => this.affectedUrls.push(url)); + } + onAffectedUrlAdd(): void { // tslint:disable-next-line:no-string-literal const newUrl = this.findingFormGroup.controls['findingAffectedUrls'].value; @@ -127,11 +142,12 @@ export class FindingDialogComponent implements OnInit { const newFindingData = this.findingFormGroup.getRawValue(); Object.entries(newFindingData).forEach(entry => { const [key, value] = entry; - if (value === null) { + // Affected Url form field can be ignored since changes here will be recognised inside affectedUrls of tag-list + if (value === null || key === 'findingAffectedUrls') { newFindingData[key] = ''; } }); - const didChange = !deepEqual(oldFindingData, newFindingData); + const didChange = !deepEqual(oldFindingData, newFindingData) || !deepEqual(this.initialAffectedUrls, this.affectedUrls); return didChange; } @@ -143,8 +159,13 @@ export class FindingDialogComponent implements OnInit { const findingData = {}; Object.entries(dialogData.form).forEach(entry => { const [key, value] = entry; + // console.info(key); findingData[key] = value.controlsConfig[0] ? (value.controlsConfig[0].value ? value.controlsConfig[0].value : value.controlsConfig[0]) : ''; + // Affected Url form field can be ignored since changes here will be recognised inside affectedUrls of tag-list + if (key === 'findingAffectedUrls') { + findingData[key] = ''; + } }); return findingData; } diff --git a/security-c4po-angular/src/shared/modules/finding-dialog/service/finding-dialog.service.ts b/security-c4po-angular/src/shared/modules/finding-dialog/service/finding-dialog.service.ts index 6157a2e..55a3fbf 100644 --- a/security-c4po-angular/src/shared/modules/finding-dialog/service/finding-dialog.service.ts +++ b/security-c4po-angular/src/shared/modules/finding-dialog/service/finding-dialog.service.ts @@ -34,6 +34,11 @@ export class FindingDialogService { config?: Partial | string>>): Observable { let dialogOptions: Partial | string>>; let dialogData: GenericDialogData; + let severity; + // transform severity of finding if existing + if (finding) { + severity = typeof finding.severity !== 'number' ? Severity[finding.severity] : finding.severity; + } // Setup FindingDialogBody dialogData = { form: { @@ -56,7 +61,7 @@ export class FindingDialogService { labelKey: 'finding.severity.label', placeholder: 'finding.severity', controlsConfig: [ - {value: finding ? finding.severity : Severity.LOW, disabled: false}, + {value: finding ? severity : Severity.LOW, disabled: false}, [Validators.required] ], errors: [ @@ -95,7 +100,7 @@ export class FindingDialogService { labelKey: 'finding.affectedUrls.label', placeholder: 'finding.affectedUrls.placeholder', controlsConfig: [ - {value: '', disabled: false}, + {value: finding ? finding.affectedUrls : [], disabled: false}, [] ], errors: [ diff --git a/security-c4po-angular/src/shared/services/pentest.service.ts b/security-c4po-angular/src/shared/services/pentest.service.ts index 7b1e2ea..d79bc87 100644 --- a/security-c4po-angular/src/shared/services/pentest.service.ts +++ b/security-c4po-angular/src/shared/services/pentest.service.ts @@ -85,42 +85,14 @@ export class PentestService { */ public getFindingsByPentestId(pentestId: string): Observable { return this.http.get(`${this.apiBaseURL}/${pentestId}/findings`); - // return of([]); - /*Todo: Remove mocked Findings? - return of([ - { - id: 'ca96cc19-88ff-4874-8406-dc892620afd4', - title: 'This is a creative title', - description: 'test', - impact: 'This impacts only the UI', - severity: Severity.LOW, - reproduction: '' - }, - { - id: 'ca96cc19-88ff-4874-8406-dc892620afd4', - title: 'This is a creative title', - description: 'test', - impact: 'This is impacts some things', - severity: Severity.MEDIUM, - reproduction: '' - }, - { - id: 'ca96cc19-88ff-4874-8406-dc892620afd4', - title: 'This is a creative title', - description: 'test', - impact: 'This is impacts a lot', - severity: Severity.HIGH, - reproduction: '' - }, - { - id: 'ca96cc19-88ff-4874-8406-dc892620afd4', - title: 'This is a creative title', - description: 'test', - impact: 'This is impacts a lot', - severity: Severity.CRITICAL, - reproduction: '' - } - ]);*/ + } + + /** + * Get Finding by Id + * @param findingId the id of the finding + */ + public getFindingById(findingId: string): Observable { + return this.http.get(`${this.apiBaseURL}/${findingId}/finding`); } /** @@ -132,6 +104,15 @@ export class PentestService { return this.http.post(`${this.apiBaseURL}/${pentestId}/finding`, finding); } + /** + * Update Finding + * @param findingId the id of the finding + * @param finding the information of the finding + */ + public updateFinding(findingId: string, finding: Finding): Observable { + return this.http.patch(`${this.apiBaseURL}/${findingId}/finding`, finding); + } + /** * Get Comments for Pentest Id * @param pentestId the id of the project diff --git a/security-c4po-api/security-c4po-api.postman_collection.json b/security-c4po-api/security-c4po-api.postman_collection.json index 351d9b1..f33af08 100644 --- a/security-c4po-api/security-c4po-api.postman_collection.json +++ b/security-c4po-api/security-c4po-api.postman_collection.json @@ -339,6 +339,85 @@ } }, "response": [] + }, + { + "name": "getFindingById", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzA0MTQ3ODYsImlhdCI6MTY3MDQxNDQ4NiwianRpIjoiM2FmOWU5M2MtY2YzNi00MjQwLTkzNWEtNDkxYTJkZTY2MWU4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI5M2ExNTBlMC03ZWRkLTQxZTgtYWE4Yi0yZWY5YTgzOWU4NDciLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.QjUkCInyCJ5Wsz4q56gfsLqERr6pYlGjwNw-VsKNJ_3Jp-8Dazq9UmDGN8AmAkQ0sp0b-FMm3jArKMBpr84gKd65trvQx_qHvXev5x2MWBG4_9v3C9MmjxWcAYRVmfRdURUOhfto-4YfRwMwNRsKJfwMIjfS5VT8bHJWipcCDzaidN8h_LLORbmmQZ2o0l4Jnv5qrrWzUcSTeEeBpHGOjes1-T0gOlDJa34Z9x_xrsTsybKAylrmX03mDSI-f2h5XqqtgnrxtddtHXHatfxB1BHWq-FILDsGf0UG47FEQjqapFvn9bFiNyq0GVrgdK42miEO7ywOtCOKpCfAUnMwdQ", + "type": "string" + }, + { + "key": "undefined", + "type": "any" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8443/pentests/cb33fad4-7965-4654-a9f9-f007edaca35c/finding", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8443", + "path": [ + "pentests", + "cb33fad4-7965-4654-a9f9-f007edaca35c", + "finding" + ] + } + }, + "response": [] + }, + { + "name": "updateFinding", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzA0MTQ3ODYsImlhdCI6MTY3MDQxNDQ4NiwianRpIjoiM2FmOWU5M2MtY2YzNi00MjQwLTkzNWEtNDkxYTJkZTY2MWU4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI5M2ExNTBlMC03ZWRkLTQxZTgtYWE4Yi0yZWY5YTgzOWU4NDciLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.QjUkCInyCJ5Wsz4q56gfsLqERr6pYlGjwNw-VsKNJ_3Jp-8Dazq9UmDGN8AmAkQ0sp0b-FMm3jArKMBpr84gKd65trvQx_qHvXev5x2MWBG4_9v3C9MmjxWcAYRVmfRdURUOhfto-4YfRwMwNRsKJfwMIjfS5VT8bHJWipcCDzaidN8h_LLORbmmQZ2o0l4Jnv5qrrWzUcSTeEeBpHGOjes1-T0gOlDJa34Z9x_xrsTsybKAylrmX03mDSI-f2h5XqqtgnrxtddtHXHatfxB1BHWq-FILDsGf0UG47FEQjqapFvn9bFiNyq0GVrgdK42miEO7ywOtCOKpCfAUnMwdQ", + "type": "string" + }, + { + "key": "undefined", + "type": "any" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Test Title\",\n \"severity\": \"CRITICAL\",\n \"description\": \"Test Description\",\n \"impact\": \"Test Impact\",\n \"affectedUrls\": [\n \"https://akveo.github.io/nebular/docs/components/progress-bar/examples#nbprogressbarcomponent\"\n ],\n \"reproduction\": \"Step 1: Test\",\n \"mitigation\": \"Test Mitigatin\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8443/pentests/cb33fad4-7965-4654-a9f9-f007edaca35c/finding", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8443", + "path": [ + "pentests", + "cb33fad4-7965-4654-a9f9-f007edaca35c", + "finding" + ] + } + }, + "response": [] } ] }, diff --git a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc index ac1be46..2b4a567 100644 --- a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc +++ b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc @@ -175,6 +175,26 @@ include::{snippets}/getFindingsByPentestId/http-response.adoc[] include::{snippets}/getFindingsByPentestId/response-fields.adoc[] +=== Get finding for id + +To get a finding by id, call the GET request /pentests/+{findingId}+/finding. + +==== Request example + +include::{snippets}/getFindingById/http-request.adoc[] + +==== Request structure + +include::{snippets}/getFindingById/path-parameters.adoc[] + +==== Response example + +include::{snippets}/getFindingById/http-response.adoc[] + +==== Response structure + +include::{snippets}/getFindingById/response-fields.adoc[] + === Save finding To save a finding, call the POST request /pentests/+{pentestId}+/finding @@ -195,10 +215,32 @@ include::{snippets}/saveFindingByPentestId/http-response.adoc[] include::{snippets}/saveFindingByPentestId/response-fields.adoc[] +=== Update finding + +To update a finding, call the PATCH request /pentests/+{findingId}+/finding + +==== Request example + +include::{snippets}/updateFindingById/http-request.adoc[] + +==== Request structure + +include::{snippets}/updateFindingById/path-parameters.adoc[] + +==== Response example + +include::{snippets}/updateFindingById/http-response.adoc[] + +==== Response structure + +include::{snippets}/updateFindingById/response-fields.adoc[] + == Change History |=== |Date |Change +|2022-12-09 +|Added GET and PATCH endpoint for Finding |2022-12-02 |Added GET and POST endpoint for Findings |2022-11-21 diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/finding/Finding.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/finding/Finding.kt index 440d585..c940536 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/finding/Finding.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/finding/Finding.kt @@ -16,6 +16,19 @@ data class Finding ( val mitigation: String? ) +fun buildFinding(body: FindingRequestBody, findingEntity: FindingEntity): Finding { + return Finding( + id = findingEntity.data.id, + severity = Severity.valueOf(body.severity), + title = body.title, + description = body.description, + impact = body.impact, + affectedUrls = body.affectedUrls, + reproduction = body.reproduction, + mitigation = body.mitigation + ) +} + data class FindingRequestBody( val severity: String, val title: String, diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/finding/FindingService.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/finding/FindingService.kt index 398f857..fcc0495 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/finding/FindingService.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/finding/FindingService.kt @@ -11,6 +11,7 @@ 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) @@ -57,6 +58,43 @@ class FindingService(private val findingRepository: FindingRepository, private v } } + /** + * Update [Finding] + * + * @throws [InvalidModelException] if the [Finding] is invalid + * @throws [TransactionInterruptedException] if the [Finding] could not be stored + * @return saved [Finding] + */ + fun updateFinding(findingId: String, body: FindingRequestBody): Mono { + validate( + require = body.isValid(), + logging = { logger.warn("Finding not valid.") }, + mappedException = InvalidModelException( + "Finding not valid.", Errorcode.FindingInvalid + ) + ) + return this.findingRepository.findFindingById(findingId).switchIfEmpty { + logger.warn("Finding with id $findingId not found.") + val msg = "Finding with id $findingId not found." + val ex = EntityNotFoundException(msg, Errorcode.FindingNotFound) + throw ex + }.flatMap { currentFindingEntity: FindingEntity -> + currentFindingEntity.lastModified = Instant.now() + currentFindingEntity.data = buildFinding(body, currentFindingEntity) + findingRepository.save(currentFindingEntity).map { + it.toFinding() + } + }.doOnError { + throw wrappedException( + logging = { logger.warn("Finding could not be updated in Database. Thrown exception: ", it) }, + mappedException = TransactionInterruptedException( + "Finding could not be stored.", + Errorcode.FindingInsertionFailed + ) + ) + } + } + /** * Get [Finding] by findingId * 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 8abbd0d..efaf8d0 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 @@ -82,6 +82,13 @@ class PentestController(private val pentestService: PentestService, private val } } + @GetMapping("/{findingId}/finding") + fun getFinding(@PathVariable(value = "findingId") findingId: String):Mono> { + return this.findingService.getFindingById(findingId).map { + ResponseEntity.ok().body(it.toFindingResponseBody()) + } + } + @PostMapping("/{pentestId}/finding") fun saveFinding( @PathVariable(value = "pentestId") pentestId: String, @@ -91,4 +98,14 @@ class PentestController(private val pentestService: PentestService, private val ResponseEntity.accepted().body(it.toFindingResponseBody()) } } + + @PatchMapping("/{findingId}/finding") + fun updateFinding( + @PathVariable(value = "findingId") findingId: String, + @RequestBody body: FindingRequestBody + ): Mono> { + return this.findingService.updateFinding(findingId, body).map { + ResponseEntity.accepted().body(it.toFindingResponseBody()) + } + } } \ No newline at end of file diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/PentestControllerDocumentationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/PentestControllerDocumentationTest.kt index ae1b129..63df7fb 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 @@ -297,6 +297,66 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { ) } + @Nested + inner class GetFinding { + @Test + fun getFindingById() { + val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f" + webTestClient.get() + .uri("/pentests/{findingId}/finding", findingId) + .header("Authorization", "Bearer $tokenAdmin") + .exchange() + .expectStatus().isOk + .expectHeader().doesNotExist("") + .expectBody().json(Json.write(findingOne.toFindingResponseBody())) + .consumeWith( + WebTestClientRestDocumentation.document( + "{methodName}", + Preprocessors.preprocessRequest( + Preprocessors.prettyPrint(), + Preprocessors.modifyUris().removePort(), + Preprocessors.removeHeaders("Host", "Content-Length") + ), + Preprocessors.preprocessResponse( + Preprocessors.prettyPrint() + ), + RequestDocumentation.relaxedPathParameters( + RequestDocumentation.parameterWithName("findingId").description("The id of the feinidng you want to get") + ), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING) + .description("The id of the requested pentest"), + PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING) + .description("The severity of the finding"), + PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING) + .description("The title of the requested finding"), + PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING) + .description("The description number of the finding"), + PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING) + .description("The impact of the finding"), + PayloadDocumentation.fieldWithPath("affectedUrls").type(JsonFieldType.ARRAY) + .description("List of affected Urls of the finding"), + PayloadDocumentation.fieldWithPath("reproduction").type(JsonFieldType.STRING) + .description("The reproduction steps of the finding"), + PayloadDocumentation.fieldWithPath("mitigation").type(JsonFieldType.STRING) + .description("The example mitigation for the finding") + ) + ) + ) + } + + private val findingOne = Finding( + id = "ab62d365-1b1d-4da1-89bc-5496616e220f", + severity = Severity.LOW, + title = "Found Bug", + description = "OTG-INFO-002 Bug", + impact = "Service", + affectedUrls = emptyList(), + reproduction = "Step 1: Hack", + mitigation = "None" + ) + } + @Nested inner class SaveFinding { @Test @@ -357,6 +417,66 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { ) } + @Nested + inner class UpdateFinding { + @Test + fun updateFindingById() { + val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f" + webTestClient.patch() + .uri("/pentests/{findingId}/finding", findingId) + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(findingBody), FindingRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().doesNotExist("") + .expectBody().json(Json.write(findingBody)) + .consumeWith( + WebTestClientRestDocumentation.document( + "{methodName}", + Preprocessors.preprocessRequest( + Preprocessors.prettyPrint(), + Preprocessors.modifyUris().removePort(), + Preprocessors.removeHeaders("Host", "Content-Length") + ), + Preprocessors.preprocessResponse( + Preprocessors.prettyPrint() + ), + RequestDocumentation.relaxedPathParameters( + RequestDocumentation.parameterWithName("findingId").description("The id of the finding you want to update") + ), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING) + .description("The id of the requested pentest"), + PayloadDocumentation.fieldWithPath("severity").type(JsonFieldType.STRING) + .description("The severity of the finding"), + PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING) + .description("The title of the requested finding"), + PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING) + .description("The description number of the finding"), + PayloadDocumentation.fieldWithPath("impact").type(JsonFieldType.STRING) + .description("The impact of the finding"), + PayloadDocumentation.fieldWithPath("affectedUrls").type(JsonFieldType.ARRAY) + .description("List of affected Urls of the finding"), + PayloadDocumentation.fieldWithPath("reproduction").type(JsonFieldType.STRING) + .description("The reproduction steps of the finding"), + PayloadDocumentation.fieldWithPath("mitigation").type(JsonFieldType.STRING) + .description("The example mitigation for the finding") + ) + ) + ) + } + + private val findingBody = FindingRequestBody( + severity = "HIGH", + title = "Updated Bug", + description = "Updated OTG-INFO-002 Bug", + impact = "Service", + affectedUrls = emptyList(), + reproduction = "Step 1: Hack", + mitigation = "Still None" + ) + } + private fun persistBasicTestScenario() { // setup test data // Project @@ -408,7 +528,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { reproduction = "Step 1: Hack", mitigation = "None" ) - // persist test data in database + // persist test data in database mongoTemplate.save(ProjectEntity(projectOne)) mongoTemplate.save(PentestEntity(pentestOne)) mongoTemplate.save(PentestEntity(pentestTwo)) 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 5c7a1d0..80e349a 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 @@ -190,7 +190,33 @@ class PentestControllerIntTest : BaseIntTest() { } @Nested - inner class SaveFindings { + inner class GetFinding { + @Test + fun `requesting finding by findingId successfully`() { + val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f" + webTestClient.get() + .uri("/pentests/{findingId}/finding", findingId) + .header("Authorization", "Bearer $tokenAdmin") + .exchange() + .expectStatus().isOk + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody().json(Json.write(findingOne.toFindingResponseBody())) + } + + private val findingOne = Finding( + id = "ab62d365-1b1d-4da1-89bc-5496616e220f", + severity = Severity.LOW, + title = "Found Bug", + description = "OTG-INFO-002 Bug", + impact = "Service", + affectedUrls = emptyList(), + reproduction = "Step 1: Hack", + mitigation = "None" + ) + } + + @Nested + inner class SaveFinding { @Test fun `save finding successfully`() { val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002" @@ -222,6 +248,39 @@ class PentestControllerIntTest : BaseIntTest() { ) } + @Nested + inner class UpdateFinding { + @Test + fun `update finding successfully`() { + val findingId = "43fbc63c-f624-11ec-b939-0242ac120002" + webTestClient.post() + .uri("/pentests/{findingId}/finding", findingId) + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(findingBody), FindingRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody() + .jsonPath("$.severity").isEqualTo("HIGH") + .jsonPath("$.title").isEqualTo("Updated Bug") + .jsonPath("$.description").isEqualTo("Updated OTG-INFO-002 Bug") + .jsonPath("$.impact").isEqualTo("Service") + .jsonPath("$.affectedUrls").isEmpty + .jsonPath("$.reproduction").isEqualTo("Step 1: Hack") + .jsonPath("$.mitigation").isEqualTo("Still None") + } + + private val findingBody = FindingRequestBody( + severity = "HIGH", + title = "Updated Bug", + description = "Updated OTG-INFO-002 Bug", + impact = "Service", + affectedUrls = emptyList(), + reproduction = "Step 1: Hack", + mitigation = "Still None" + ) + } + private fun persistBasicTestScenario() { // setup test data // project