feat: added pentest endpoint to get pentests by projectId & category

This commit is contained in:
norman-schmidt 2022-08-05 11:00:15 +02:00 committed by GitHub
parent ca77f6f208
commit 24dccb3e8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 549 additions and 7 deletions

View File

@ -1,6 +1,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {environment} from '../../environments/environment'; import {environment} from '../../environments/environment';
import {HttpClient} from '@angular/common/http'; import {HttpClient, HttpParams} from '@angular/common/http';
import {Observable, of} from 'rxjs'; import {Observable, of} from 'rxjs';
import {Category} from '@shared/models/category.model'; import {Category} from '@shared/models/category.model';
import {Pentest} from '@shared/models/pentest.model'; import {Pentest} from '@shared/models/pentest.model';
@ -47,6 +47,7 @@ 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
*/ */
private getPentestByProjectIdAndCategory(projectId: string, category: Category): Observable<Pentest[]> { private getPentestByProjectIdAndCategory(projectId: string, category: Category): Observable<Pentest[]> {
return this.http.get<Pentest[]>(`${this.apiBaseURL}?projectId=${projectId}?category=${category}`); const queryParams = new HttpParams().append('projectId', projectId).append('category', Category[category]);
return this.http.get<Pentest[]>(`${this.apiBaseURL}`, {params: queryParams});
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"info": { "info": {
"_postman_id": "6537da59-5c7a-478d-bf24-09a39022a690", "_postman_id": "58adc500-c0c6-47f3-b268-5fcc16e0944d",
"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"
@ -255,6 +255,40 @@
} }
] ]
}, },
{
"name": "pentests",
"item": [
{
"name": "getPentestsByProjectIdAndCategory",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8443/pentests?projectId=8bc16303-f652-418a-b745-8a03d89356fb&category=INFORMATION_GATHERING",
"protocol": "http",
"host": [
"localhost"
],
"port": "8443",
"path": [
"pentests"
],
"query": [
{
"key": "projectId",
"value": "8bc16303-f652-418a-b745-8a03d89356fb"
},
{
"key": "category",
"value": "INFORMATION_GATHERING"
}
]
}
},
"response": []
}
]
},
{ {
"name": "getHealth", "name": "getHealth",
"request": { "request": {

View File

@ -57,12 +57,16 @@ include::{snippets}/saveProject/response-fields.adoc[]
=== Delete project === Delete project
To delete a project, call the DELETE request /projects/{projectId} To delete a project, call the DELETE request /projects/+{projectId}+
==== Request example ==== Request example
include::{snippets}/deleteProject/http-request.adoc[] include::{snippets}/deleteProject/http-request.adoc[]
==== Request structure
include::{snippets}/deleteProject/path-parameters.adoc[]
==== Response example ==== Response example
include::{snippets}/deleteProject/http-response.adoc[] include::{snippets}/deleteProject/http-response.adoc[]
@ -77,7 +81,7 @@ include::{snippets}/deleteProject/response-fields.adoc[]
=== Update project === Update project
To update a project, call the PATCH request /projects/{projectId} To update a project, call the PATCH request /projects/+{projectId}+
==== Request example ==== Request example
@ -106,3 +110,25 @@ include::{snippets}/updateProject/response-fields.adoc[]
|2021-02-12 |2021-02-12
|Initial version |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[]

View File

@ -1,6 +1,5 @@
package com.securityc4po.api.configuration.security package com.securityc4po.api.configuration.security
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
@ -34,6 +33,7 @@ class WebSecurityConfiguration(private val userAccountDetailsService: UserAccoun
.disable() .disable()
.authorizeExchange() .authorizeExchange()
.pathMatchers(HttpMethod.GET, "/v1/projects/**").authenticated() .pathMatchers(HttpMethod.GET, "/v1/projects/**").authenticated()
.pathMatchers(HttpMethod.GET, "/v1/pentests/**").authenticated()
.pathMatchers("/actuator/**").permitAll() .pathMatchers("/actuator/**").permitAll()
.pathMatchers("/docs/SecurityC4PO.html").permitAll() .pathMatchers("/docs/SecurityC4PO.html").permitAll()
.anyExchange().authenticated() .anyExchange().authenticated()

View File

@ -0,0 +1,30 @@
package com.securityc4po.api.pentest
import com.securityc4po.api.ResponseBody
import org.springframework.data.mongodb.core.index.Indexed
import java.util.UUID
data class Pentest(
@Indexed(background = true, unique = true)
val id: String = UUID.randomUUID().toString(),
val projectId: String,
val category: PentestCategory,
val title: String,
val refNumber: String,
val status: PentestStatus,
val findingIds: String,
val commentIds: String
)
fun Pentest.toPentestResponseBody(): ResponseBody {
return mapOf(
"id" to id,
"projectId" to projectId,
"category" to category,
"title" to title,
"refNumber" to refNumber,
"status" to status,
"findingIds" to findingIds,
"commentIds" to commentIds
)
}

View File

@ -0,0 +1,15 @@
package com.securityc4po.api.pentest
enum class PentestCategory {
INFORMATION_GATHERING,
CONFIGURATION_AND_DEPLOY_MANAGEMENT_TESTING,
IDENTITY_MANAGEMENT_TESTING,
AUTHENTICATION_TESTING,
AUTHORIZATION_TESTING,
SESSION_MANAGEMENT_TESTING,
INPUT_VALIDATION_TESTING,
ERROR_HANDLING,
CRYPTOGRAPHY,
BUSINESS_LOGIC_TESTING,
CLIENT_SIDE_TESTING
}

View File

@ -0,0 +1,40 @@
package com.securityc4po.api.pentest
import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.extensions.getLoggerFor
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import com.securityc4po.api.ResponseBody
import org.springframework.http.ResponseEntity
import org.springframework.http.ResponseEntity.noContent
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono
@RestController
@RequestMapping("/pentests")
@CrossOrigin(
origins = [],
allowCredentials = "false",
allowedHeaders = ["*"],
methods = [RequestMethod.GET]
)
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION)
class PentestController(private val pentestService: PentestService) {
var logger = getLoggerFor<PentestController>()
@GetMapping
fun getPentestsByProjectIdAndCategory(
@RequestParam("projectId") projectId: String,
@RequestParam("category") category: String
): Mono<ResponseEntity<List<ResponseBody>>> {
return pentestService.getPentests(projectId, PentestCategory.valueOf(category)).map { pentestList ->
pentestList.map {
it.toPentestResponseBody()
}
}.map {
if (it.isEmpty()) noContent().build()
else ResponseEntity.ok(it)
}
}
}

View File

@ -0,0 +1,32 @@
package com.securityc4po.api.pentest
import com.securityc4po.api.BaseEntity
import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.configuration.MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.springframework.data.mongodb.core.mapping.Document
@Document(collection = "pentests")
open class PentestEntity(
data: Pentest
) : BaseEntity<Pentest>(data)
fun PentestEntity.toPentest(): Pentest {
return Pentest(
this.data.id,
this.data.projectId,
this.data.category,
this.data.title,
this.data.refNumber,
this.data.status,
this.data.findingIds,
this.data.commentIds
)
}
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
fun List<PentestEntity>.toPentests(): List<Pentest> {
return this.map {
it.toPentest()
}
}

View File

@ -0,0 +1,14 @@
package com.securityc4po.api.pentest
import org.springframework.data.mongodb.repository.Query
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux
@Repository
interface PentestRepository : ReactiveMongoRepository<PentestEntity, String> {
@Query("{'data.projectId': ?0, 'data.category': ?1}")
fun findPentestByProjectIdAndCategory(projectId: String, category: PentestCategory): Flux<PentestEntity>
}

View File

@ -0,0 +1,26 @@
package com.securityc4po.api.pentest
import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.configuration.MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION
import com.securityc4po.api.extensions.getLoggerFor
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
@Service
@SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION)
class PentestService(private val pentestRepository: PentestRepository) {
var logger = getLoggerFor<PentestService>()
/**
* Get all [Pentest]s by projectId and category
*
* @return list of [Pentest]
*/
fun getPentests(projectId: String, category: PentestCategory): Mono<List<Pentest>> {
return pentestRepository.findPentestByProjectIdAndCategory(projectId, category).collectList().map {
it.map { pentestEntity -> pentestEntity.toPentest() }
}
}
}

View File

@ -0,0 +1,11 @@
package com.securityc4po.api.pentest
enum class PentestStatus {
NOT_STARTED,
OPEN,
UNDER_REVIEW,
DISABLED,
CHECKED,
REPORTED,
TRIAGED
}

View File

@ -0,0 +1,167 @@
package com.securityc4po.api.pentest
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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.query.Query
import org.springframework.restdocs.operation.preprocess.Preprocessors
import org.springframework.restdocs.payload.JsonFieldType
import org.springframework.restdocs.payload.PayloadDocumentation
import org.springframework.restdocs.request.RequestDocumentation
import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation
@SuppressFBWarnings(
SIC_INNER_SHOULD_BE_STATIC,
NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
)
class PentestControllerDocumentationTest : BaseDocumentationIntTest() {
@Autowired
lateinit var mongoTemplate: MongoTemplate
@BeforeEach
fun init() {
configureAdminToken()
persistBasicTestScenario()
}
@AfterEach
fun destroy() {
cleanUp()
}
@Nested
inner class GetPentests {
@Test
fun getPentestsByProjectIdAndCategory() {
val projectId = "d2e126ba-f608-11ec-b939-0242ac120002"
val category = "INFORMATION_GATHERING"
webTestClient.get()
.uri("/pentests?projectId={projectId}&category={category}", projectId, category)
.header("Authorization", "Bearer $tokenAdmin")
.exchange()
.expectStatus().isOk
.expectHeader().doesNotExist("")
.expectBody().json(Json.write(getProjectsResponse()))
.consumeWith(
WebTestClientRestDocumentation.document(
"{methodName}",
Preprocessors.preprocessRequest(
Preprocessors.prettyPrint(),
Preprocessors.modifyUris().removePort(),
Preprocessors.removeHeaders("Host", "Content-Length")
),
Preprocessors.preprocessResponse(
Preprocessors.prettyPrint()
),
RequestDocumentation.relaxedRequestParameters(
RequestDocumentation.parameterWithName("projectId").description("The id of the project you want to get the pentests for"),
RequestDocumentation.parameterWithName("category").description("The category you want to get the pentests for")
),
PayloadDocumentation.relaxedResponseFields(
PayloadDocumentation.fieldWithPath("[].id").type(JsonFieldType.STRING)
.description("The id of the requested pentest"),
PayloadDocumentation.fieldWithPath("[].projectId").type(JsonFieldType.STRING)
.description("The id of the project of the requested pentest"),
PayloadDocumentation.fieldWithPath("[].category").type(JsonFieldType.STRING)
.description("The category of the requested pentest"),
PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING)
.description("The title of the requested pentest"),
PayloadDocumentation.fieldWithPath("[].refNumber").type(JsonFieldType.STRING)
.description("The reference number of the requested pentest according to the current OWASP Testing Guide"),
PayloadDocumentation.fieldWithPath("[].status").type(JsonFieldType.STRING)
.description("The status of the requested pentest"),
PayloadDocumentation.fieldWithPath("[].findingIds").type(JsonFieldType.STRING)
.description("The ids of the findings in the requested pentest"),
PayloadDocumentation.fieldWithPath("[].commentIds").type(JsonFieldType.STRING)
.description("The ids of the comments of the requested pentest")
)
)
)
}
private val pentestOne = Pentest(
id = "9c8af320-f608-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING,
title = "Search engine discovery/reconnaissance",
refNumber = "OTG-INFO-001",
status = PentestStatus.NOT_STARTED,
findingIds = "",
commentIds = ""
)
private val pentestTwo = Pentest(
id = "43fbc63c-f624-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING,
title = "Fingerprint Web Server",
refNumber = "OTG-INFO-002",
status = PentestStatus.REPORTED,
findingIds = "",
commentIds = ""
)
private fun getProjectsResponse() = listOf(
pentestOne.toPentestResponseBody(),
pentestTwo.toPentestResponseBody()
)
}
private fun persistBasicTestScenario() {
// setup test data
val pentestOne = Pentest(
id = "9c8af320-f608-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING,
title = "Search engine discovery/reconnaissance",
refNumber = "OTG-INFO-001",
status = PentestStatus.NOT_STARTED,
findingIds = "",
commentIds = ""
)
val pentestTwo = Pentest(
id = "43fbc63c-f624-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING,
title = "Fingerprint Web Server",
refNumber = "OTG-INFO-002",
status = PentestStatus.REPORTED,
findingIds = "",
commentIds = ""
)
val pentestThree = Pentest(
id = "74eae112-f62c-11ec-b939-0242ac120002",
projectId = "6fad3474-fc29-49f9-bd37-e039e9e60c18",
category = PentestCategory.AUTHENTICATION_TESTING,
title = "Testing for Credentials Transported over an Encrypted Channel",
refNumber = "OTG-AUTHN-001",
status = PentestStatus.CHECKED,
findingIds = "",
commentIds = ""
)
// persist test data in database
mongoTemplate.save(PentestEntity(pentestOne))
mongoTemplate.save(PentestEntity(pentestTwo))
mongoTemplate.save(PentestEntity(pentestThree))
}
private fun configureAdminToken() {
tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local")
}
private fun cleanUp() {
mongoTemplate.findAllAndRemove(Query(), PentestEntity::class.java)
tokenAdmin = "n/a"
}
}

View File

@ -0,0 +1,142 @@
package com.securityc4po.api.pentest
import com.github.tomakehurst.wiremock.common.Json
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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
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 java.time.Duration
@SuppressFBWarnings(
SIC_INNER_SHOULD_BE_STATIC,
NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR,
RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE
)
class PentestControllerIntTest : BaseIntTest() {
@LocalServerPort
private var port = 0
@Autowired
lateinit var mongoTemplate: MongoTemplate
@Autowired
private lateinit var webTestClient: WebTestClient
@BeforeEach
fun setupWebClient() {
webTestClient = WebTestClient.bindToServer()
.baseUrl("http://localhost:$port")
.responseTimeout(Duration.ofMillis(10000))
.build()
}
@BeforeEach
fun init() {
configureAdminToken()
persistBasicTestScenario()
}
@AfterEach
fun destroy() {
cleanUp()
}
@Nested
inner class GetPentests {
@Test
fun `requesting pentests by projectId and category successfully`() {
webTestClient.get()
.uri("/pentests?projectId=d2e126ba-f608-11ec-b939-0242ac120002&category=INFORMATION_GATHERING")
.header("Authorization", "Bearer $tokenAdmin")
.exchange()
.expectStatus().isOk
.expectHeader().valueEquals("Application-Name", "SecurityC4PO")
.expectBody().json(Json.write(getPentests()))
}
private val pentestOne = Pentest(
id = "9c8af320-f608-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING,
title = "Search engine discovery/reconnaissance",
refNumber = "OTG-INFO-001",
status = PentestStatus.NOT_STARTED,
findingIds = "",
commentIds = ""
)
private val pentestTwo = Pentest(
id = "43fbc63c-f624-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING,
title = "Fingerprint Web Server",
refNumber = "OTG-INFO-002",
status = PentestStatus.REPORTED,
findingIds = "",
commentIds = ""
)
private fun getPentests() = listOf(
pentestOne.toPentestResponseBody(),
pentestTwo.toPentestResponseBody()
)
}
private fun persistBasicTestScenario() {
// setup test data
val pentestOne = Pentest(
id = "9c8af320-f608-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING,
title = "Search engine discovery/reconnaissance",
refNumber = "OTG-INFO-001",
status = PentestStatus.NOT_STARTED,
findingIds = "",
commentIds = ""
)
val pentestTwo = Pentest(
id = "43fbc63c-f624-11ec-b939-0242ac120002",
projectId = "d2e126ba-f608-11ec-b939-0242ac120002",
category = PentestCategory.INFORMATION_GATHERING,
title = "Fingerprint Web Server",
refNumber = "OTG-INFO-002",
status = PentestStatus.REPORTED,
findingIds = "",
commentIds = ""
)
val pentestThree = Pentest(
id = "74eae112-f62c-11ec-b939-0242ac120002",
projectId = "6fad3474-fc29-49f9-bd37-e039e9e60c18",
category = PentestCategory.AUTHENTICATION_TESTING,
title = "Testing for Credentials Transported over an Encrypted Channel",
refNumber = "OTG-AUTHN-001",
status = PentestStatus.CHECKED,
findingIds = "",
commentIds = ""
)
// persist test data in database
mongoTemplate.save(PentestEntity(pentestOne))
mongoTemplate.save(PentestEntity(pentestTwo))
mongoTemplate.save(PentestEntity(pentestThree))
}
private fun configureAdminToken() {
tokenAdmin = getAccessToken("test_admin", "test", "c4po_local", "c4po_realm_local")
}
private fun cleanUp() {
mongoTemplate.findAllAndRemove(Query(), PentestEntity::class.java)
tokenAdmin = "n/a"
}
}

View File

@ -154,7 +154,8 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() {
inner class DeleteProject { inner class DeleteProject {
@Test @Test
fun deleteProject() { fun deleteProject() {
webTestClient.delete().uri("/projects/${project.id}") val id = project.id
webTestClient.delete().uri("/projects/{id}", id)
.header("Authorization", "Bearer $tokenAdmin") .header("Authorization", "Bearer $tokenAdmin")
.exchange() .exchange()
.expectStatus().isOk .expectStatus().isOk
@ -172,6 +173,9 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() {
Preprocessors.preprocessResponse( Preprocessors.preprocessResponse(
Preprocessors.prettyPrint() Preprocessors.prettyPrint()
), ),
RequestDocumentation.relaxedPathParameters(
RequestDocumentation.parameterWithName("id").description("The id of the project you want to delete")
),
PayloadDocumentation.relaxedResponseFields( PayloadDocumentation.relaxedResponseFields(
PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING) PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING)
.description("The id of the deleted project") .description("The id of the deleted project")