diff --git a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc index 75f5cae..ac1be46 100644 --- a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc +++ b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc @@ -95,10 +95,114 @@ include::{snippets}/updateProject/http-response.adoc[] include::{snippets}/updateProject/response-fields.adoc[] +== Pentest + +=== Get pentests for category + +To get pentests by projectId and category, call the GET request /pentests with the appropriate parameters. + +==== Request example + +include::{snippets}/getPentestsByProjectIdAndCategory/http-request.adoc[] + +==== Request structure + +include::{snippets}/getPentestsByProjectIdAndCategory/request-parameters.adoc[] + +==== Response example + +include::{snippets}/getPentestsByProjectIdAndCategory/http-response.adoc[] + +==== Response structure + +include::{snippets}/getPentestsByProjectIdAndCategory/response-fields.adoc[] + +=== Save pentest + +To save a pentest, call the POST request /pentests/+{projectId}+ + +==== Request example + +include::{snippets}/savePentestByProjectId/http-request.adoc[] + +==== Request structure + +include::{snippets}/savePentestByProjectId/path-parameters.adoc[] + +==== Response example + +include::{snippets}/savePentestByProjectId/http-response.adoc[] + +==== Response structure + +include::{snippets}/savePentestByProjectId/response-fields.adoc[] + +=== Update pentest + +To update a pentest, call the PATCH request /pentests/+{pentestId}+ + +==== Request example + +include::{snippets}/updatePentestByProjectId/http-request.adoc[] + +==== Response example + +include::{snippets}/updatePentestByProjectId/http-response.adoc[] + +==== Response structure + +include::{snippets}/updatePentestByProjectId/response-fields.adoc[] + +== Finding + +=== Get findings for pentest + +To get findings by pentestId, call the GET request /pentests/+{pentestId}+/findings. + +==== Request example + +include::{snippets}/getFindingsByPentestId/http-request.adoc[] + +==== Request structure + +include::{snippets}/getFindingsByPentestId/path-parameters.adoc[] + +==== Response example + +include::{snippets}/getFindingsByPentestId/http-response.adoc[] + +==== Response structure + +include::{snippets}/getFindingsByPentestId/response-fields.adoc[] + +=== Save finding + +To save a finding, call the POST request /pentests/+{pentestId}+/finding + +==== Request example + +include::{snippets}/saveFindingByPentestId/http-request.adoc[] + +==== Request structure + +include::{snippets}/saveFindingByPentestId/path-parameters.adoc[] + +==== Response example + +include::{snippets}/saveFindingByPentestId/http-response.adoc[] + +==== Response structure + +include::{snippets}/saveFindingByPentestId/response-fields.adoc[] + == Change History |=== |Date |Change +|2022-12-02 +|Added GET and POST endpoint for Findings +|2022-11-21 +|Added GET, POST and PATCH endpoint for Pentests |2022-03-07 |Added PATCH endpoint to update Projects |2022-02-01 @@ -110,25 +214,3 @@ include::{snippets}/updateProject/response-fields.adoc[] |2021-02-12 |Initial version |=== - -== Pentest - -=== Get pentests by projectId and category - -To get pentests by projectId and category, call the GET request /pentests with the appropriate parameters. - -==== Request example - -include::{snippets}/getPentestsByProjectIdAndCategory/http-request.adoc[] - -==== Request Structure - -include::{snippets}/getPentestsByProjectIdAndCategory/request-parameters.adoc[] - -==== Response example - -include::{snippets}/getPentestsByProjectIdAndCategory/http-response.adoc[] - -==== Response structure - -include::{snippets}/getPentestsByProjectIdAndCategory/response-fields.adoc[] \ No newline at end of file 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 3f936ad..440d585 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 @@ -12,7 +12,7 @@ data class Finding ( val description: String, val impact: String, val affectedUrls: List? = emptyList(), - val reproduction: String?, + val reproduction: String, val mitigation: String? ) @@ -22,7 +22,7 @@ data class FindingRequestBody( val description: String, val impact: String, val affectedUrls: List? = emptyList(), - val reproduction: String?, + val reproduction: String, val mitigation: String? ) 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 6d58525..8abbd0d 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 @@ -50,7 +50,6 @@ class PentestController(private val pentestService: PentestService, private val } }*/ - // ToDo: Add Documentation & Tests @PostMapping("/{projectId}") fun savePentest( @PathVariable(value = "projectId") projectId: String, @@ -61,7 +60,6 @@ class PentestController(private val pentestService: PentestService, private val } } - // ToDo: Add Documentation & Tests @PatchMapping("/{pentestId}") fun updatePentest( @PathVariable(value = "pentestId") pentestId: String, @@ -72,18 +70,6 @@ class PentestController(private val pentestService: PentestService, private val } } - // ToDo: Add Documentation & Tests - @PostMapping("/{pentestId}/finding") - fun saveFinidng( - @PathVariable(value = "pentestId") pentestId: String, - @RequestBody body: FindingRequestBody - ): Mono> { - return this.findingService.saveFinding(pentestId, body).map { - ResponseEntity.accepted().body(it.toFindingResponseBody()) - } - } - - // ToDo: Add Documentation & Tests @GetMapping("/{pentestId}/findings") fun getFindings(@PathVariable(value = "pentestId") pentestId: String): Mono>> { return this.pentestService.getFindingIdsByPentestId(pentestId).flatMap { findingIds: List -> @@ -95,4 +81,14 @@ class PentestController(private val pentestService: PentestService, private val else ResponseEntity.ok(it) } } + + @PostMapping("/{pentestId}/finding") + fun saveFinding( + @PathVariable(value = "pentestId") pentestId: String, + @RequestBody body: FindingRequestBody + ): Mono> { + return this.findingService.saveFinding(pentestId, body).map { + ResponseEntity.accepted().body(it.toFindingResponseBody()) + } + } } \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt index e088c30..7ebbb1a 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectService.kt @@ -151,7 +151,7 @@ class ProjectService(private val projectRepository: ProjectRepository) { it.toProject() }.doOnError { throw wrappedException( - logging = { logger.warn("Project Pentests could not be updated in Database. Thrown exception: ", it) }, + logging = { logger.warn("Project Pentests could not be updated or saved in Database. Thrown exception: ", it) }, mappedException = TransactionInterruptedException( "Project could not be updated.", Errorcode.ProjectInsertionFailed diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/PentestControllerDocumentationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/PentestControllerDocumentationTest.kt index 67646cb..ae1b129 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 @@ -1,10 +1,14 @@ package com.securityc4po.api.pentest +import com.fasterxml.jackson.databind.ObjectMapper import com.github.tomakehurst.wiremock.common.Json import com.securityc4po.api.BaseDocumentationIntTest import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC +import com.securityc4po.api.finding.* +import com.securityc4po.api.project.Project +import com.securityc4po.api.project.ProjectEntity import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -18,6 +22,7 @@ import org.springframework.restdocs.payload.JsonFieldType import org.springframework.restdocs.payload.PayloadDocumentation import org.springframework.restdocs.request.RequestDocumentation import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation +import reactor.core.publisher.Mono @SuppressFBWarnings( SIC_INNER_SHOULD_BE_STATIC, @@ -28,6 +33,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { @Autowired lateinit var mongoTemplate: MongoTemplate + var mapper = ObjectMapper() @BeforeEach fun init() { @@ -44,7 +50,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { inner class GetPentests { @Test fun getPentestsByProjectIdAndCategory() { - val projectId = "d2e126ba-f608-11ec-b939-0242ac120002" + val projectId = "d2e126ba-f608-11ec-b939-0242ac120025" val category = "INFORMATION_GATHERING" webTestClient.get() .uri("/pentests?projectId={projectId}&category={category}", projectId, category) @@ -90,7 +96,7 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { private val pentestOne = Pentest( id = "9c8af320-f608-11ec-b939-0242ac120002", - projectId = "d2e126ba-f608-11ec-b939-0242ac120002", + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", category = PentestCategory.INFORMATION_GATHERING, refNumber = "OTG-INFO-001", status = PentestStatus.NOT_STARTED, @@ -99,11 +105,11 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { ) private val pentestTwo = Pentest( id = "43fbc63c-f624-11ec-b939-0242ac120002", - projectId = "d2e126ba-f608-11ec-b939-0242ac120002", + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", category = PentestCategory.INFORMATION_GATHERING, refNumber = "OTG-INFO-002", status = PentestStatus.IN_PROGRESS, - findingIds = emptyList(), + findingIds = listOf("ab62d365-1b1d-4da1-89bc-5496616e220f"), commentIds = emptyList() ) @@ -113,11 +119,260 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { ) } + @Nested + inner class SavePentest { + @Test + fun savePentestByProjectId() { + val projectId = "d2e126ba-f608-11ec-b939-0242ac120025" + webTestClient.post() + .uri("/pentests/{projectId}", projectId) + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(newPentestBody), PentestRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().doesNotExist("") + .expectBody().json(Json.write(newPentestBody)) + .consumeWith( + WebTestClientRestDocumentation.document( + "{methodName}", + Preprocessors.preprocessRequest( + Preprocessors.prettyPrint(), + Preprocessors.modifyUris().removePort(), + Preprocessors.removeHeaders("Host", "Content-Length") + ), + Preprocessors.preprocessResponse( + Preprocessors.prettyPrint() + ), + RequestDocumentation.relaxedPathParameters( + RequestDocumentation.parameterWithName("projectId").description("The id of the project you want to save the pentest for") + ), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING) + .description("The id of the created pentest"), + PayloadDocumentation.fieldWithPath("projectId").type(JsonFieldType.STRING) + .description("The id of the project of the created pentest"), + PayloadDocumentation.fieldWithPath("category").type(JsonFieldType.STRING) + .description("The category of the created pentest"), + PayloadDocumentation.fieldWithPath("refNumber").type(JsonFieldType.STRING) + .description("The reference number of the created pentest according to the current OWASP Testing Guide"), + PayloadDocumentation.fieldWithPath("status").type(JsonFieldType.STRING) + .description("The status of the created pentest"), + PayloadDocumentation.fieldWithPath("findingIds").type(JsonFieldType.ARRAY) + .description("List of ids of the findings in the created pentest"), + PayloadDocumentation.fieldWithPath("commentIds").type(JsonFieldType.ARRAY) + .description("List of ids of the comments of the created pentest") + ) + ) + ) + } + + val newPentestBody = PentestRequestBody( + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", + category = "CLIENT_SIDE_TESTING", + refNumber = "OTG-CLIENT-001", + status = "IN_PROGRESS", + findingIds = emptyList(), + commentIds = emptyList() + ) + } + + @Nested + inner class UpdatePentest { + @Test + fun updatePentestByProjectId() { + val pentestOneId = "9c8af320-f608-11ec-b939-0242ac120002" + webTestClient.patch() + .uri("/pentests/{pentestId}", pentestOneId) + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(pentestOneBody), PentestRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().doesNotExist("") + .expectBody().json(Json.write(pentestOneBody)) + .consumeWith( + WebTestClientRestDocumentation.document( + "{methodName}", + Preprocessors.preprocessRequest( + Preprocessors.prettyPrint(), + Preprocessors.modifyUris().removePort(), + Preprocessors.removeHeaders("Host", "Content-Length") + ), + Preprocessors.preprocessResponse( + Preprocessors.prettyPrint() + ), + RequestDocumentation.relaxedPathParameters( + RequestDocumentation.parameterWithName("pentestId").description("The id of the pentest you want to update") + ), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING) + .description("The id of the updated pentest"), + PayloadDocumentation.fieldWithPath("projectId").type(JsonFieldType.STRING) + .description("The id of the project of the updated pentest"), + PayloadDocumentation.fieldWithPath("category").type(JsonFieldType.STRING) + .description("The category of the updated pentest"), + PayloadDocumentation.fieldWithPath("refNumber").type(JsonFieldType.STRING) + .description("The reference number of the updated pentest according to the current OWASP Testing Guide"), + PayloadDocumentation.fieldWithPath("status").type(JsonFieldType.STRING) + .description("The status of the updated pentest"), + PayloadDocumentation.fieldWithPath("findingIds").type(JsonFieldType.ARRAY) + .description("List of ids of the findings in the updated pentest"), + PayloadDocumentation.fieldWithPath("commentIds").type(JsonFieldType.ARRAY) + .description("List of ids of the comments of the updated pentest") + ) + ) + ) + } + + val pentestOneBody = PentestRequestBody( + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", + category = "INFORMATION_GATHERING", + refNumber = "OTG-INFO-001", + status = "OPEN", + findingIds = emptyList(), + commentIds = emptyList() + ) + } + + @Nested + inner class GetFindings { + @Test + fun getFindingsByPentestId() { + val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002" + webTestClient.get() + .uri("/pentests/{pentestId}/findings", pentestTwoId) + .header("Authorization", "Bearer $tokenAdmin") + .exchange() + .expectStatus().isOk + .expectHeader().doesNotExist("") + .expectBody().json(Json.write(getFindingsResponse())) + .consumeWith( + WebTestClientRestDocumentation.document( + "{methodName}", + Preprocessors.preprocessRequest( + Preprocessors.prettyPrint(), + Preprocessors.modifyUris().removePort(), + Preprocessors.removeHeaders("Host", "Content-Length") + ), + Preprocessors.preprocessResponse( + Preprocessors.prettyPrint() + ), + RequestDocumentation.relaxedPathParameters( + RequestDocumentation.parameterWithName("pentestId").description("The id of the pentest you want to get the findings for") + ), + 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" + ) + + private fun getFindingsResponse() = listOf( + findingOne.toFindingResponseBody() + ) + } + + @Nested + inner class SaveFinding { + @Test + fun saveFindingByPentestId() { + val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002" + webTestClient.post() + .uri("/pentests/{pentestId}/finding", pentestTwoId) + .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("pentestId").description("The id of the pentest you want to save the finding for") + ), + 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 = "MEDIUM", + title = "Found another Bug", + description = "Another OTG-INFO-002 Bug", + impact = "Service", + affectedUrls = emptyList(), + reproduction = "Step 1: Hack more", + mitigation = "Another None" + ) + } + private fun persistBasicTestScenario() { // setup test data + // Project + val projectOne = Project( + id = "d2e126ba-f608-11ec-b939-0242ac120025", + client = "E Corp", + title = "Some Mock API (v1.0) Scanning", + createdAt = "2021-01-10T18:05:00Z", + tester = "Novatester", + projectPentests = emptyList(), + createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032" + ) + // Pentests val pentestOne = Pentest( id = "9c8af320-f608-11ec-b939-0242ac120002", - projectId = "d2e126ba-f608-11ec-b939-0242ac120002", + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", category = PentestCategory.INFORMATION_GATHERING, refNumber = "OTG-INFO-001", status = PentestStatus.NOT_STARTED, @@ -126,26 +381,39 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { ) val pentestTwo = Pentest( id = "43fbc63c-f624-11ec-b939-0242ac120002", - projectId = "d2e126ba-f608-11ec-b939-0242ac120002", + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", category = PentestCategory.INFORMATION_GATHERING, refNumber = "OTG-INFO-002", status = PentestStatus.IN_PROGRESS, - findingIds = emptyList(), + findingIds = listOf("ab62d365-1b1d-4da1-89bc-5496616e220f"), commentIds = emptyList() ) val pentestThree = Pentest( id = "74eae112-f62c-11ec-b939-0242ac120002", - projectId = "6fad3474-fc29-49f9-bd37-e039e9e60c18", + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", category = PentestCategory.AUTHENTICATION_TESTING, refNumber = "OTG-AUTHN-001", status = PentestStatus.COMPLETED, findingIds = emptyList(), commentIds = emptyList() ) - // persist test data in database + // Findings + 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" + ) + // persist test data in database + mongoTemplate.save(ProjectEntity(projectOne)) mongoTemplate.save(PentestEntity(pentestOne)) mongoTemplate.save(PentestEntity(pentestTwo)) mongoTemplate.save(PentestEntity(pentestThree)) + mongoTemplate.save(FindingEntity(findingOne)) } private fun configureAdminToken() { @@ -153,7 +421,9 @@ class PentestControllerDocumentationTest : BaseDocumentationIntTest() { } private fun cleanUp() { + mongoTemplate.findAllAndRemove(Query(), ProjectEntity::class.java) mongoTemplate.findAllAndRemove(Query(), PentestEntity::class.java) + mongoTemplate.findAllAndRemove(Query(), FindingEntity::class.java) tokenAdmin = "n/a" } 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 3f86895..5c7a1d0 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 @@ -5,6 +5,9 @@ import com.securityc4po.api.BaseIntTest import com.securityc4po.api.configuration.NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR import com.securityc4po.api.configuration.RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE import com.securityc4po.api.configuration.SIC_INNER_SHOULD_BE_STATIC +import com.securityc4po.api.finding.* +import com.securityc4po.api.project.Project +import com.securityc4po.api.project.ProjectEntity import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -15,6 +18,7 @@ import org.springframework.boot.web.server.LocalServerPort import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Query import org.springframework.test.web.reactive.server.WebTestClient +import reactor.core.publisher.Mono import java.time.Duration @SuppressFBWarnings( @@ -56,8 +60,10 @@ class PentestControllerIntTest : BaseIntTest() { inner class GetPentests { @Test fun `requesting pentests by projectId and category successfully`() { + val projectId = "d2e126ba-f608-11ec-b939-0242ac120025" + val category = "INFORMATION_GATHERING" webTestClient.get() - .uri("/pentests?projectId=d2e126ba-f608-11ec-b939-0242ac120002&category=INFORMATION_GATHERING") + .uri("/pentests?projectId={projectId}&category={category}", projectId, category) .header("Authorization", "Bearer $tokenAdmin") .exchange() .expectStatus().isOk @@ -67,7 +73,7 @@ class PentestControllerIntTest : BaseIntTest() { private val pentestOne = Pentest( id = "9c8af320-f608-11ec-b939-0242ac120002", - projectId = "d2e126ba-f608-11ec-b939-0242ac120002", + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", category = PentestCategory.INFORMATION_GATHERING, refNumber = "OTG-INFO-001", status = PentestStatus.NOT_STARTED, @@ -76,11 +82,11 @@ class PentestControllerIntTest : BaseIntTest() { ) private val pentestTwo = Pentest( id = "43fbc63c-f624-11ec-b939-0242ac120002", - projectId = "d2e126ba-f608-11ec-b939-0242ac120002", + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", category = PentestCategory.INFORMATION_GATHERING, refNumber = "OTG-INFO-002", status = PentestStatus.IN_PROGRESS, - findingIds = emptyList(), + findingIds = listOf("ab62d365-1b1d-4da1-89bc-5496616e220f"), commentIds = emptyList() ) @@ -90,11 +96,147 @@ class PentestControllerIntTest : BaseIntTest() { ) } + @Nested + inner class SavePentest { + @Test + fun `save pentest successfully`() { + val projectId = "d2e126ba-f608-11ec-b939-0242ac120025" + webTestClient.post() + .uri("/pentests/{projectId}", projectId) + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(pentestRequestBody), PentestRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody() + .jsonPath("$.projectId").isEqualTo("d2e126ba-f608-11ec-b939-0242ac120025") + .jsonPath("$.category").isEqualTo("CLIENT_SIDE_TESTING") + .jsonPath("$.refNumber").isEqualTo("OTG-CLIENT-001") + .jsonPath("$.status").isEqualTo("IN_PROGRESS") + .jsonPath("$.findingIds").isEmpty + .jsonPath("$.commentIds").isEmpty + } + + val pentestRequestBody = PentestRequestBody( + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", + category = "CLIENT_SIDE_TESTING", + refNumber = "OTG-CLIENT-001", + status = "IN_PROGRESS", + findingIds = emptyList(), + commentIds = emptyList() + ) + } + + @Nested + inner class UpdatePentest { + @Test + fun `update pentest successfully`() { + val pentestOneId = "9c8af320-f608-11ec-b939-0242ac120002" + webTestClient.patch() + .uri("/pentests/{pentestId}", pentestOneId) + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(pentestOneRequestBody), PentestRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody() + .jsonPath("$.id").isEqualTo(pentestOneId) + .jsonPath("$.projectId").isEqualTo("d2e126ba-f608-11ec-b939-0242ac120025") + .jsonPath("$.category").isEqualTo("INFORMATION_GATHERING") + .jsonPath("$.refNumber").isEqualTo("OTG-INFO-001") + .jsonPath("$.status").isEqualTo("OPEN") + .jsonPath("$.findingIds").isEmpty + .jsonPath("$.commentIds").isEmpty + } + + val pentestOneRequestBody = PentestRequestBody( + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", + category = "INFORMATION_GATHERING", + refNumber = "OTG-INFO-001", + status = "OPEN", + findingIds = emptyList(), + commentIds = emptyList() + ) + } + + @Nested + inner class GetFindings { + @Test + fun `requesting findings by pentestId successfully`() { + val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002" + webTestClient.get() + .uri("/pentests/{pentestId}/findings", pentestTwoId) + .header("Authorization", "Bearer $tokenAdmin") + .exchange() + .expectStatus().isOk + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody().json(Json.write(getFindings())) + } + + 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" + ) + + private fun getFindings() = listOf( + findingOne.toFindingResponseBody() + ) + } + + @Nested + inner class SaveFindings { + @Test + fun `save finding successfully`() { + val pentestTwoId = "43fbc63c-f624-11ec-b939-0242ac120002" + webTestClient.post() + .uri("/pentests/{pentestId}/finding", pentestTwoId) + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(findingBody), FindingRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody() + .jsonPath("$.severity").isEqualTo("MEDIUM") + .jsonPath("$.title").isEqualTo("Found another Bug") + .jsonPath("$.description").isEqualTo("Another OTG-INFO-002 Bug") + .jsonPath("$.impact").isEqualTo("Service") + .jsonPath("$.affectedUrls").isEmpty + .jsonPath("$.reproduction").isEqualTo("Step 1: Hack more") + .jsonPath("$.mitigation").isEqualTo("Another None") + } + + private val findingBody = FindingRequestBody( + severity = "MEDIUM", + title = "Found another Bug", + description = "Another OTG-INFO-002 Bug", + impact = "Service", + affectedUrls = emptyList(), + reproduction = "Step 1: Hack more", + mitigation = "Another None" + ) + } + private fun persistBasicTestScenario() { // setup test data + // project + val projectOne = Project( + id = "d2e126ba-f608-11ec-b939-0242ac120025", + client = "E Corp", + title = "Some Mock API (v1.0) Scanning", + createdAt = "2021-01-10T18:05:00Z", + tester = "Novatester", + createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032" + ) + // pentests val pentestOne = Pentest( id = "9c8af320-f608-11ec-b939-0242ac120002", - projectId = "d2e126ba-f608-11ec-b939-0242ac120002", + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", category = PentestCategory.INFORMATION_GATHERING, refNumber = "OTG-INFO-001", status = PentestStatus.NOT_STARTED, @@ -103,26 +245,39 @@ class PentestControllerIntTest : BaseIntTest() { ) val pentestTwo = Pentest( id = "43fbc63c-f624-11ec-b939-0242ac120002", - projectId = "d2e126ba-f608-11ec-b939-0242ac120002", + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", category = PentestCategory.INFORMATION_GATHERING, refNumber = "OTG-INFO-002", status = PentestStatus.IN_PROGRESS, - findingIds = emptyList(), + findingIds = listOf("ab62d365-1b1d-4da1-89bc-5496616e220f"), commentIds = emptyList() ) val pentestThree = Pentest( - id = "74eae112-f62c-11ec-b939-0242ac120002", - projectId = "6fad3474-fc29-49f9-bd37-e039e9e60c18", + id = "16vbc63c-f624-11ec-b939-0242ac120002", + projectId = "d2e126ba-f608-11ec-b939-0242ac120025", category = PentestCategory.AUTHENTICATION_TESTING, refNumber = "OTG-AUTHN-001", status = PentestStatus.COMPLETED, findingIds = emptyList(), commentIds = emptyList() ) + // Finding + 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" + ) // persist test data in database + mongoTemplate.save(ProjectEntity(projectOne)) mongoTemplate.save(PentestEntity(pentestOne)) mongoTemplate.save(PentestEntity(pentestTwo)) mongoTemplate.save(PentestEntity(pentestThree)) + mongoTemplate.save(FindingEntity(findingOne)) } private fun configureAdminToken() { @@ -130,7 +285,9 @@ class PentestControllerIntTest : BaseIntTest() { } private fun cleanUp() { + mongoTemplate.findAllAndRemove(Query(), ProjectEntity::class.java) mongoTemplate.findAllAndRemove(Query(), PentestEntity::class.java) + mongoTemplate.findAllAndRemove(Query(), FindingEntity::class.java) tokenAdmin = "n/a" } diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt index 6707db4..57b0427 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt @@ -92,7 +92,8 @@ class ProjectControllerIntTest : BaseIntTest() { inner class SaveProject { @Test fun `save project successfully`() { - webTestClient.post().uri("/projects") + webTestClient.post() + .uri("/projects") .header("Authorization", "Bearer $tokenAdmin") .body(Mono.just(project), ProjectRequestBody::class.java) .exchange() diff --git a/wiki/C4PO-Roadmap.png b/wiki/C4PO-Roadmap.png index 011f081..1d23188 100644 Binary files a/wiki/C4PO-Roadmap.png and b/wiki/C4PO-Roadmap.png differ