From 9e28a78e6a88b68be60f85e5a6368e36b26967e1 Mon Sep 17 00:00:00 2001 From: LautaroPetaccio Date: Sat, 10 Oct 2026 21:22:14 -0300 Subject: [PATCH] AsyncAPI 3.x: taint the payloads of AsyncAPI messages REST, GraphQL, RPC and Web all run taint analysis in white-box mode: a string gene carries a recognisable tainted value, the instrumented SUT reports what it compared that value with, and the gene learns the constant for mutation to use. AsyncAPI never did, so a service that branches on a payload field equal to some constant left the search guessing. Nothing else was missing. Payload and header strings are StringGenes that already take tainted values at sampling and mutation time, they travel to the service inside the message, the instrumentation reports what it saw regardless of the problem type, and the controller records one AdditionalInfo entry per action it is told about. So AsyncApiFitness calls TaintAnalysis.doTaintAnalysis after reading the coverage, under the same conditions as REST and GraphQL: white-box, during the search, with taint analysis enabled, which it is by default. Worth flagging for review: - The size check is against the call results, not the equality GraphQL asserts: no more entries than AsyncApiCallResults. A test stops at the first message that could not be published, so there can be fewer results than actions, and a driver that could not be reached records no entry for its message, so there can be fewer entries than results. TaintAnalysis itself only refuses more. - The action index in the instrumentation is global, so a comparison made on a consumer thread is recorded against whichever action is current. This is harmless: the analysis matches each reported value against the genes of every action. AsyncApiFitnessTest covers a payload string gaining the constant from the AdditionalInfoDto a fake driver reports, a test stopped by a failed publish with fewer entries than actions, a black-box run under MIO (where taint analysis is enabled) that never asks for coverage and so never analyses, and an evaluation outside the search that does not analyse either. Black-box runs are unchanged, as are all other problem types. --- .../asyncapi/service/AsyncApiFitness.kt | 17 +++ .../asyncapi/service/AsyncApiFitnessTest.kt | 123 ++++++++++++++++++ 2 files changed, 140 insertions(+) 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 761397cb16..673417a6b8 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 @@ -160,6 +162,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 7905611936..3a0a32fd04 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 @@ -5,11 +5,17 @@ import com.google.inject.Injector import com.google.inject.Key import com.google.inject.TypeLiteral 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 @@ -21,10 +27,12 @@ import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory import org.evomaster.core.problem.enterprise.SampleType import org.evomaster.core.problem.rest.builder.RestActionBuilderV3 import org.evomaster.core.search.EvaluatedIndividual +import org.evomaster.core.search.gene.string.StringGene import org.evomaster.core.search.service.FitnessFunction import org.evomaster.core.search.service.IdMapper import org.evomaster.core.search.service.Randomness import org.evomaster.core.search.service.SearchGlobalState +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 @@ -56,6 +64,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. @@ -242,6 +265,106 @@ 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 = individualOf(*Array(count) { "publishEvent" }) + payloadField(individual).value = TAINTED + + return fitness.calculateCoverage(individual, modifiedSpec = null) + ?: fail("the fitness gave up on the 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() {