feat: As a user I want to edit my finding

This commit is contained in:
Marcel Haag 2022-12-07 14:04:30 +01:00
parent 076fa087e8
commit cd399ff859
13 changed files with 472 additions and 52 deletions

View File

@ -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<boolean> {

View File

@ -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": {

View File

@ -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": {

View File

@ -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;
}

View File

@ -34,6 +34,11 @@ export class FindingDialogService {
config?: Partial<NbDialogConfig<Partial<any> | string>>): Observable<any> {
let dialogOptions: Partial<NbDialogConfig<Partial<any> | 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: [

View File

@ -85,42 +85,14 @@ export class PentestService {
*/
public getFindingsByPentestId(pentestId: string): Observable<Finding[]> {
return this.http.get<Finding[]>(`${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<Finding> {
return this.http.get<Finding>(`${this.apiBaseURL}/${findingId}/finding`);
}
/**
@ -132,6 +104,15 @@ export class PentestService {
return this.http.post<Finding>(`${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<Finding> {
return this.http.patch<Finding>(`${this.apiBaseURL}/${findingId}/finding`, finding);
}
/**
* Get Comments for Pentest Id
* @param pentestId the id of the project

View File

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

View File

@ -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

View File

@ -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,

View File

@ -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<Finding> {
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
*

View File

@ -82,6 +82,13 @@ class PentestController(private val pentestService: PentestService, private val
}
}
@GetMapping("/{findingId}/finding")
fun getFinding(@PathVariable(value = "findingId") findingId: String):Mono<ResponseEntity<ResponseBody>> {
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<ResponseEntity<ResponseBody>> {
return this.findingService.updateFinding(findingId, body).map {
ResponseEntity.accepted().body(it.toFindingResponseBody())
}
}
}

View File

@ -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

View File

@ -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