diff --git a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/service/AsyncApiFitness.kt b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/service/AsyncApiFitness.kt index b3431eb46a..39bd4a2b55 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/service/AsyncApiFitness.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/asyncapi/service/AsyncApiFitness.kt @@ -9,6 +9,7 @@ import com.webfuzzing.asyncapi.models.AsyncApiReply import org.evomaster.client.java.controller.api.dto.SutInfoDto import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiActionDto import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiReplyDto +import org.evomaster.core.Lazy import org.evomaster.core.database.sql.SqlAction import org.evomaster.core.logging.LoggingUtil import org.evomaster.core.problem.api.service.ApiWsFitness @@ -24,6 +25,7 @@ import org.evomaster.core.search.EvaluatedIndividual import org.evomaster.core.search.FitnessValue import org.evomaster.core.search.action.ActionResult import org.evomaster.core.search.gene.utils.GeneUtils +import org.evomaster.core.taint.TaintAnalysis import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -150,6 +152,21 @@ class AsyncApiFitness : ApiWsFitness() { val dto = updateFitnessAfterEvaluation(targets, allTargets, fullyCovered, descriptiveIds, individual, fv) ?: return null handleExtra(dto, fv) + + if (epc.isInSearch() && config.isEnabledTaintAnalysis()) { + /* + A tainted string in a payload or a header reaches the service inside the + message. The analysis matches what the service reports against the genes of + every action, so nested fields are covered, whichever thread consumed it. + + At most one entry per result, not exactly one as GraphQL asserts: a driver that + could not be reached records no entry for its message. + + Only during the search, as for REST and GraphQL: nothing mutates the genes later. + */ + Lazy.assert { dto.additionalInfoList.size <= actionResults.count { it is AsyncApiCallResult } } + TaintAnalysis.doTaintAnalysis(individual, dto.additionalInfoList, randomness, config) + } } return EvaluatedIndividual( diff --git a/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/service/AsyncApiFitnessTest.kt b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/service/AsyncApiFitnessTest.kt index d2e51dc1dc..0a3ac5a070 100644 --- a/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/service/AsyncApiFitnessTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/problem/asyncapi/service/AsyncApiFitnessTest.kt @@ -3,11 +3,17 @@ package org.evomaster.core.problem.asyncapi.service import com.fasterxml.jackson.databind.ObjectMapper import com.google.inject.Injector import com.webfuzzing.asyncapi.access.AsyncApiAccess +import org.evomaster.client.java.controller.api.dto.AdditionalInfoDto +import org.evomaster.client.java.controller.api.dto.StringSpecializationInfoDto import org.evomaster.client.java.controller.api.dto.TargetInfoDto import org.evomaster.client.java.controller.api.dto.TestResultsDto import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiActionDto import org.evomaster.client.java.controller.api.dto.problem.asyncapi.AsyncApiReplyDto import org.evomaster.client.java.instrumentation.shared.ObjectiveNaming +import org.evomaster.client.java.instrumentation.shared.StringSpecialization +import org.evomaster.client.java.instrumentation.shared.TaintInputName +import org.evomaster.client.java.instrumentation.shared.TaintType +import org.evomaster.core.EMConfig import org.evomaster.core.problem.asyncapi.data.AsyncApiAction import org.evomaster.core.problem.asyncapi.data.AsyncApiCallResult import org.evomaster.core.problem.asyncapi.data.AsyncApiIndividual @@ -20,6 +26,8 @@ import org.evomaster.core.problem.rest.builder.RestActionBuilderV3 import org.evomaster.core.search.EvaluatedIndividual import org.evomaster.core.search.service.IdMapper import org.evomaster.core.problem.asyncapi.service.AsyncApiTestInjector.DOUBLE_RESULT +import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.search.service.time.ExecutionPhaseController import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -47,6 +55,21 @@ class AsyncApiFitnessTest { */ private const val SUT_LINE_ID = 1 + /** + * The payload field of [EVENTS] that taint analysis is checked on. + */ + private const val PAYLOAD_FIELD = "id" + + /** + * What the service compared the tainted payload field with, in the tests on taint. + */ + private const val COMPARED_WITH = "order-created" + + /** + * A value taint analysis would recognise as its own once the service reports it back. + */ + private val TAINTED = TaintInputName.getTaintName(0) + /** * One operation that expects nothing back, and one whose message carries a header * besides the one the correlation id is stamped into. @@ -215,6 +238,105 @@ class AsyncApiFitnessTest { assertEquals(2, driver.published.size) } + /** + * The [PAYLOAD_FIELD] string gene in the payload of the action at [index]. + */ + private fun payloadField(individual: AsyncApiIndividual, index: Int = 0): StringGene = + individual.seeMainExecutableActions()[index].seeTopGenes() + .flatMap { it.flatView() } + .filterIsInstance() + .single { it.name == PAYLOAD_FIELD } + + /** + * What an instrumented driver reports for an action where the service compared [TAINTED] + * with [COMPARED_WITH]. + */ + private fun comparedWithAConstant() = AdditionalInfoDto().apply { + stringSpecializations[TAINTED] = listOf(StringSpecializationInfoDto( + StringSpecialization.CONSTANT.name, COMPARED_WITH, TaintType.FULL_MATCH.name)) + } + + /** + * Evaluates [count] messages of [EVENTS], the first of which carries [TAINTED] in its payload, + * with the driver reporting [perAction] as what the service did with its inputs. + */ + private fun evaluateTainted(count: Int, vararg perAction: AdditionalInfoDto): EvaluatedIndividual { + + driver.testResults = { TestResultsDto().apply { additionalInfoList.addAll(perAction) } } + + val individual = AsyncApiTestInjector.individualOf(injector, *Array(count) { "publishEvent" }) + payloadField(individual).value = TAINTED + + return AsyncApiTestInjector.evaluate(injector, individual) + } + + private fun startTheSearch() = + injector.getInstance(ExecutionPhaseController::class.java).markStartingSearch() + + private fun specializationsOf(gene: StringGene) = gene.specializationGenes.map { it.getValueAsRawString() } + + @Test + fun testAPayloadStringLearnsWhatTheServiceComparedItWith() { + + start(EVENTS) { fireAndForget() } + startTheSearch() + + val evaluated = evaluateTainted(1, comparedWithAConstant()) + + //the tainted value went out inside the message, which is how the service got to see it + assertTrue(driver.published.single().payload.contains(TAINTED), driver.published.single().payload) + assertEquals(listOf(COMPARED_WITH), specializationsOf(payloadField(evaluated.individual))) + } + + @Test + fun testATestThatStoppedAtAFailedPublishIsStillAnalysed() { + + /* + The second publish fails, so the controller reports two entries for three actions. + That is what a test that stops early looks like, not an inconsistency. + */ + var sent = 0 + start(EVENTS) { + sent++ + if (sent == 1) fireAndForget() else AsyncApiReplyDto().apply { published = false } + } + startTheSearch() + + val evaluated = evaluateTainted(3, comparedWithAConstant(), AdditionalInfoDto()) + + assertEquals(2, driver.published.size) + assertTrue(results(evaluated)[1].stopping) + assertEquals(listOf(COMPARED_WITH), specializationsOf(payloadField(evaluated.individual))) + } + + @Test + fun testABlackBoxRunNeverRunsTaintAnalysis() { + + /* + MIO turns taint analysis on even in black-box mode, but there is nothing to analyse + without asking the driver what the service did, and a black-box run does not ask. + */ + start(EVENTS, "$BLACK_BOX=true", "--algorithm=MIO") { fireAndForget() } + startTheSearch() + assertTrue(injector.getInstance(EMConfig::class.java).isEnabledTaintAnalysis()) + + val evaluated = evaluateTainted(1, comparedWithAConstant()) + + assertEquals(0, driver.testResultsRequests) + assertTrue(specializationsOf(payloadField(evaluated.individual)).isEmpty()) + } + + @Test + fun testTaintAnalysisRunsOnlyDuringTheSearch() { + + start(EVENTS) { fireAndForget() } + + val evaluated = evaluateTainted(1, comparedWithAConstant()) + + assertTrue(driver.testResultsRequests > 0) + assertTrue(specializationsOf(payloadField(evaluated.individual)).isEmpty()) + } + @Test fun testTheDriverIsToldEverythingItNeeds() {