diff --git a/core-it/src/main/kotlin/bar/examples/it/spring/multipleendpoints/MultipleEndpointsApplication.kt b/core-it/src/main/kotlin/bar/examples/it/spring/multipleendpoints/MultipleEndpointsApplication.kt new file mode 100644 index 0000000000..e42f0831ec --- /dev/null +++ b/core-it/src/main/kotlin/bar/examples/it/spring/multipleendpoints/MultipleEndpointsApplication.kt @@ -0,0 +1,251 @@ +package bar.examples.it.spring.multipleendpoints + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api"]) +@RestController +open class MultipleEndpointsApplication { + + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(MultipleEndpointsApplication::class.java, *args) + } + } + + + // GET methods + + /** + * Get endpoint 1 with identifier endpointIdentifier, returns 200 as response. + */ + @GetMapping("/endpoint1/{endpointIdentifier}") + open fun getByIdEndpoint1(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(201).body("endpoint1_GET : $endpointIdentifier") + } + + /** + * Get endpoint 1 with the given status code as the response. + */ + @GetMapping("/endpoint1/setStatus/{status}") + open fun getResponseWithGivenStatusEndpoint1(@PathVariable status: Int) : ResponseEntity { + + return ResponseEntity.status(status).body("endpoint1_SET_STATUS") + } + + /** + * Get endpoint 2 with identifier endpointIdentifier, returns 201 as response. + */ + @GetMapping("/endpoint2/{endpointIdentifier}") + open fun getByIdEndpoint2(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(202).body("endpoint2_GET : $endpointIdentifier") + } + + /** + * Get endpoint 2 with the given status code as the response. + */ + @GetMapping("/endpoint2/setStatus/{status}") + open fun getResponseWithGivenStatusEndpoint2(@PathVariable status: Int) : ResponseEntity { + + return ResponseEntity.status(status).body("endpoint2_SET_STATUS") + } + + /** + * Get endpoint 3 with identifier endpointIdentifier, returns 202 as response. + */ + @GetMapping("/endpoint3/{endpointIdentifier}") + open fun getByIdEndpoint3(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(203).body("endpoint3_GET : $endpointIdentifier") + } + + /** + * Get endpoint 3 with the given status code as the response. + */ + @GetMapping("/endpoint3/setStatus/{status}") + open fun getResponseWithGivenStatusEndpoint3(@PathVariable status: Int) : ResponseEntity { + + return ResponseEntity.status(status).body("endpoint3_SET_STATUS") + } + + /** + * Get endpoint 4 with identifier endpointIdentifier, returns 203 as response. + */ + @GetMapping("/endpoint4/{endpointIdentifier}") + open fun getByIdEndpoint4(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(204).body("endpoint4_GET : $endpointIdentifier") + } + + /** + * Get endpoint 4 with the given status code as the response. + */ + @GetMapping("/endpoint4/setStatus/{status}") + open fun getResponseWithGivenStatusEndpoint4(@PathVariable status: Int) : ResponseEntity { + + return ResponseEntity.status(status).body("endpoint4_SET_STATUS") + } + + /** + * Get endpoint 5 with identifier endpointIdentifier, returns 204 as response. + */ + @GetMapping("/endpoint5/{endpointIdentifier}") + open fun getByIdEndpoint5(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(205).body("endpoint5_GET : $endpointIdentifier") + } + + /** + * Get endpoint 5 with the given status code as the response. + */ + @GetMapping("/endpoint5/setStatus/{status}") + open fun getResponseWithGivenStatusEndpoint5(@PathVariable status: Int) : ResponseEntity { + + return ResponseEntity.status(status).body("endpoint5_SET_STATUS") + } + + /** + * POST endpoint 1, returns 301 + */ + @PostMapping("/endpoint1") + open fun postEndpoint1() : ResponseEntity { + + return ResponseEntity.status(301).body("endpoint1_POST") + } + + /** + * POST endpoint 2, returns 302 + */ + @PostMapping("/endpoint2") + open fun postEndpoint2() : ResponseEntity { + + return ResponseEntity.status(302).body("endpoint2_POST") + } + + /** + * POST endpoint 3, returns 303 + */ + @PostMapping("/endpoint3") + open fun postEndpoint3() : ResponseEntity { + + return ResponseEntity.status(303).body("endpoint3_POST") + } + + /** + * POST endpoint 4, returns 304 + */ + @PostMapping("/endpoint4") + open fun postEndpoint4() : ResponseEntity { + + return ResponseEntity.status(304).body("endpoint4_POST") + } + + /** + * POST endpoint 5, returns 305 + */ + @PostMapping("/endpoint5") + open fun postEndpoint5() : ResponseEntity { + + return ResponseEntity.status(305).body("endpoint5_POST") + } + + /** + * PUT endpoint 1, returns 401 + */ + @PutMapping("/endpoint1/{endpointIdentifier}") + open fun putByIdEndpoint1(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(401).body("endpoint1_PUT : $endpointIdentifier") + } + + /** + * PUT endpoint 2, returns 402 + */ + @PutMapping("/endpoint2/{endpointIdentifier}") + open fun putByIdEndpoint2(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(402).body("endpoint2_PUT : $endpointIdentifier") + } + + /** + * PUT endpoint 3, returns 403 + */ + @PutMapping("/endpoint3/{endpointIdentifier}") + open fun putByIdEndpoint3(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(403).body("endpoint3_PUT : $endpointIdentifier") + } + + /** + * PUT endpoint 4, returns 404 + */ + @PutMapping("/endpoint4/{endpointIdentifier}") + open fun putByIdEndpoint4(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(404).body("endpoint4_PUT : $endpointIdentifier") + } + + /** + * PUT endpoint 5, returns 405 + */ + @PutMapping("/endpoint5/{endpointIdentifier}") + open fun putByIdEndpoint5(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(405).body("endpoint5_PUT : $endpointIdentifier") + } + + /** + * DELETE endpoint 1, returns 501 + */ + @DeleteMapping("/endpoint1/{endpointIdentifier}") + open fun deleteByIdEndpoint1(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(501).body("endpoint1_DELETE : $endpointIdentifier") + } + + /** + * DELETE endpoint 2, returns 502 + */ + @DeleteMapping("/endpoint2/{endpointIdentifier}") + open fun deleteByIdEndpoint2(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(502).body("endpoint2_DELETE : $endpointIdentifier") + } + + /** + * DELETE endpoint 3, returns 503 + */ + @DeleteMapping("/endpoint3/{endpointIdentifier}") + open fun deleteByIdEndpoint3(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(503).body("endpoint3_DELETE : $endpointIdentifier") + } + + /** + * DELETE endpoint 4, returns 504 + */ + @DeleteMapping("/endpoint4/{endpointIdentifier}") + open fun deleteByIdEndpoint4(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(504).body("endpoint4_DELETE : $endpointIdentifier") + } + + /** + * DELETE endpoint 5, returns 505 + */ + @DeleteMapping("/endpoint5/{endpointIdentifier}") + open fun deleteByIdEndpoint5(@PathVariable endpointIdentifier: Int) : ResponseEntity { + + return ResponseEntity.status(505).body("endpoint5_DELETE : $endpointIdentifier") + } + + +} \ No newline at end of file diff --git a/core-it/src/test/kotlin/bar/examples/it/spring/multipleendpoints/MultipleEndpointsController.kt b/core-it/src/test/kotlin/bar/examples/it/spring/multipleendpoints/MultipleEndpointsController.kt new file mode 100644 index 0000000000..87fd030504 --- /dev/null +++ b/core-it/src/test/kotlin/bar/examples/it/spring/multipleendpoints/MultipleEndpointsController.kt @@ -0,0 +1,5 @@ +package bar.examples.it.spring.multipleendpoints + +import bar.examples.it.spring.SpringController + +class MultipleEndpointsController : SpringController(MultipleEndpointsApplication::class.java) \ No newline at end of file diff --git a/core-it/src/test/kotlin/org/evomaster/core/problem/rest/selectorutils/RestIndividualSelectorUtilsPathStatusTest.kt b/core-it/src/test/kotlin/org/evomaster/core/problem/rest/selectorutils/RestIndividualSelectorUtilsPathStatusTest.kt index 6c34662a22..9c7d5b87c8 100644 --- a/core-it/src/test/kotlin/org/evomaster/core/problem/rest/selectorutils/RestIndividualSelectorUtilsPathStatusTest.kt +++ b/core-it/src/test/kotlin/org/evomaster/core/problem/rest/selectorutils/RestIndividualSelectorUtilsPathStatusTest.kt @@ -1,11 +1,9 @@ package org.evomaster.core.problem.rest.selectorutils import bar.examples.it.spring.pathstatus.PathStatusController -import org.evomaster.core.problem.rest.HttpVerb -import org.evomaster.core.problem.rest.IntegrationTestRestBase -import org.evomaster.core.problem.rest.RestIndividualSelectorUtils -import org.evomaster.core.problem.rest.RestPath +import org.evomaster.core.problem.rest.* import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test @@ -19,7 +17,6 @@ class RestIndividualSelectorUtilsPathStatusTest : IntegrationTestRestBase(){ } } - @Test fun testPathStatus(){ @@ -45,6 +42,67 @@ class RestIndividualSelectorUtilsPathStatusTest : IntegrationTestRestBase(){ val r1 = RestIndividualSelectorUtils.findIndividuals(individuals, HttpVerb.GET, byStatus, 500) assertEquals(0, r1.size) + + val r2 = RestIndividualSelectorUtils.findIndividuals(individuals, HttpVerb.GET, others, 200) + assertEquals(2, r2.size) + } + + @Test + fun testIndex(){ + + val pirTest = getPirToRest() + + val byStatus = RestPath("/api/pathstatus/byStatus/{status}") + val others = RestPath("/api/pathstatus/others/{x}") + + val s200 = pirTest.fromVerbPath("get", "/api/pathstatus/byStatus/200")!! + val s400 = pirTest.fromVerbPath("get", "/api/pathstatus/byStatus/400")!! + val o200 = pirTest.fromVerbPath("get", "/api/pathstatus/others/200")!! + + val x = createIndividual(listOf(s200,s400,o200,s200.copy() as RestCallAction)) + + assertEquals(2, x.individual.getActionIndex(HttpVerb.GET, others)) + assertTrue(x.individual.getActionIndex(HttpVerb.POST, others) < 0) + + assertEquals(0, x.individual.getActionIndex(HttpVerb.GET, byStatus)) + } + + + @Test + fun testFindAction() { + + val pirTest = getPirToRest() + + val others = RestPath("/api/pathstatus/others/{x}") + + // create 10 actions + val action1 = pirTest.fromVerbPath("get", "/api/pathstatus/byStatus/200")!! + val action2 = pirTest.fromVerbPath("get", "/api/pathstatus/byStatus/201")!! + val action3 = pirTest.fromVerbPath("get", "/api/pathstatus/byStatus/202")!! + val action4 = pirTest.fromVerbPath("get", "/api/pathstatus/byStatus/204")!! + val action5 = pirTest.fromVerbPath("get", "/api/pathstatus/byStatus/301")!! + val action6 = pirTest.fromVerbPath("get", "/api/pathstatus/byStatus/302")!! + val action7 = pirTest.fromVerbPath("get", "/api/pathstatus/others/304")!! + val action8 = pirTest.fromVerbPath("get", "/api/pathstatus/byStatus/401")!! + val action9 = pirTest.fromVerbPath("get", "/api/pathstatus/others/402")!! + val action10 = pirTest.fromVerbPath("get", "/api/pathstatus/byStatus/404")!! + + val createdIndividualFirst = createIndividual(listOf(action1, action2, action3, action4, action5)) + val createdIndividualSecond = createIndividual(listOf(action6, action7, action8, action9, action10)) + + val listOfIndividuals = listOf(createdIndividualFirst, createdIndividualSecond) + + // find action with GET request + val actionWithGet = RestIndividualSelectorUtils.findAction(listOfIndividuals, HttpVerb.GET) as RestCallAction + assertTrue(actionWithGet.verb == HttpVerb.GET) + + // find action with get request having path as others and status code as 200 + val eval = RestIndividualSelectorUtils.findEvaluatedAction(listOfIndividuals, HttpVerb.GET, others, 200 ) + val actionWithPathOthers = eval!!.action as RestCallAction + val actionWithPathOthersResult = eval.result as RestCallResult + + assertTrue(actionWithPathOthers.verb == HttpVerb.GET) + assertTrue(actionWithPathOthersResult.getStatusCode() == 200) } } \ No newline at end of file diff --git a/core-it/src/test/kotlin/org/evomaster/core/problem/rest/selectorutils/RestIndividualSelectorUtilsTest.kt b/core-it/src/test/kotlin/org/evomaster/core/problem/rest/selectorutils/RestIndividualSelectorUtilsTest.kt new file mode 100644 index 0000000000..9da6c32ee1 --- /dev/null +++ b/core-it/src/test/kotlin/org/evomaster/core/problem/rest/selectorutils/RestIndividualSelectorUtilsTest.kt @@ -0,0 +1,514 @@ +package org.evomaster.core.problem.rest.selectorutils + +import bar.examples.it.spring.multipleendpoints.MultipleEndpointsController +import org.evomaster.core.problem.httpws.auth.AuthenticationHeader +import org.evomaster.core.problem.httpws.auth.HttpWsAuthenticationInfo +import org.evomaster.core.problem.rest.* +import org.evomaster.core.search.EvaluatedIndividual +import org.junit.Assert +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class RestIndividualSelectorUtilsTest : IntegrationTestRestBase() { + + /** + * Tests for methods inside RestIndividualSelectorUtils.kt + * + * 1. findAction based on VERB only DONE + * 2. findAction based on PATH only + * 3. findAction based on STATUS only + * 4. findAction based on STATUS GROUP only + * 5. findAction based on if the action is authenticated or not + * 6. findAction based on VERB and PATH + * 7. findAction based on VERB and STATUS + * 8. findAction based on PATH and STATUS + * 9. findAction based on PATH and STATUS GROUP + * 10. findAction based on VERB, PATH, and STATUS + * 11. findAction based on VERB, PATH, and STATUS GROUP + * 12. findAction authenticated actions + * 13. findAction non-authenticated actions + * + * 14. findEvaluated action for all above (13 test cases) + * 15. findIndividuals with same 13 test cases. (13 test cases) + * 16. test for get all action definitions + * 17. 4 tests for findIndividualWithEndpointCreationForResource + * 18. getIndexOfAction existing action + * 19. getIndexOfAction non-existing action + * 20. sliceAllCallsAfterIndividual start from beginning + * 21. sliceAllCallsAfterIndividual startFromEnd + * 22. sliceAllCallsAfterIndividual from middle + * 23. sliceAllCallsAfterIndividual from index smaller than 0 + * 24. sliceAllCallsAfterIndividual from index greater than the number of actions. + */ + + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(MultipleEndpointsController()) + + } + } + + // a sample set of 4 individuals needed for tests + private fun initializeIndividuals(individual1WithoutGet : Boolean = false) : List> { + + val pirTest = getPirToRest() + + // first individual contains endpoint 2 GET, endpoint 1 POST, endpoint 3 PUT, endpoint 4 DELETE + // and endpoint 5 authenticated GET with the status code + val action1Ind1 = pirTest.fromVerbPath("GET", "/api/endpoint2/1000")!! + val action2Ind1 = pirTest.fromVerbPath("POST", "/api/endpoint1")!! + val action3Ind1 = pirTest.fromVerbPath("PUT", "/api/endpoint3/2000")!! + val action4Ind1 = pirTest.fromVerbPath("DELETE", "/api/endpoint4/1005")!! + val action5Ind1 = pirTest.fromVerbPath("GET", "/api/endpoint5/setStatus/402")!! + + val individual1 = if (!individual1WithoutGet) { + createIndividual(listOf(action1Ind1, action2Ind1, action3Ind1, action4Ind1, action5Ind1)) + } else { + createIndividual(listOf(action2Ind1, action3Ind1, action4Ind1)) + } + + // second individual contains endpoint 1 GET, endpoint 2 GET with status code, endpoint 3 POST, endpoint 4 PUT + // and endpoint 5 authenticated DELETE + val action1Ind2 = pirTest.fromVerbPath("GET", "/api/endpoint1/5000")!! + val action2Ind2 = pirTest.fromVerbPath("POST", "/api/endpoint3")!! + val action3Ind2 = pirTest.fromVerbPath("PUT", "/api/endpoint4/6000")!! + val action4Ind2 = pirTest.fromVerbPath("DELETE", "/api/endpoint5/8000")!! + action4Ind2.auth = HttpWsAuthenticationInfo("action4Ind2", + listOf(AuthenticationHeader("name", "authentication")), + null, false) + val action5Ind2 = pirTest.fromVerbPath("GET", "/api/endpoint2/setStatus/403")!! + + val individual2 = createIndividual(listOf(action1Ind2, action2Ind2, action3Ind2, action4Ind2, action5Ind2)) + + + // third individual contains endpoint 1 POST, endpoint 2 PUT, endpoint 3 GET with authenticated, endpoint 4 + // GET with status code authenticated, endpoint 5 DELETE + val action1Ind3 = pirTest.fromVerbPath("GET", "/api/endpoint3/8500")!! + val action2Ind3 = pirTest.fromVerbPath("POST", "/api/endpoint1")!! + val action3Ind3 = pirTest.fromVerbPath("PUT", "/api/endpoint2/9000")!! + val action4Ind3 = pirTest.fromVerbPath("DELETE", "/api/endpoint5/8700")!! + action1Ind3.auth = HttpWsAuthenticationInfo("action1Ind3", + listOf(AuthenticationHeader("name", "authentication")), + null, false) + val action5Ind3 = pirTest.fromVerbPath("GET", "/api/endpoint4/setStatus/415")!! + + val individual3 = createIndividual(listOf(action1Ind3, action2Ind3, action3Ind3, action4Ind3, action5Ind3)) + + // fourth individual contains endpoint 5 GET with authentication, endpoint 4 POST, endpoint 3 with DELETE + // endpoint 2 with PUT, endpoint 1 with GET with status code. + val action1Ind4 = pirTest.fromVerbPath("GET", "/api/endpoint5/8800")!! + val action2Ind4 = pirTest.fromVerbPath("POST", "/api/endpoint4")!! + val action3Ind4 = pirTest.fromVerbPath("PUT", "/api/endpoint2/9600")!! + val action4Ind4 = pirTest.fromVerbPath("DELETE", "/api/endpoint3/1700")!! + action1Ind4.auth = HttpWsAuthenticationInfo("action1Ind3", + listOf(AuthenticationHeader("name", "authentication")), + null, false) + val action5Ind4 = pirTest.fromVerbPath("GET", "/api/endpoint4/setStatus/404")!! + + val individual4 = createIndividual(listOf(action1Ind4, action2Ind4, action3Ind4, action4Ind4, action5Ind4)) + + return listOf(individual1, individual2, individual3, individual4) + } + + @Test + fun testFindActionVerbOnly() { + + val listOfIndividuals = initializeIndividuals() + + // in the beginning all the individuals are selected since each has GET request + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals(listOfIndividuals, HttpVerb.GET) + + // All the 4 individuals are selected since each has GET request + Assertions.assertTrue(selectedIndividuals.size == 4) + + // now remove individual1 from the list + + // select create individuals without individual1 having GET + val listOfIndividualsSecond = initializeIndividuals(true) + + val selectedIndividualsSecond = RestIndividualSelectorUtils.findIndividuals(listOfIndividualsSecond, HttpVerb.GET) + + // Now 3 individuals are selected since each has GET request + Assertions.assertTrue(selectedIndividualsSecond.size == 3) + + } + + @Test + fun testFindActionPathOnly() { + + val listOfIndividuals = initializeIndividuals() + + // in the beginning all the individuals are selected since each has GET request + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals(listOfIndividuals, null, + RestPath("/api/endpoint5/setStatus/{status}")) + + // Only 1 individual has a request with the path "/api/endpoint5/setStatus/{status}" + Assertions.assertTrue(selectedIndividuals.size == 1) + + // now find all individuals with endpoint /api/endpoint2, this is none of the individuals + val secondSelectedIndividuals = RestIndividualSelectorUtils.findIndividuals(listOfIndividuals, null, + RestPath("/api/endpoint2")) + + // Only 1 individual has a request with the path "/api/endpoint2" + Assertions.assertTrue(secondSelectedIndividuals.isEmpty()) + + // now find all individuals with endpoint /api/endpoint3, this is one of the individuals + val thirdSelectedIndividuals = RestIndividualSelectorUtils.findIndividuals(listOfIndividuals, null, + RestPath("/api/endpoint3")) + + // Only 1 individual has a request with the path "/api/endpoint3", + Assertions.assertTrue(thirdSelectedIndividuals.size == 1) + } + + @Test + fun testFindActionStatusOnly() { + + val listOfIndividuals = initializeIndividuals() + + // in the beginning all the individuals are selected since each has GET request + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals(listOfIndividuals, null, + null, 201) + + // Only 1 individual has a request with the path "/api/endpoint5/setStatus/{status}" + Assertions.assertTrue(selectedIndividuals.size == 1) + + // now find all individuals with endpoint /api/endpoint2, this is none of the individuals + val secondSelectedIndividuals = RestIndividualSelectorUtils.findIndividuals(listOfIndividuals, null, + null, 415) + + // Only 1 individual has a request with the path "/api/endpoint2" + Assertions.assertTrue(secondSelectedIndividuals.size == 1) + + // now find all individuals with endpoint /api/endpoint3, this is one of the individuals + val thirdSelectedIndividuals = RestIndividualSelectorUtils.findIndividuals(listOfIndividuals, null, + null, 499) + + // Only 1 individual has a request with the path "/api/endpoint3", + Assertions.assertTrue(thirdSelectedIndividuals.isEmpty()) + } + + @Test + fun testFindActionStatusGroupOnly() { + + val listOfIndividuals = initializeIndividuals() + + // in the beginning all the individuals are selected since each has GET request + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals(listOfIndividuals, null, + null, null, StatusGroup.G_2xx) + + // Only 1 individual has a request with the path "/api/endpoint5/setStatus/{status}" + Assertions.assertTrue(selectedIndividuals.size == 4) + + // now find all individuals with endpoint /api/endpoint2, this is none of the individuals + val secondSelectedIndividuals = RestIndividualSelectorUtils.findIndividuals(listOfIndividuals, null, + null, null, StatusGroup.G_4xx) + + // Only 1 individual has a request with the path "/api/endpoint2" + Assertions.assertTrue(secondSelectedIndividuals.size == 4) + + // now find all individuals with endpoint /api/endpoint3, this is one of the individuals + val thirdSelectedIndividuals = RestIndividualSelectorUtils.findIndividuals(listOfIndividuals, null, + null, null, StatusGroup.G_5xx) + + // Only 1 individual has a request with the path "/api/endpoint3", + Assertions.assertTrue(thirdSelectedIndividuals.size == 4) + } + + @Test + fun testFindActionAuthenticatedActionsOnly() { + + val listOfIndividuals = initializeIndividuals() + + // in the beginning all the individuals are selected since each has GET request + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals( + listOfIndividuals, null, + null, null, null, true + ) + + // Only 1 individual has a request with the path "/api/endpoint5/setStatus/{status}" + Assertions.assertTrue(selectedIndividuals.size == 3) + } + + @Test + fun testFindActionGroupsBasedOnVerbAndPath() { + + val listOfIndividuals = initializeIndividuals() + + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals( + listOfIndividuals, HttpVerb.DELETE, + RestPath("/api/endpoint5/{endpointIdentifier}") + ) + + Assertions.assertTrue(selectedIndividuals.size == 2) + + val secondSelectedIndividuals = RestIndividualSelectorUtils.findIndividuals( + listOfIndividuals, HttpVerb.POST, + RestPath("/api/endpoint5/{endpointIdentifier}") + ) + + Assertions.assertTrue(secondSelectedIndividuals.isEmpty()) + } + + @Test + fun testFindActionGroupsBasedOnVerbAndStatus() { + + val listOfIndividuals = initializeIndividuals() + + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals( + listOfIndividuals, HttpVerb.DELETE, + null, 201 + ) + + Assertions.assertTrue(selectedIndividuals.isEmpty()) + + val secondSelectedIndividuals = RestIndividualSelectorUtils.findIndividuals( + listOfIndividuals, HttpVerb.POST, + null, 301 + ) + + Assertions.assertTrue(secondSelectedIndividuals.size == 2) + } + + @Test + fun testFindActionGroupsBasedOnPathAndStatus() { + + val listOfIndividuals = initializeIndividuals() + + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals( + listOfIndividuals, null, RestPath("/api/endpoint5/setStatus/{status}"), 402) + + Assertions.assertTrue(selectedIndividuals.size == 1) + + + } + + @Test + fun testFindActionGroupsBasedOnPathAndStatusGroup() { + + val listOfIndividuals = initializeIndividuals() + + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals( + listOfIndividuals, null, RestPath("/api/endpoint3"), null, + StatusGroup.G_3xx) + + Assertions.assertTrue(selectedIndividuals.size == 1) + + } + + @Test + fun testFindActionGroupsBasedOnVerbPathStatus() { + + val listOfIndividuals = initializeIndividuals() + + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals( + listOfIndividuals, HttpVerb.GET, RestPath("/api/endpoint2/setStatus/{status}"), 403) + + Assertions.assertTrue(selectedIndividuals.size == 1) + + } + + @Test + fun testFindActionGroupsBasedOnVerbPathStatusStatusGroup() { + + val listOfIndividuals = initializeIndividuals() + + Assertions.assertThrows(IllegalArgumentException::class.java) { + RestIndividualSelectorUtils.findIndividuals( + listOfIndividuals, HttpVerb.GET, RestPath("/api/endpoint2/setStatus/{status}"), + 403, StatusGroup.G_4xx) + } + + } + + @Test + fun testFindActionGroupsBasedOnVerbPathStatusGroup() { + + val listOfIndividuals = initializeIndividuals() + + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals( + listOfIndividuals, HttpVerb.GET, RestPath("/api/endpoint2/setStatus/{status}"), null, + StatusGroup.G_5xx) + + Assertions.assertTrue(selectedIndividuals.isEmpty()) + + } + + @Test + fun testFindAuthenticatedActions() { + + val listOfIndividuals = initializeIndividuals() + + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals( + listOfIndividuals, null, null, null, + null, true) + + Assertions.assertTrue(selectedIndividuals.size == 3) + + } + + @Test + fun testFindNonAuthenticatedActions() { + + val listOfIndividuals = initializeIndividuals() + + val selectedIndividuals = RestIndividualSelectorUtils.findIndividuals( + listOfIndividuals, null, null, null, + null, false) + + Assertions.assertTrue(selectedIndividuals.size == 4) + + } + + /** + * Find the first evaluated action with POST and /api/endpoint2 + */ + @Test + fun testFindEvaluatedAction() { + + val listOfIndividuals = initializeIndividuals() + + val selectedAction = RestIndividualSelectorUtils.findEvaluatedAction( + listOfIndividuals, HttpVerb.POST, RestPath("/api/endpoint3"), 303 + ) + + Assertions.assertTrue((selectedAction?.action as RestCallAction).verb == HttpVerb.POST) + Assertions.assertTrue((selectedAction?.action as RestCallAction).path == RestPath("/api/endpoint3")) + Assertions.assertTrue((selectedAction?.result as RestCallResult).getStatusCode() == 303) + + } + + /** + * Find the first evaluated action with POST and /api/endpoint2 + */ + @Test + fun testFindEvaluatedActionNonExistent() { + + val listOfIndividuals = initializeIndividuals() + + val selectedAction = RestIndividualSelectorUtils.findEvaluatedAction( + listOfIndividuals, HttpVerb.POST, RestPath("/api/endpoint3"), 404 + ) + + Assertions.assertTrue( selectedAction == null) + + } + + /** + * Test findAction + */ + @Test + fun testFindAction() { + + val listOfIndividuals = initializeIndividuals() + + val selectedAction = RestIndividualSelectorUtils.findAction( + listOfIndividuals, HttpVerb.POST, RestPath("/api/endpoint3"), 303 + ) + + Assertions.assertTrue( (selectedAction?.verb == HttpVerb.POST) ) + Assertions.assertTrue( (selectedAction?.path == RestPath("/api/endpoint3")) ) + + } + + /** + * Test getting indices of actions in individuals. + */ + @Test + fun testGetIndexOfAction() { + + val listOfIndividuals = initializeIndividuals() + + val actionIndex = RestIndividualSelectorUtils.getIndexOfAction(listOfIndividuals[0], HttpVerb.POST, + RestPath("/api/endpoint1"), 301) + + Assertions.assertTrue(actionIndex == 1) + + val actionIndexSecond = RestIndividualSelectorUtils.getIndexOfAction(listOfIndividuals[2], HttpVerb.GET, + RestPath("/api/endpoint4/setStatus/{status}"), 415) + + Assertions.assertTrue(actionIndexSecond == 4) + + } + + /** + * Test getting indices of actions in individuals. + */ + @Test + fun testGetIndexOfActionNonExistent() { + + val listOfIndividuals = initializeIndividuals() + + val actionIndex = RestIndividualSelectorUtils.getIndexOfAction(listOfIndividuals[0], HttpVerb.POST, + RestPath("/api/endpoint1"), 501) + + Assertions.assertTrue(actionIndex == -1) + + val actionIndexSecond = RestIndividualSelectorUtils.getIndexOfAction(listOfIndividuals[2], HttpVerb.GET, + RestPath("/api/endpoint4/setStatus/{status}"), 417) + + Assertions.assertTrue(actionIndexSecond == -1) + + } + + @Test + fun testSliceAllCallsInIndividualAfterAction() { + val listOfIndividuals = initializeIndividuals() + + val newIndividual = RestIndividualSelectorUtils.sliceAllCallsInIndividualAfterAction( + listOfIndividuals[1].individual, 2) + + // now check actions in the newIndividual + Assertions.assertTrue(newIndividual.size() == 3) + + // check actions of slices + Assertions.assertTrue(newIndividual.seeMainExecutableActions()[0].verb == HttpVerb.GET) + Assertions.assertTrue(newIndividual.seeMainExecutableActions()[1].verb == HttpVerb.POST) + Assertions.assertTrue(newIndividual.seeMainExecutableActions()[2].verb == HttpVerb.PUT) + + // check paths of slices + Assertions.assertTrue(newIndividual.seeMainExecutableActions()[0].path == RestPath("/api/endpoint1/{endpointIdentifier}")) + Assertions.assertTrue(newIndividual.seeMainExecutableActions()[1].path == RestPath("/api/endpoint3") ) + Assertions.assertTrue(newIndividual.seeMainExecutableActions()[2].path == RestPath("/api/endpoint4/{endpointIdentifier}")) + + + // now try from index 1 + val newIndividualSecond = RestIndividualSelectorUtils.sliceAllCallsInIndividualAfterAction( + listOfIndividuals[1].individual, 1) + + // now check actions in the newIndividual + Assertions.assertTrue(newIndividualSecond.size() == 2) + + // check actions of slices + Assertions.assertTrue(newIndividualSecond.seeMainExecutableActions()[0].verb == HttpVerb.GET) + Assertions.assertTrue(newIndividualSecond.seeMainExecutableActions()[1].verb == HttpVerb.POST) + + + // check paths of slices + Assertions.assertTrue(newIndividualSecond.seeMainExecutableActions()[0].path == RestPath("/api/endpoint1/{endpointIdentifier}")) + Assertions.assertTrue(newIndividualSecond.seeMainExecutableActions()[1].path == RestPath("/api/endpoint3") ) + + } + + @Test + fun testSliceAllCallsInIndividualAfterActionNonValidIndices() { + val listOfIndividuals = initializeIndividuals() + + Assert.assertThrows(IllegalArgumentException::class.java) { + RestIndividualSelectorUtils.sliceAllCallsInIndividualAfterAction( + listOfIndividuals[1].individual, 10) + } + + Assert.assertThrows(IllegalArgumentException::class.java) { + RestIndividualSelectorUtils.sliceAllCallsInIndividualAfterAction( + listOfIndividuals[1].individual, -10) + } + + } + + + +} \ No newline at end of file diff --git a/core-it/src/test/kotlin/org/evomaster/core/problem/rest/service/securitytest/SecurityRestDeleteTest.kt b/core-it/src/test/kotlin/org/evomaster/core/problem/rest/service/securitytest/SecurityRestDeleteTest.kt new file mode 100644 index 0000000000..65a8a555f3 --- /dev/null +++ b/core-it/src/test/kotlin/org/evomaster/core/problem/rest/service/securitytest/SecurityRestDeleteTest.kt @@ -0,0 +1,27 @@ +package org.evomaster.core.problem.rest.service.securitytest + +import bar.examples.it.spring.pathstatus.PathStatusController +import org.evomaster.core.problem.rest.IntegrationTestRestBase +import org.junit.Ignore +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +class SecurityRestDeleteTest : IntegrationTestRestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(PathStatusController()) + } + } + + @Disabled + @Test + fun testDeletePut(){ + + //TODO + val pirTest = getPirToRest() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/enterprise/EnterpriseIndividual.kt b/core/src/main/kotlin/org/evomaster/core/problem/enterprise/EnterpriseIndividual.kt index 7749a27e87..9a2cb6e4c5 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/enterprise/EnterpriseIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/enterprise/EnterpriseIndividual.kt @@ -32,7 +32,8 @@ import java.util.* * per action, and not here in the initialization phase. */ abstract class EnterpriseIndividual( - val sampleType: SampleType, + //see https://discuss.kotlinlang.org/t/private-setter-for-var-in-primary-constructor/3640/11 + private var sampleTypeField: SampleType, /** * a tracked operator to manipulate the individual (nullable) */ @@ -123,6 +124,18 @@ abstract class EnterpriseIndividual( } } + val sampleType get() = sampleTypeField + + /** + * This should never happen directly during the search. + * However, we might manually create new individuals by modifying and copying existing individuals. + * In those cases it simple to modify the sample directly, instead of re-building with same actions + * (which actually could be a possibility...). + */ + fun modifySampleType(x: SampleType){ + sampleTypeField = x + } + /** * a list of db actions for its Initialization */ diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/RestIndividual.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/RestIndividual.kt index 88c7c0984d..f814ef2e10 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/RestIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/RestIndividual.kt @@ -404,4 +404,18 @@ class RestIndividual( override fun seeMainExecutableActions(): List { return super.seeMainExecutableActions() as List } + + /** + * Finds the first index of a main REST action with a given verb and path among actions in the individual. + * + * @return negative value if not found + */ + fun getActionIndex( + actionVerb: HttpVerb, + path: RestPath + ) : Int { + return seeMainExecutableActions().indexOfFirst { + it.verb == actionVerb && it.path.isEquivalent(path) + } + } } diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/RestIndividualSelectorUtils.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/RestIndividualSelectorUtils.kt index 431be6559a..dcd55d64ae 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/RestIndividualSelectorUtils.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/RestIndividualSelectorUtils.kt @@ -1,13 +1,10 @@ package org.evomaster.core.problem.rest -import org.evomaster.client.java.controller.api.dto.auth.AuthenticationDto -import org.evomaster.core.problem.enterprise.SampleType -import org.evomaster.core.problem.httpws.auth.HttpWsAuthenticationInfo -import org.evomaster.core.problem.rest.param.PathParam + +import org.evomaster.core.problem.enterprise.auth.NoAuth +import org.evomaster.core.search.EvaluatedAction import org.evomaster.core.search.EvaluatedIndividual -import org.evomaster.core.search.StructuralElement -import org.evomaster.core.search.action.ActionResult -import org.evomaster.core.search.gene.string.StringGene + /** * Utility functions to select one or more REST Individual from a group, based on different criteria. @@ -15,582 +12,243 @@ import org.evomaster.core.search.gene.string.StringGene */ object RestIndividualSelectorUtils { - //FIXME this needs to be cleaned-up - - /** - * Finds the first index of a main REST action with a given verb and path among actions in the individual. - * - * @return negative value if not found + * check a given action for the given conditions */ - fun getActionIndexFromIndividual( - individual: RestIndividual, - actionVerb: HttpVerb, - path: RestPath - ) : Int { - return individual.seeMainExecutableActions().indexOfFirst { - it.verb == actionVerb && it.path.isEquivalent(path) - } - } - - fun findActionFromIndividual( - individual: EvaluatedIndividual, - actionVerb: HttpVerb, - actionStatus: Int - ): RestCallAction? { + private fun checkIfActionSatisfiesConditions(act: EvaluatedAction, + verb: HttpVerb? = null, + path: RestPath? = null, + status: Int? = null, + statusGroup: StatusGroup? = null, + mustBeAuthenticated : Boolean = false + ) : Boolean { - individual.evaluatedMainActions().forEach { currentAct -> + // get actions and results first + val action = act.action as RestCallAction + val resultOfAction = act.result as RestCallResult - val act = currentAct.action as RestCallAction - val res = currentAct.result as RestCallResult + // now check all conditions to see if any of them make the action not be included - if ( (res.getStatusCode() == actionStatus) && act.verb == actionVerb) { - return act - } + // verb + if (verb != null && action.verb != verb) { + return false } - return null - } - - fun old_findIndividuals( - individuals: List>, - verb: HttpVerb, - statusGroup: String - ):List> { - - val individualsList = mutableListOf>() - - individuals.forEach { ind -> - val actions = ind.evaluatedMainActions() - - val successfulDeleteContained = false - - for (a in actions) { - - val act = a.action as RestCallAction - val res = a.result as RestCallResult - - - if ( (res.getStatusCode().toString().first() == statusGroup.first()) - && act.verb == verb - ) { - if (!individualsList.contains(ind)) { - individualsList.add(ind) - } - } - - } + // path + if(path != null && !action.path.isEquivalent(path)) { + return false } - return individualsList - } - - /* - Find individuals containing a certain action and STATUS - */ - fun getIndividualsWithActionAndStatus( - individualsInSolution: List>, - verb: HttpVerb, - statusCode: Int - ):List> { - - val individualsList = mutableListOf>() - - individualsInSolution.forEach { ind -> - val actions = ind.evaluatedMainActions() - - val successfulDeleteContained = false - - for (a in actions) { - - val act = a.action as RestCallAction - val res = a.result as RestCallResult - - - if ( (res.getStatusCode() == statusCode) && act.verb == verb) { - - if (!individualsList.contains(ind)) { - individualsList.add(ind) - } - } - } + // status + if(status!=null && resultOfAction.getStatusCode() != status){ + return false } - return individualsList - } - - /* - This method identifies a specific action in an individual. It is used to transform an individual containing - one action to RestCallAction - */ - fun findActionFromIndividuals(individualList: List>, - verb: HttpVerb, path: RestPath): RestCallAction? { - - // search for RESTCall action in an individual. - var foundRestAction : RestCallAction? = null - - for (ind : EvaluatedIndividual in individualList) { - - for (act : RestCallAction in ind.individual.seeMainExecutableActions()) { - - if (act.verb == verb && act.path == path) { - foundRestAction = act - } - - } + if(statusGroup != null && !statusGroup.isInGroup(resultOfAction.getStatusCode())){ + return false + } + // authenticated or not + if(mustBeAuthenticated && action.auth is NoAuth){ + return false } - // the action that has been found - return foundRestAction + return true } - fun findIndividuals( - individuals: List>, - verb: HttpVerb, - path: RestPath, - statusCode: Int - ): List> { - - return individuals.filter { ind -> - ind.evaluatedMainActions().any{ea -> - val a = ea.action as RestCallAction - val r = ea.result as RestCallResult + fun findAction( + individualsInSolution: List>, + verb: HttpVerb? = null, + path: RestPath? = null, + status: Int? = null, + statusGroup: StatusGroup? = null, + authenticated: Boolean = false + ): RestCallAction? { - a.verb == verb && a.path.isEquivalent(path) && r.getStatusCode() == statusCode - } - } + return findEvaluatedAction(individualsInSolution,verb,path,status,statusGroup,authenticated) + ?.action as RestCallAction } - fun getIndividualsWithActionAndStatusGroup( - individuals: List>, - verb: HttpVerb, - path: RestPath, - statusGroup: String - ): List> { - return individuals.filter { ind -> - ind.evaluatedMainActions().any{ea -> - val a = ea.action as RestCallAction - val r = ea.result as RestCallResult + fun findEvaluatedAction( + individualsInSolution: List>, + verb: HttpVerb? = null, + path: RestPath? = null, + status: Int? = null, + statusGroup: StatusGroup? = null, + authenticated: Boolean = false + ): EvaluatedAction? { - a.verb == verb && a.path.isEquivalent(path) && - r.getStatusCode().toString().first() == statusGroup.first() - } + if(status != null && statusGroup!= null){ + throw IllegalArgumentException("Shouldn't specify both status and status group") } - } - - fun getIndividualsWithAction( - individuals: List>, - verb: HttpVerb, - path: RestPath, - ): List> { - - return individuals.filter { ind -> - ind.evaluatedMainActions().any{ea -> - val a = ea.action as RestCallAction - a.verb == verb && a.path.isEquivalent(path) + individualsInSolution.forEach {ind -> + ind.evaluatedMainActions().forEach { a -> + if(checkIfActionSatisfiesConditions(a, verb, path, status, statusGroup, authenticated)){ + return a + } } } + return null } - fun sliceAllCallsInIndividualAfterAction(individual: EvaluatedIndividual, - action: RestCallAction) : RestIndividual { - - // find the index of the individual - val mainActions = individual.individual.seeMainExecutableActions() - val actIndex = individual.individual.seeMainExecutableActions().indexOf(action) - - var actionList = mutableListOf() - - for (index in 0..mainActions.size) { - if (index <= actIndex) { - actionList.add(mainActions.get(index)) - } - } - - val newIndividual = RestIndividual(actionList, SampleType.SECURITY) - - return newIndividual - - } /** - * Just retrieve the action with a given index - * Precondition: 0 <= index <= number of actions - */ - fun getActionWithIndex(individual: EvaluatedIndividual, actionIndex : Int) : RestCallAction { - - return getActionWithIndexRestIndividual(individual.individual, actionIndex) - - } - - fun getActionWithIndexRestIndividual(individual: RestIndividual, actionIndex : Int) : RestCallAction { - - return individual.seeMainExecutableActions()[actionIndex] - - } - - /* - Function to extract actions and results based on the same resource. It is not used for now but it can be - used in the future. + * Find individuals which contain actions with given parameters, such as verb, path, result. + * If any of those parameters are not given as null, that parameter is used for filtering individuals. */ - private fun extractActionsAndResultsBasedOnSameResource ( - actionListPost : MutableList, - actionListDelete : MutableList - ) : MutableMap { - - val result = mutableMapOf() - - for( actionPost : RestCallAction in actionListPost) { - - for( actionDelete : RestCallAction in actionListDelete) { - - if (actionPost.path == actionDelete.path) { - - - // now check for same values - var actionPostStr : String = actionPost.toString() - var actionDeleteStr : String = actionDelete.toString() - - actionPostStr = actionPostStr.substring(actionPostStr.indexOf(' ')) - - if (actionPostStr.contains('?')) { - actionPostStr = actionPostStr.substring(0,actionPostStr.indexOf('?') ) - } - - actionDeleteStr = actionDeleteStr.substring(actionDeleteStr.indexOf(' ')) - - if (actionDeleteStr.contains('?')) { - actionDeleteStr = actionDeleteStr.substring(0,actionDeleteStr.indexOf('?') ) - } + fun findIndividuals( + individualsInSolution: List>, + verb: HttpVerb? = null, + path: RestPath? = null, + status: Int? = null, + statusGroup: StatusGroup? = null, + authenticated: Boolean = false + ): List> { - if (actionPostStr == actionDeleteStr) { - result[actionPost] = actionDelete - } - } - } + if(status != null && statusGroup!= null){ + throw IllegalArgumentException("Shouldn't specify both status and status group") } - return result - } - - /* - Function to get parameter values of an endpoint. It is not used for now but it can be used in the future. - */ - private fun getPathParameterValuesOfEndpoint (action : RestCallAction): List { - val pathParams = mutableListOf() - - for( item: StructuralElement in action.getViewOfChildren()) { - - if (item::class == PathParam::class) { - pathParams.add(item as PathParam) + return individualsInSolution.filter {ind -> + ind.evaluatedMainActions().any { a -> + checkIfActionSatisfiesConditions(a, verb, path, status, statusGroup, authenticated) } - } - - return pathParams - } - /* - Function to extract actions and results based on properties. It is not used for now but may be used later. + /** + * get all action definitions from the swagger based on the given verb */ - private fun extractActionsAndResultsBasedOnProperties ( - restIndividuals : List>, - actionVerb : HttpVerb, - authenticated : Boolean, - statusCode : Int, - actionList : MutableList, - resultList : MutableList - ) { - - var actions: List - var results: List - - var currentAction : RestCallAction? - var currentResult : ActionResult? - - for (restIndividual : EvaluatedIndividual in restIndividuals) { - - actions = restIndividual.individual.seeMainExecutableActions() - results = restIndividual.seeResults(actions) - - - for (i in actions.indices) { - - currentAction = actions[i] - currentResult = results[i] - - // to retrieve authenticated calls to POST - if (currentAction.verb == actionVerb && currentAction.auth.name == "NoAuth" && !authenticated) { - - val resultStatus = Integer.parseInt(currentResult.getResultValue("STATUS_CODE")) - - if (resultStatus == statusCode) { - actionList.add(currentAction) - resultList.add(currentResult) - } - } - } - } + fun getAllActionDefinitions(actionDefinitions: List, verb: HttpVerb): List { + return actionDefinitions.filter { it.verb == verb } } - /* - * Remove all calls AFTER the given call. + /** + * Given a resource path, we want to find any individual that has a successful create operation for that resource + * + * Assume for example the resource "/users/{id}" + * + * We could search for a + * PUT "/users/id" + * that returns 201 + * + * or a + * POST "/users" + * that returns something in 2xx (not necessarily 201) + * + * @return null if none found */ - // TODO add checking status code as well - private fun sliceIndividual(individual: RestIndividual, verb: HttpVerb, path: RestPath, statusCode: Int) { - - // Find the index of the action - var index = 0 - var found = false - val actions = individual.seeMainExecutableActions() - var currentAction : RestCallAction - - while (!found) { - - currentAction = actions[index] - - if ( currentAction.verb == verb && - currentAction.path == path ) { - found = true - } - else { - index += 1 - } + fun findIndividualWithEndpointCreationForResource( individuals: List>, + resourcePath: RestPath, + mustBeAuthenticated: Boolean + ) : Pair,Endpoint>?{ + + //TODO needs finishing implementation + + // there is not. need to create it based on successful create resources with authenticated user + lateinit var verbUsedForCreation : HttpVerb + // search for create resource for endpoint of DELETE using PUT + lateinit var existingEndpointForCreation : EvaluatedIndividual + + val existingPuts = findIndividuals( + individuals, + HttpVerb.PUT, + resourcePath, + 201, + statusGroup = null, + mustBeAuthenticated + ) + + if(existingPuts.isNotEmpty()){ + return Pair( + existingPuts.sortedBy { it.individual.size() }[0], + Endpoint(HttpVerb.PUT, resourcePath) + ) } - - if (found) { - // delete all calls after the index - for (item in index + 1 until actions.size) { - individual.removeMainExecutableAction(index + 1) - } + // if not, search for a + else { + // TODO since we cannot search for a POST with the same path, we can just return null + return null } + /* - } - - /* - * This method is used to get the endpoint for a given action. - */ - private fun getEndPointFromAction(act: RestCallAction) : String? { - - val listOfPathParams = ArrayList() - - // find the path parameter - recursiveTreeTraversalForFindingInformationForItem(act, "org.evomaster.core.problem.rest.param.PathParam", listOfPathParams) - - if (listOfPathParams.size > 0 ) { - - // starting from the path parameter, find the endpoint - val pathParameterObject = listOfPathParams[0] + lateinit var existingPostReqForEndpointOfDelete : List> - val listOfStringGenes = ArrayList() - - recursiveTreeTraversalForFindingInformationForItem( - pathParameterObject, - "org.evomaster.core.search.gene.string.StringGene", - listOfStringGenes + if (existingPutForEndpointOfDelete.isNotEmpty()) { + existingEndpointForCreation = existingPutForEndpointOfDelete[0] + verbUsedForCreation = HttpVerb.PUT + } + else { + // if there is no such, search for an existing POST + existingPostReqForEndpointOfDelete = RestIndividualSelectorUtils.getIndividualsWithActionAndStatusGroup( + individualsInSolution, + HttpVerb.POST, + delete.path, // FIXME might be a parent, eg POST:/users for DELETE:/users/{id} . BUT CHECK FOR PATH STRUCTURE + "2xx" ) - if (listOfStringGenes.size > 0) { - - val stringGeneValue = (listOfStringGenes[0] as StringGene).value - - // find the child of type RestResourceCalls - return stringGeneValue - + if (existingPostReqForEndpointOfDelete.isNotEmpty()) { + existingEndpointForCreation = existingPostReqForEndpointOfDelete[0] + verbUsedForCreation = HttpVerb.DELETE } - } - - // if path parameter is not found, just return null - return null - - } - - /* - This method conducts a recursive tree traversal for finding objects of given types in the tree - For each item which is of the type we are looking for, adds them into an ArrayList - This method is used in many places. - It does not have any return types, but it adds to the finalListOfItems. - */ - private fun recursiveTreeTraversalForFindingInformationForItem(startingPoint: Any, typeOfItem : String, finalListOfItems: MutableList ) { - - if (startingPoint.javaClass.name.equals(typeOfItem)) { - finalListOfItems.add(startingPoint) } - // for each child, recursively call the function too - for( child : Any in (startingPoint as StructuralElement).getViewOfChildren()) { - recursiveTreeTraversalForFindingInformationForItem(child, typeOfItem, finalListOfItems) - } - - } - + return null - - private fun createCopyOfActionWithDifferentVerbOrUser ( actionId : String, - act: RestCallAction, - newVerb : HttpVerb, - newUser: HttpWsAuthenticationInfo - ) : RestCallAction{ - - val a = RestCallAction(actionId, newVerb, act.path, - act.parameters.toMutableList(), newUser, act.saveLocation, - act.locationId, act.produces, act.responseRefs, act.skipOracleChecks) - - // change the authentication information - return a + */ } + fun getIndexOfAction(individual: EvaluatedIndividual, + verb: HttpVerb, + path: RestPath, + statusCode: Int + ) : Int { - private fun getPathParameter(act: RestCallAction) : String { - - val listOfPathParameters = mutableListOf() - - // find the path parameter - this.recursiveTreeTraversalForFindingInformationForItem(act, - "org.evomaster.core.problem.rest.param.PathParam", listOfPathParameters) - - if (listOfPathParameters.isNotEmpty()) { - - // starting from the path parameter, find the endpoint - val pathParameterObject = listOfPathParameters[0] - - val listOfStringGenes = ArrayList() - - recursiveTreeTraversalForFindingInformationForItem( - pathParameterObject, - "org.evomaster.core.search.gene.string.StringGene", - listOfStringGenes - ) - - if (listOfStringGenes.size > 0) { - - val stringGeneValue = (listOfStringGenes[0] as StringGene).value + val actions = individual.evaluatedMainActions() - return stringGeneValue + for(index in actions.indices){ + val a = actions[index].action as RestCallAction + val r = actions[index].result as RestCallResult + if(a.verb == verb && a.path.isEquivalent(path) && r.getStatusCode() == statusCode){ + return index } } - // if path parameter is not found, just return empty String - return "" - + return -1 } - /* - This method is used to change the value of a given path parameter. + /** + * Create a copy of individual, where all main actions after index are removed */ - private fun changePathParameter(act: RestCallAction, newParam : String) { - - val listOfPathParams = ArrayList() - - // find the path parameter - recursiveTreeTraversalForFindingInformationForItem(act, - "org.evomaster.core.problem.rest.param.PathParam", listOfPathParams) - - if (listOfPathParams.size > 0 ) { - - // starting from the path parameter, find the endpoint - val pathParameterObject = listOfPathParams[0] + fun sliceAllCallsInIndividualAfterAction(restIndividual: RestIndividual, actionIndex: Int) : RestIndividual { - val listOfStringGenes = ArrayList() - - recursiveTreeTraversalForFindingInformationForItem( - pathParameterObject, - "org.evomaster.core.search.gene.string.StringGene", - listOfStringGenes - ) - - if (listOfStringGenes.size > 0) { - - (listOfStringGenes[0] as StringGene).value = newParam - - } + // we need to check that the index is within the range + if (actionIndex < 0 || actionIndex > restIndividual.size() -1) { + throw IllegalArgumentException("Action index has to be between 0 and ${restIndividual.size()}") } - } - - - - - /* - This function searches for AuthenticationDto object in authInfo - which is not utilized in authenticationObjectsForPutOrPatch - */ - private fun findAuthenticationDtoDifferentFromUsedAuthenticationObjects(authInfo: List, - authenticationObjectsForPutOrPatch: List): - AuthenticationDto? { - - // the AuthenticationDto object which has not been used - var authenticationDtoNotUsed : AuthenticationDto? = null + val ind = restIndividual.copy() as RestIndividual - for (firstAuth : AuthenticationDto in authInfo) { - - var notUsedInAny = true - - // compare AuthenticationDto with HttpWsAuthenticationInfo - for (secondAuth : HttpWsAuthenticationInfo in authenticationObjectsForPutOrPatch) { - - //if ( firstAuth.headers[0].value == secondAuth.headers[0].value ) { - // notUsedInAny = false - // } - - } - - // if it is not used in any HttpWsAuthenticationInfo, then the authentication header - // which has not been used has been found. - if (notUsedInAny) { - authenticationDtoNotUsed = firstAuth - } + val n = ind.seeMainExecutableActions().size + /* + We start from last, going backward. + So, actionIndex stays the same + */ + for(i in n-1 downTo actionIndex+1){ + ind.removeMainExecutableAction(i) } - return authenticationDtoNotUsed - - } - - - - - - /** - This method obtains HttpWsAuthenticationInfo objects from list of individuals. - */ - private fun identifyAuthenticationInformationUsedForIndividuals(listOfIndividuals: List>) - : List{ - - val listOfAuthenticationInfoUsedInIndividuals = mutableListOf() - val listOfResults = mutableListOf() - - // for each individual - for (ind : EvaluatedIndividual in listOfIndividuals) { - - for (child : StructuralElement in ind.individual.getViewOfChildren() ){ - - listOfResults.clear() - - // identify HttpWsAuthenticationInfo objects used in the individual - recursiveTreeTraversalForFindingInformationForItem(child, - "org.evomaster.core.problem.rest.RestCallAction", - listOfResults - ) - - // for each item in listOfResults which is a list of RestCallAction objects, identify authentication - for( item : Any in listOfResults) { - - listOfAuthenticationInfoUsedInIndividuals.add((item as RestCallAction).auth) - } - } - } + ind.fixGeneBindingsIfNeeded() + ind.removeLocationId() - // return the list of AuthenticationInfo objects - return listOfAuthenticationInfoUsedInIndividuals + return ind } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/StatusGroup.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/StatusGroup.kt new file mode 100644 index 0000000000..79f6d0e0fa --- /dev/null +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/StatusGroup.kt @@ -0,0 +1,19 @@ +package org.evomaster.core.problem.rest + +enum class StatusGroup { + + G_1xx, G_2xx, G_3xx, G_4xx, G_5xx; + + fun isInGroup(status: Int?) : Boolean{ + if(status == null){ + return false + } + return when(this){ + G_1xx -> status in 100..199 + G_2xx -> status in 200..299 + G_3xx -> status in 300..399 + G_4xx -> status in 400..499 + G_5xx -> status in 500..599 + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestModule.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestModule.kt index 8ed74f46fd..87799d708f 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestModule.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestModule.kt @@ -17,6 +17,7 @@ import org.evomaster.core.search.service.mutator.StructureMutator import org.evomaster.core.seeding.service.rest.PirToRest +@Deprecated("For WB use ResourceRestModule, for BB use BlackBoxRestModule") class RestModule(private val bindRemote : Boolean = true) : AbstractModule(){ override fun configure() { @@ -49,6 +50,11 @@ class RestModule(private val bindRemote : Boolean = true) : AbstractModule(){ .to(RestFitness::class.java) .asEagerSingleton() + bind(object : TypeLiteral() {}) + .to(RestResourceFitness::class.java) + .asEagerSingleton() + + bind(object : TypeLiteral>() {}) .to(RestFitness::class.java) .asEagerSingleton() diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt index 975057e816..4bd70c188d 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/SecurityRest.kt @@ -6,21 +6,28 @@ import javax.annotation.PostConstruct import org.evomaster.core.logging.LoggingUtil import org.evomaster.core.problem.enterprise.SampleType import org.evomaster.core.problem.enterprise.auth.AuthSettings +import org.evomaster.core.problem.enterprise.auth.NoAuth import org.evomaster.core.problem.httpws.auth.HttpWsAuthenticationInfo import org.evomaster.core.problem.rest.* +import org.evomaster.core.problem.rest.param.PathParam +import org.evomaster.core.problem.rest.resource.RestResourceCalls import org.evomaster.core.search.* import org.evomaster.core.search.service.Archive -import org.evomaster.core.search.service.FitnessFunction import org.evomaster.core.search.service.Randomness +import org.slf4j.Logger +import org.slf4j.LoggerFactory -//FIXME this needs to be cleaned-up /** * Service class used to do security testing after the search phase */ class SecurityRest { + companion object { + private val log: Logger = LoggerFactory.getLogger(SecurityRest::class.java) + } + /** * Archive including test cases */ @@ -33,17 +40,20 @@ class SecurityRest { @Inject private lateinit var randomness: Randomness + @Inject + private lateinit var fitness: RestFitness /** * All actions that can be defined from the OpenAPI schema */ - private lateinit var actionDefinitions : List + private lateinit var actionDefinitions: List /** - * Individuals in the solution + * Individuals in the solution. + * Derived from archive. */ - private lateinit var individualsInSolution : List> + private lateinit var individualsInSolution: List> private lateinit var authSettings: AuthSettings @@ -52,10 +62,8 @@ class SecurityRest { * and authentication settings. */ @PostConstruct - private fun postInit(){ - + private fun postInit() { - // get action definitions actionDefinitions = sampler.getActionDefinitions() as List authSettings = sampler.authentications @@ -65,17 +73,20 @@ class SecurityRest { * Apply a set rule of generating new test cases, which will be added to the current archive. * Extract a new test suite(s) from the archive. */ - fun applySecurityPhase() : Solution{ + fun applySecurityPhase(): Solution { // extract individuals from the archive - val archivedSolution : Solution = this.archive.extractSolution() - individualsInSolution = archivedSolution.individuals + val archivedSolution: Solution = this.archive.extractSolution() + individualsInSolution = archivedSolution.individuals // we can see what is available from the schema, and then check if already existing a test for it in archive + // newly generated tests will be added back to archive addForAccessControl() - // just return the archive for solutions including the security test. + //TODO possible other kinds of security tests here + + // just return the archive for solutions including the security tests. return archive.extractSolution() } @@ -103,9 +114,14 @@ class SecurityRest { // quite a few rules here that can be defined handleForbiddenDeleteButOkPutOrPatch() + //TODO other rules - } + //eg, ok PUT but not DELETE or PATCH + //eg, ok PATCH but not DELETE or PUT + // eg 401/403 info leakage + //etc. + } /** @@ -129,6 +145,7 @@ class SecurityRest { --- search for create resource for endpoint of DELETE --- do slice and remove calls after create resource endpoint call --- add a DELETE with different user (verify get a 403) + --- make sure that DELETE is bound on some resource path as the create operation - append new REST Call action (see mutator classes) for PATCH/PUT with different auth, based on action copies from action definitions. should be on same endpoint. - need to resolve path element parameters to point to same resolved endpoint as DELETE @@ -140,175 +157,271 @@ class SecurityRest { */ // Check if at least 2 users. if not, nothing to do - if (authSettings.size(HttpWsAuthenticationInfo::class.java) <= 1) { + if (!checkForAtLeastNumberOfAuthenticatedUsers(2)) { // nothing to test if there are not at least 2 users LoggingUtil.getInfoLogger().debug( - "Security test handleForbiddenDeleteButOkPutOrPatch requires at least 2 authenticated users") + "Security test handleForbiddenDeleteButOkPutOrPatch requires at least 2 authenticated users" + ) return } // From schema, check all DELETE operations, in order to do that // obtain DELETE operations in the SUT according to the swagger - val deleteOperations = getAllActionDefinitions(HttpVerb.DELETE) + val deleteOperations = RestIndividualSelectorUtils.getAllActionDefinitions(actionDefinitions, HttpVerb.DELETE) // for each endpoint for which there is a DELETE operation deleteOperations.forEach { delete -> // from archive, search if there is any test with a DELETE returning a 403 - val existing403 : List> = - RestIndividualSelectorUtils.findIndividuals(individualsInSolution, HttpVerb.DELETE, delete.path, 403) - - var individualToChooseForTest : RestIndividual + val existing403 = RestIndividualSelectorUtils.findIndividuals( + individualsInSolution, + HttpVerb.DELETE, + delete.path, + 403, + authenticated = true + ) + // individual to choose for test, this is the individual we are going to manipulate + val individualToChooseForTest: RestIndividual // if there is such an individual if (existing403.isNotEmpty()) { // current individual in the list of existing 403. Since the list is not empty,\ // we can just get the first item + // TODO add test for existing 403 val currentIndividualWith403 = existing403[0] - val deleteAction = RestIndividualSelectorUtils.getActionIndexFromIndividual(currentIndividualWith403.individual, HttpVerb.DELETE, - delete.path) - - val deleteActionIndex = RestIndividualSelectorUtils.getActionWithIndex(currentIndividualWith403, deleteAction) + val deleteActionIndex = RestIndividualSelectorUtils.getIndexOfAction( + currentIndividualWith403, + HttpVerb.DELETE, + delete.path, + 403 + ) // slice the individual in a way that delete all calls after the DELETE request - individualToChooseForTest = RestIndividualSelectorUtils.sliceAllCallsInIndividualAfterAction(currentIndividualWith403, deleteActionIndex) + individualToChooseForTest = RestIndividualSelectorUtils.sliceAllCallsInIndividualAfterAction( + currentIndividualWith403.individual, + deleteActionIndex + ) + } else { // there is not. need to create it based on successful create resources with authenticated user - var verbUsedForCreation : HttpVerb? = null; - // search for create resource for endpoint of DELETE using PUT - lateinit var existingEndpointForCreation : EvaluatedIndividual - - val existingPutForEndpointOfDelete : List> = - RestIndividualSelectorUtils.getIndividualsWithActionAndStatusGroup(individualsInSolution, HttpVerb.PUT, delete.path, - "2xx") - - lateinit var existingPostReqForEndpointOfDelete : List> - - if (existingPutForEndpointOfDelete.isNotEmpty()) { - existingEndpointForCreation = existingPutForEndpointOfDelete[0] - verbUsedForCreation = HttpVerb.PUT - } - else { - // if there is no such, search for an existing POST - existingPostReqForEndpointOfDelete = RestIndividualSelectorUtils.getIndividualsWithActionAndStatusGroup( + // but, first let's check if we can have any successfully delete action + + /* + We have a DELETE path in form for example + /users/{id} + and want to get a _resolved_ creation action (either PUT or POST) for it. + The new DELETE we are going to create must point to the same resolved action. + But DELETE could have query parameters and possibly body payloads... all with + constraints that must be satisfied. + So we cannot easily just create it from scratch. + Need to re-use an existing one, if any. + */ + var deleteIndividuals = RestIndividualSelectorUtils.findIndividuals( + individualsInSolution, + HttpVerb.DELETE, + delete.path, + statusGroup = StatusGroup.G_2xx, + ) + if(deleteIndividuals.isEmpty()){ + /* + This needs a bit of explanation. + We want to get a DELETE that works, with failed constraint validation on + query parameters or body payloads. + Ideally, a 2xx would do. + But what if we could not create any because they all fail to point to an existing + resource? if 404, could still be fine, as then we link it to creation operation + */ + deleteIndividuals = RestIndividualSelectorUtils.findIndividuals( individualsInSolution, - HttpVerb.POST, delete.path, - "2xx" + HttpVerb.DELETE, + delete.path, + status = 404 ) - - if (existingPostReqForEndpointOfDelete.isNotEmpty()) { - existingEndpointForCreation = existingPostReqForEndpointOfDelete[0] - verbUsedForCreation = HttpVerb.DELETE - } - } - - // if neither POST not PUT exists for the endpoint, we need to handle that case specifically - if (existingEndpointForCreation == null) { - // TODO - LoggingUtil.getInfoLogger().debug( - "The archive does not contain any successful PUT or POST requests, this case is not handled") - return + if(deleteIndividuals.isEmpty()){ + //no point trying to create a DELETE action directly, if none was evolved at all... + //as likely would fail as well here + return@forEach } - var actionIndexForCreation = -1 - - if (verbUsedForCreation != null) { - - // so we found an individual with a successful PUT or POST, we will slice all calls after PUT or POST - actionIndexForCreation = RestIndividualSelectorUtils.getActionIndexFromIndividual( - existingEndpointForCreation.individual, - verbUsedForCreation, - delete.path - ) - + //start from base test in which resource is created + val (creationIndividual, creationEndpoint) = RestIndividualSelectorUtils.findIndividualWithEndpointCreationForResource( + individualsInSolution, + delete.path, + true + ) ?: return@forEach + + // find the index of the creation action + val actionIndexForCreation = creationIndividual.individual.getActionIndex( + creationEndpoint.verb, + creationEndpoint.path + ) + val creationAction = creationIndividual.individual.seeMainExecutableActions()[actionIndexForCreation] + assert(creationAction.auth !is NoAuth) + + //we don't need anything after the creation action + val sliced = RestIndividualSelectorUtils.sliceAllCallsInIndividualAfterAction( + creationIndividual.individual, + actionIndexForCreation + ) + + val deleteInd = deleteIndividuals.first().individual + val deleteActionIndex = deleteInd.getActionIndex(HttpVerb.DELETE, delete.path) + val deleteAction = deleteInd.seeMainExecutableActions()[deleteActionIndex].copy() as RestCallAction + assert(deleteAction.verb == HttpVerb.DELETE && deleteAction.path.isEquivalent(delete.path)) + deleteAction.resetLocalId() + deleteAction.auth = authSettings.getDifferentOne(creationAction.auth.name, HttpWsAuthenticationInfo::class.java, randomness) + + //TODO bind to same path as creation action + /* + for now let's bind just to a PUT. + We need to create "links" when dealing with POST creating new ids + TODO this would be part anyway of ongoing refactoring of BB testing + */ + if(creationEndpoint.path.isEquivalent(delete.path)){ + deleteAction.bindBasedOn(creationAction.path, creationAction.parameters.filterIsInstance(),null) + } else { + //TODO. eg POST on ancestor path + return@forEach } - // create a copy of the existingEndpointForCreation - val existingEndpointForCreationCopy = existingEndpointForCreation.copy() - val actionForCreation = RestIndividualSelectorUtils.getActionWithIndex(existingEndpointForCreation, actionIndexForCreation) - - RestIndividualSelectorUtils.sliceAllCallsInIndividualAfterAction(existingEndpointForCreationCopy, actionForCreation) - - // add a DELETE call with another user - individualToChooseForTest = - createIndividualWithAnotherActionAddedDifferentAuthRest(existingEndpointForCreationCopy.individual, - actionForCreation, HttpVerb.DELETE ) - + sliced.addResourceCall(restCalls = RestResourceCalls(actions = mutableListOf(deleteAction), sqlActions = listOf())) + individualToChooseForTest = sliced } - // After having a set of requests in which the last one is a DELETE call with another user, add a PUT - // with another user - val deleteActionIndex = RestIndividualSelectorUtils.getActionIndexFromIndividual(individualToChooseForTest, HttpVerb.DELETE, - delete.path) - - val deleteAction = RestIndividualSelectorUtils.getActionWithIndexRestIndividual(individualToChooseForTest, deleteActionIndex) + // After having a set of requests in which the last one is a 403 DELETE call with another user, add a PUT + // or PATCH with same user as 403 DELETE + + //at this point, the individual we are building should end with a 403 DELETE + val lastAction = individualToChooseForTest.seeMainExecutableActions().last() + assert(lastAction.verb == HttpVerb.DELETE) + // but, for checking 403, we need to evaluate it. + // so, we ll do this check at the end + val lastActionIndex = individualToChooseForTest.seeMainExecutableActions().size - 1 + + val put = RestIndividualSelectorUtils.findAction( + individualsInSolution, + HttpVerb.PUT, + delete.path, + statusGroup = StatusGroup.G_2xx + )?.copy() as RestCallAction? + + + val patch = RestIndividualSelectorUtils.findAction( + individualsInSolution, + HttpVerb.PATCH, + delete.path, + statusGroup = StatusGroup.G_2xx + )?.copy() as RestCallAction? + + listOf(put, patch).forEach { + if(it != null){ + it.resetLocalId() + it.auth = lastAction.auth + + val finalIndividual = individualToChooseForTest.copy() as RestIndividual + finalIndividual.addResourceCall(restCalls = RestResourceCalls(actions = mutableListOf(it), sqlActions = listOf())) + finalIndividual.modifySampleType(SampleType.SECURITY) + + val evaluatedIndividual = fitness.computeWholeAchievedCoverageForPostProcessing(finalIndividual) + if(evaluatedIndividual == null){ + log.warn("Failed to evaluate constructed individual in security testing phase") + return@forEach + } - var individualToAddToSuite = createIndividualWithAnotherActionAddedDifferentAuthRest(individualToChooseForTest, - deleteAction, HttpVerb.PUT ) + //first, let's verify indeed second last action if a 403 DELETE. otherwise, it is a problem + val ema = evaluatedIndividual.evaluatedMainActions() + val secondLast = ema[ema.size-2] + if(!(secondLast.action is RestCallAction && secondLast.action.verb == HttpVerb.DELETE + && secondLast.result is RestCallResult && secondLast.result.getStatusCode() == 403)){ + log.warn("Issue with constructing evaluated individual. Expected a 403 DELETE, but got: $secondLast") + return@forEach + } - /* - FIXME fitness bean must be injected, and not instantiated directly. - note, issue we have 2 different implementation, need to double-check - */ + //now we can finally ask, is there a problem with last call? - // Then evaluate the fitness function to create evaluatedIndividual - val fitness : FitnessFunction = RestFitness() + //TODO new testing targets + /* + FIXME: if we do it only here, then we would lose this info if the test case is re-evaluated + for any reason... eg, in minimizer + need to think of a good, general solution... - val evaluatedIndividual = fitness.computeWholeAchievedCoverageForPostProcessing(individualToAddToSuite) + TODO let's do it directly in the fitness function, with new security test oracle + */ - // add the evaluated individual to the archive - if (evaluatedIndividual != null) { - archive.addIfNeeded(evaluatedIndividual) + // add the evaluated individual to the archive + archive.addIfNeeded(evaluatedIndividual) + } } } - } + + /** - * Create another individual from the individual by adding a new action with a new verb and with a different - * authentication. - * @param individual - REST indovidual which is the starting point - * @param currentAction - REST action for the new individual - * @param newActionVerb - verb for the new action, such as GET, POST + * Information leakage + * accessing endpoint with id with not authorized should return 403, even if not exists. + * otherwise, if returning 404, we can find out that the resource does not exist. */ - private fun createIndividualWithAnotherActionAddedDifferentAuthRest(individual: RestIndividual, - currentAction : RestCallAction, - newActionVerb : HttpVerb, - ) : RestIndividual { + fun handleNotAuthorizedResourceExistenceLeakage() { - var actionList = mutableListOf() + //TODO - for (act in individual.seeMainExecutableActions()) { - actionList.add(act) + // There has to be at least two authenticated users + if (!checkForAtLeastNumberOfAuthenticatedUsers(2)) { + // nothing to test if there are not at least 2 users + LoggingUtil.getInfoLogger().debug( + "Security test handleNotAuthorizedInAnyCase requires at least 2 authenticated users" + ) + return } - // create a new action with the authentication not used in current individual - val authenticationOfOther = sampler.authentications.getDifferentOne(currentAction.auth.name, HttpWsAuthenticationInfo::class.java, randomness) + // get all endpoints each user has access to (this can be a function since we need this often) - var newRestCallAction :RestCallAction? = null; + // among endpoints userA has access, those endpoints should give 2xx as response - if (authenticationOfOther != null) { - newRestCallAction = RestCallAction("newDelete", newActionVerb, currentAction.path, - currentAction.parameters.toMutableList(), authenticationOfOther ) - } + // find an endpoint userA does not have access but another user has access, this should give 403 + + // try with an endpoint that does not exist, this should give 403 as well, not 404. - if (newRestCallAction != null) { - actionList.add(newRestCallAction) - } + } - val newIndividual = RestIndividual(actionList, SampleType.SECURITY) - return newIndividual + /** + * Wrong Authorization + * + * User A, access endpoint X, but get 401 (instead of 403). + * In theory, a bug. But, could be false positive if A is misconfigured. + * How to check it? + * See if on any other endpoint Y we get a 2xx with A. + * But, maybe Y does not need authentication... + * so, check if there is any test case for which on Y we get a 401 or 403. + * if yes, then X is buggy, as it should rather have returned 403 for A. + * This seems to actually happen for familie-ba-sak, new NAV SUT. + */ + fun handleUnauthorizedInsteadOfForbidden() { } - private fun getAllActionDefinitions(verb: HttpVerb): List { - return actionDefinitions.filter { it.verb == verb } + /** + * + */ + private fun checkForAtLeastNumberOfAuthenticatedUsers(numberOfUsers: Int): Boolean { + + // check the number of authenticated users + if (authSettings.size(HttpWsAuthenticationInfo::class.java) < numberOfUsers) { + + LoggingUtil.getInfoLogger().debug( + "Security test for this method requires at least $numberOfUsers authenticated users" + ) + return false + } + + return true } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/search/Individual.kt b/core/src/main/kotlin/org/evomaster/core/search/Individual.kt index 81129d73b8..26517ecac8 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/Individual.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/Individual.kt @@ -400,6 +400,16 @@ abstract class Individual(override var trackOperator: TrackOperator? = null, return true } + fun fixGeneBindingsIfNeeded() : Boolean{ + if (!verifyBindingGenes()){ + cleanBrokenBindingReference() + computeTransitiveBindingGenes() + return true + } + return false + } + + /** * @return an action based on the specified [localId] */ diff --git a/core/src/main/kotlin/org/evomaster/core/search/StructuralElement.kt b/core/src/main/kotlin/org/evomaster/core/search/StructuralElement.kt index 6d3f0af980..c044d0527b 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/StructuralElement.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/StructuralElement.kt @@ -56,7 +56,7 @@ abstract class StructuralElement ( if (!hasLocalId()) this.localId = id else - throw IllegalStateException("cannot re-assign the id of the action, the current id is ${this.localId}") + throw IllegalStateException("Cannot re-assign the id of the element with $id. The current id is ${this.localId}") } /** diff --git a/core/src/main/kotlin/org/evomaster/core/search/service/Minimizer.kt b/core/src/main/kotlin/org/evomaster/core/search/service/Minimizer.kt index 02de88cffe..d08267a79a 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/service/Minimizer.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/service/Minimizer.kt @@ -174,6 +174,10 @@ class Minimizer { val n = ind.size() + /* + * This is done because ind might not be flattened. in this case, in REST, there can be SQL actions for + * resource handling in the main action group-trees + */ val sqlActions = if (ind is EnterpriseIndividual) ind.seeSQLActionBeforeIndex(index).map { it.copy() as SqlAction} else null for(i in n-1 downTo index+1){ @@ -187,10 +191,7 @@ class Minimizer { ind.addChildrenToGroup(sqlActions, GroupsOfChildren.INITIALIZATION_SQL) } - if (!ind.verifyBindingGenes()){ - ind.cleanBrokenBindingReference() - ind.computeTransitiveBindingGenes() - } + ind.fixGeneBindingsIfNeeded() if(ind is RestIndividual){ ind.removeLocationId() diff --git a/core/src/test/kotlin/org/evomaster/core/TestUtils.kt b/core/src/test/kotlin/org/evomaster/core/TestUtils.kt index e087a0e3c6..b03910bee1 100644 --- a/core/src/test/kotlin/org/evomaster/core/TestUtils.kt +++ b/core/src/test/kotlin/org/evomaster/core/TestUtils.kt @@ -10,6 +10,8 @@ import org.evomaster.core.sql.schema.Column import org.evomaster.core.sql.schema.ColumnDataType import org.evomaster.core.sql.schema.Table import org.evomaster.core.problem.enterprise.SampleType +import org.evomaster.core.problem.httpws.auth.HttpWsAuthenticationInfo +import org.evomaster.core.problem.httpws.auth.HttpWsNoAuth import org.evomaster.core.problem.rest.* import org.evomaster.core.problem.rest.param.QueryParam import org.evomaster.core.search.gene.numeric.IntegerGene @@ -88,5 +90,4 @@ object TestUtils { return RestCallAction(id, HttpVerb.GET, RestPath(pathString), actions) } - } \ No newline at end of file diff --git a/core/src/test/kotlin/org/evomaster/core/problem/rest/StatusGroupTest.kt b/core/src/test/kotlin/org/evomaster/core/problem/rest/StatusGroupTest.kt new file mode 100644 index 0000000000..19b529b277 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/problem/rest/StatusGroupTest.kt @@ -0,0 +1,146 @@ +package org.evomaster.core.problem.rest + +import org.junit.Assert +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +/** + * Test cases for StatusGroup + */ +class StatusGroupTest { + + /** + * All possible status groups + */ + private val statusGroup1xx = StatusGroup.G_1xx + private val statusGroup2xx = StatusGroup.G_2xx + private val statusGroup3xx = StatusGroup.G_3xx + private val statusGroup4xx = StatusGroup.G_4xx + private val statusGroup5xx = StatusGroup.G_5xx + + @Test + fun isInGroupWith1xx() { + + // for each number between 100 and 199 inclusive, they are in group + for( i in 100..199) { + Assertions.assertTrue(statusGroup1xx.isInGroup(i)) + } + + // 99 and 200 are not in the group (edge cases) + Assertions.assertFalse(statusGroup1xx.isInGroup(99)) + Assertions.assertFalse(statusGroup1xx.isInGroup(200)) + + // 2000 is not in the group 2xx + Assertions.assertFalse(statusGroup1xx.isInGroup(2000)) + + // -2000 is not in the group 2xx + Assertions.assertFalse(statusGroup1xx.isInGroup(-2000)) + } + + + @Test + fun isInGroupWith2xx() { + + // for each number between 200 and 299 inclusive, they are in group + for( i in 200..299) { + Assertions.assertTrue(statusGroup2xx.isInGroup(i)) + } + + // 199 and 300 are not in the group (edge cases) + Assertions.assertFalse(statusGroup2xx.isInGroup(199)) + Assertions.assertFalse(statusGroup2xx.isInGroup(300)) + + // 3000 is not in the group 2xx + Assertions.assertFalse(statusGroup2xx.isInGroup(3000)) + + // -3000 is not in the group 2xx + Assertions.assertFalse(statusGroup2xx.isInGroup(-3000)) + } + + @Test + fun isInGroupWith3xx() { + + // for each number between 300 and 399 inclusive, they are in group + for( i in 300..399) { + Assertions.assertTrue(statusGroup3xx.isInGroup(i)) + } + + // 299 and 400 are not in the group (edge cases) + Assertions.assertFalse(statusGroup3xx.isInGroup(299)) + Assertions.assertFalse(statusGroup3xx.isInGroup(400)) + + // 4000 is not in the group 3xx + Assertions.assertFalse(statusGroup3xx.isInGroup(4000)) + + // -4000 is not in the group 3xx + Assertions.assertFalse(statusGroup3xx.isInGroup(-4000)) + } + + @Test + fun isInGroupWith4xx() { + + // for each number between 300 and 399 inclusive, they are in group + for( i in 400..499) { + Assertions.assertTrue(statusGroup4xx.isInGroup(i)) + } + + // 399 and 500 are not in the group (edge cases) + Assertions.assertFalse(statusGroup4xx.isInGroup(399)) + Assertions.assertFalse(statusGroup4xx.isInGroup(500)) + + // 5000 is not in the group 3xx + Assertions.assertFalse(statusGroup4xx.isInGroup(5000)) + + // -5000 is not in the group 3xx + Assertions.assertFalse(statusGroup4xx.isInGroup(-5000)) + } + + @Test + fun isInGroupWith5xx() { + + // for each number between 300 and 399 inclusive, they are in group + for( i in 500..599) { + Assertions.assertTrue(statusGroup5xx.isInGroup(i)) + } + + // 499 and 600 are not in the group (edge cases) + Assertions.assertFalse(statusGroup5xx.isInGroup(499)) + Assertions.assertFalse(statusGroup5xx.isInGroup(600)) + + // 6000 is not in the group 3xx + Assertions.assertFalse(statusGroup5xx.isInGroup(6000)) + + // -6000 is not in the group 3xx + Assertions.assertFalse(statusGroup5xx.isInGroup(-6000)) + } + + /** + * null element should not be in any of groups 1xx, 2xx, 3xx, 4xx, 5xx + */ + @Test + fun testIsInGroupWithNull(){ + + Assertions.assertFalse(statusGroup1xx.isInGroup(null)) + Assertions.assertFalse(statusGroup2xx.isInGroup(null)) + Assertions.assertFalse(statusGroup3xx.isInGroup(null)) + Assertions.assertFalse(statusGroup4xx.isInGroup(null)) + Assertions.assertFalse(statusGroup5xx.isInGroup(null)) + + } + + // Testing exceptional cases. + @Test + fun isInGroupParameterRetrievedFromParse() { + Assertions.assertTrue(statusGroup1xx.isInGroup(Integer.parseInt("101"))) + } + + @Test + fun isInGroupParameterRetrievedFromParseDouble() { + Assert.assertThrows(Exception::class.java) { statusGroup1xx.isInGroup(Integer.parseInt("101.89")) } + } + + @Test + fun isInGroupParameterRetrievedFromParseString() { + Assert.assertThrows(Exception::class.java) { statusGroup1xx.isInGroup(Integer.parseInt("abcdef")) } + } +} \ No newline at end of file diff --git a/e2e-tests/spring-rest-openapi-v2/src/test/java/org/evomaster/e2etests/spring/examples/security/accesscontrol/deleteput/ACDeletePutEMTest.java b/e2e-tests/spring-rest-openapi-v2/src/test/java/org/evomaster/e2etests/spring/examples/security/accesscontrol/deleteput/ACDeletePutEMTest.java index 4c26c11ff5..8abfab333f 100644 --- a/e2e-tests/spring-rest-openapi-v2/src/test/java/org/evomaster/e2etests/spring/examples/security/accesscontrol/deleteput/ACDeletePutEMTest.java +++ b/e2e-tests/spring-rest-openapi-v2/src/test/java/org/evomaster/e2etests/spring/examples/security/accesscontrol/deleteput/ACDeletePutEMTest.java @@ -6,6 +6,7 @@ import org.evomaster.core.search.Solution; import org.evomaster.e2etests.spring.examples.SpringTestBase; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; @@ -18,6 +19,7 @@ public static void initClass() throws Exception { SpringTestBase.initClass(new ACDeletePutController()); } + @Disabled @Test public void testRunEM() throws Throwable { @@ -27,6 +29,9 @@ public void testRunEM() throws Throwable { 100, (args) -> { + args.add("--security"); + args.add("true"); + Solution solution = initAndRun(args); // GET request