Skip to content

Commit e873094

Browse files
authored
Merge branch 'master' into ssrf-url-name-test
2 parents b8248a2 + 5bbad0a commit e873094

14 files changed

Lines changed: 564 additions & 19 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ Examples of Fortune 500 companies using _EvoMaster_ are:
198198

199199
![](docs/img/video-player-flaticon.png)
200200

201+
* A [45-minute talk given at TestCon'25](https://www.youtube.com/watch?v=uKKRo3LrNiw&list=PLqYhGsQ9iSEoXaRmW9WQjjXJK_1NbLlZ6&index=15) on Fuzz Testing Web APIs gives an overview of what can be expected from this kind of fuzzers.
202+
201203
* A [short video](https://youtu.be/3mYxjgnhLEo) (5 minutes)
202204
shows the use of _EvoMaster_ on one of the
203205
case studies in [EMB](https://github.com/WebFuzzing/EMB).

core/src/main/kotlin/org/evomaster/core/EMConfig.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1162,7 +1162,7 @@ class EMConfig {
11621162

11631163
enum class Algorithm {
11641164
DEFAULT, SMARTS, MIO, RANDOM, WTS, MOSA, RW,
1165-
StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA, MuPlusLambdaEA // GA variants still work-in-progress.
1165+
StandardGA, MonotonicGA, SteadyStateGA, BreederGA, CellularGA, OnePlusLambdaLambdaGA, MuLambdaEA, MuPlusLambdaEA, LIPS // GA variants still work-in-progress.
11661166
}
11671167

11681168
@Cfg("The algorithm used to generate test cases. The default depends on whether black-box or white-box testing is done.")

core/src/main/kotlin/org/evomaster/core/Main.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,8 @@ class Main {
645645
EMConfig.Algorithm.StandardGA ->
646646
Key.get(object : TypeLiteral<StandardGeneticAlgorithm<GraphQLIndividual>>() {})
647647

648+
EMConfig.Algorithm.LIPS ->
649+
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.LIPSAlgorithm<GraphQLIndividual>>() {})
648650
EMConfig.Algorithm.MuPlusLambdaEA ->
649651
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<GraphQLIndividual>>() {})
650652

@@ -684,6 +686,8 @@ class Main {
684686

685687
EMConfig.Algorithm.RW ->
686688
Key.get(object : TypeLiteral<RandomWalkAlgorithm<RPCIndividual>>() {})
689+
EMConfig.Algorithm.LIPS ->
690+
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.LIPSAlgorithm<RPCIndividual>>() {})
687691

688692
EMConfig.Algorithm.MuPlusLambdaEA ->
689693
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<RPCIndividual>>() {})
@@ -722,6 +726,8 @@ class Main {
722726

723727
EMConfig.Algorithm.RW ->
724728
Key.get(object : TypeLiteral<RandomWalkAlgorithm<WebIndividual>>() {})
729+
EMConfig.Algorithm.LIPS ->
730+
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.LIPSAlgorithm<WebIndividual>>() {})
725731

726732
EMConfig.Algorithm.MuPlusLambdaEA ->
727733
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<WebIndividual>>() {})
@@ -769,6 +775,8 @@ class Main {
769775

770776
EMConfig.Algorithm.RW ->
771777
Key.get(object : TypeLiteral<RandomWalkAlgorithm<RestIndividual>>() {})
778+
EMConfig.Algorithm.LIPS ->
779+
Key.get(object : TypeLiteral<org.evomaster.core.search.algorithms.LIPSAlgorithm<RestIndividual>>() {})
772780
EMConfig.Algorithm.MuPlusLambdaEA ->
773781
Key.get(object : TypeLiteral<MuPlusLambdaEvolutionaryAlgorithm<RestIndividual>>() {})
774782
EMConfig.Algorithm.MuLambdaEA ->

core/src/main/kotlin/org/evomaster/core/problem/rest/service/RestIndividualBuilder.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ class RestIndividualBuilder {
102102
.forEach { it.revertToWeakReference() }
103103
other.resetLocalIdRecursively()
104104

105+
//avoid possible taint id conflicts
106+
other.seeAllActions().forEach { it.forceNewTaints() }
107+
105108
val duplicates = base.addInitializingActions(other.seeInitializingActions())
106109

107110
other.getFlattenMainEnterpriseActionGroup()!!.forEach { group ->
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package org.evomaster.core.search.algorithms
2+
3+
import com.google.inject.Inject
4+
import org.evomaster.core.EMConfig
5+
import org.evomaster.core.search.Individual
6+
import org.evomaster.core.search.algorithms.wts.WtsEvalIndividual
7+
import org.evomaster.client.java.instrumentation.shared.ObjectiveNaming
8+
import org.evomaster.core.search.service.IdMapper
9+
10+
/**
11+
* Linearly Independent Path-based Search (LIPS).
12+
*
13+
* A single-objective GA that optimizes one branch target at a time.
14+
*
15+
* - Initializes a random individual i and build the initial population P = random ∪ {i}.
16+
* - Maintains a current branch target.
17+
* - Per-target budget is a fair share of the global TIME/ACTIONS budget; switches target when the target is covered or its budget is exhausted.
18+
*/
19+
class LIPSAlgorithm<T> : AbstractGeneticAlgorithm<T>() where T : Individual {
20+
21+
@Inject
22+
private lateinit var idMapper: IdMapper
23+
24+
private var currentTarget: Int? = null
25+
private lateinit var budget: LipsBudget
26+
27+
override fun getType(): EMConfig.Algorithm = EMConfig.Algorithm.LIPS
28+
29+
override fun initPopulation() {
30+
population.clear()
31+
// 1) Generate Random Individual
32+
val i = sampleSuite()
33+
// 2) P <- RandomPopulation(ps-1) ∪ {i}
34+
population.add(i)
35+
while (population.size < config.populationSize) {
36+
population.add(sampleSuite())
37+
}
38+
// Initialize budget manager and first per-target budget using current uncovered targets
39+
budget = LipsBudget(config, time)
40+
val initUncoveredSize = archive.notCoveredTargets().size
41+
budget.budgetLeftForCurrentTarget = budget.computePerTargetBudget(initUncoveredSize)
42+
}
43+
44+
override fun searchOnce() {
45+
beginGeneration()
46+
// record budget usage for this generation
47+
val startActions = time.evaluatedActions
48+
val startSeconds = time.getElapsedSeconds()
49+
50+
// Compute uncovered goals
51+
val uncovered = archive.notCoveredTargets()
52+
53+
// current target is null if covered by previous generation or out of budget
54+
// Pick target if null, or if previously covered (check coverage directly)
55+
val needNewTarget = currentTarget == null || archive.isCovered(currentTarget!!)
56+
if (needNewTarget) {
57+
val target = lastUncoveredBranchTargetId()
58+
currentTarget = target
59+
// Initialize budget for this NEW target
60+
budget.budgetLeftForCurrentTarget = budget.computePerTargetBudget(uncovered.size)
61+
}
62+
63+
// Focus scoring on the single selected target if present; otherwise use global fitness
64+
if (currentTarget == null) {
65+
frozenTargets = emptySet()
66+
} else {
67+
frozenTargets = setOf(currentTarget!!)
68+
}
69+
70+
val n = config.populationSize
71+
val nextPop: MutableList<WtsEvalIndividual<T>> = formTheNextPopulation(population)
72+
73+
while (nextPop.size < n) {
74+
beginStep()
75+
76+
val p1 = tournamentSelection()
77+
val p2 = tournamentSelection()
78+
79+
val o1 = p1.copy()
80+
val o2 = p2.copy()
81+
82+
if (randomness.nextBoolean(config.xoverProbability)) {
83+
xover(o1, o2)
84+
}
85+
if (randomness.nextBoolean(config.fixedRateMutation)) {
86+
mutate(o1)
87+
}
88+
if (randomness.nextBoolean(config.fixedRateMutation)) {
89+
mutate(o2)
90+
}
91+
92+
nextPop.add(o1)
93+
nextPop.add(o2)
94+
95+
// Stop if global budget or target budget is up
96+
val usedForTarget = budget.usedForCurrentTarget(startActions, startSeconds)
97+
if (!time.shouldContinueSearch() || usedForTarget >= budget.budgetLeftForCurrentTarget) {
98+
endStep()
99+
break
100+
}
101+
endStep()
102+
}
103+
104+
population.clear()
105+
population.addAll(nextPop)
106+
107+
// Update budget usage for this target
108+
budget.updatePerTargetBudget(startActions, startSeconds)
109+
110+
// Switch target if covered or out of budget
111+
val coveredNow = population.any { score(it) >= 1.0 }
112+
val switching = budget.shouldSwitchTarget(coveredNow)
113+
if (switching) currentTarget = null
114+
115+
endGeneration()
116+
}
117+
118+
fun lastUncoveredBranchTargetId(): Int? {
119+
val snapshot = archive.getSnapshotOfBestIndividuals()
120+
if (snapshot.isEmpty()) return null
121+
122+
// Iterate targets by numeric id in descending order
123+
val orderedIds = snapshot.keys.sortedDescending()
124+
125+
for (targetId in orderedIds) {
126+
val description = idMapper.getDescriptiveId(targetId)
127+
val isBranch = description.startsWith(ObjectiveNaming.BRANCH)
128+
val covered = archive.isCovered(targetId)
129+
if (isBranch) {
130+
if (!covered) {
131+
return targetId
132+
}
133+
}
134+
}
135+
return null
136+
}
137+
}
138+
139+
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.evomaster.core.search.algorithms
2+
3+
import org.evomaster.core.EMConfig
4+
import org.evomaster.core.search.service.SearchTimeController
5+
6+
/**
7+
* Encapsulates per-target budget accounting for LIPS.
8+
* It derives fair-share budgets from the global stopping criterion
9+
* and tracks the remaining budget for the current target.
10+
*/
11+
class LipsBudget(
12+
private val config: EMConfig,
13+
private val time: SearchTimeController
14+
) {
15+
16+
var budgetLeftForCurrentTarget: Int = 0
17+
18+
fun computePerTargetBudget(uncoveredSize: Int): Int {
19+
return when (config.stoppingCriterion) {
20+
EMConfig.StoppingCriterion.ACTION_EVALUATIONS -> {
21+
val remaining = (config.maxEvaluations - time.evaluatedActions).coerceAtLeast(0)
22+
if (uncoveredSize <= 0) remaining else remaining / uncoveredSize
23+
}
24+
EMConfig.StoppingCriterion.TIME -> {
25+
val remaining = (config.timeLimitInSeconds() - time.getElapsedSeconds()).coerceAtLeast(0)
26+
if (uncoveredSize <= 0) remaining else remaining / uncoveredSize
27+
}
28+
else -> Int.MAX_VALUE
29+
}
30+
}
31+
32+
fun usedForCurrentTarget(startActions: Int, startSeconds: Int): Int {
33+
return when (config.stoppingCriterion) {
34+
EMConfig.StoppingCriterion.ACTION_EVALUATIONS -> time.evaluatedActions - startActions
35+
EMConfig.StoppingCriterion.TIME -> time.getElapsedSeconds() - startSeconds
36+
else -> 0
37+
}
38+
}
39+
40+
fun updatePerTargetBudget(actionsAtGenStart: Int, secondsAtGenStart: Int) {
41+
val used = usedForCurrentTarget(actionsAtGenStart, secondsAtGenStart)
42+
budgetLeftForCurrentTarget -= used
43+
}
44+
45+
fun shouldSwitchTarget(coveredNow: Boolean): Boolean {
46+
val outOfBudget = budgetLeftForCurrentTarget <= 0
47+
return coveredNow || outOfBudget
48+
}
49+
}
50+
51+

core/src/main/kotlin/org/evomaster/core/search/gene/string/StringGene.kt

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,15 @@ class StringGene(
178178
// }
179179
// return true
180180
return maxLength > 0 //otherwise there is only 1 value, the empty string ""
181+
&& !isDependentTaint()
181182
}
182183

183184
override fun randomize(randomness: Randomness, tryToForceNewValue: Boolean) {
184185

186+
/*
187+
randomization does not apply any taint, and remove it if any is there
188+
*/
189+
185190
/*
186191
Even if through mutation we can get large string, we should
187192
avoid sampling very large strings by default
@@ -227,7 +232,7 @@ class StringGene(
227232
*/
228233
//assert(!tainted)
229234

230-
if(name == TaintInputName.TAINTED_MAP_EM_LABEL_IDENTIFIER){
235+
if(isDependentTaint()){
231236
/*
232237
TODO should have a better check to specify a StringGene is immutable.
233238
If we end up in other cases for this, should add an "immutable" field to this gene
@@ -267,6 +272,12 @@ class StringGene(
267272
//not applied if used data pool
268273
if (!successfulDataPool && config.taintOnSampling) {
269274

275+
/*
276+
the method is called when gene is globally initialized, which is done when sampling.
277+
when sampling a new gene, and we want to use taint, when we can check if using
278+
global info or a direct taint value
279+
*/
280+
270281
if (state.spa.hasInfoFor(name) && randomness.nextDouble() < state.config.useGlobalTaintInfoProbability) {
271282
val spec = state.spa.chooseSpecialization(name, randomness)!!
272283
assert(specializations.size == 0)
@@ -305,27 +316,40 @@ class StringGene(
305316
}
306317

307318

308-
override fun shallowMutate(randomness: Randomness, apc: AdaptiveParameterControl, mwc: MutationWeightControl,
309-
selectionStrategy: SubsetGeneMutationSelectionStrategy, enableAdaptiveGeneMutation: Boolean, additionalGeneMutationInfo: AdditionalGeneMutationInfo?) : Boolean{
319+
override fun shallowMutate(
320+
randomness: Randomness,
321+
apc: AdaptiveParameterControl,
322+
mwc: MutationWeightControl,
323+
selectionStrategy: SubsetGeneMutationSelectionStrategy,
324+
enableAdaptiveGeneMutation: Boolean,
325+
additionalGeneMutationInfo: AdditionalGeneMutationInfo?
326+
) : Boolean{
310327

311328
val allGenes = getAllGenesInIndividual()
312329

313330
if (enableAdaptiveGeneMutation){
314-
additionalGeneMutationInfo?:throw IllegalArgumentException("archive-based gene mutation cannot be applied without AdditionalGeneMutationInfo")
331+
additionalGeneMutationInfo
332+
?: throw IllegalArgumentException("archive-based gene mutation cannot be applied without AdditionalGeneMutationInfo")
333+
315334
additionalGeneMutationInfo.archiveGeneMutator.mutateStringGene(
316-
this, allGenes = allGenes, selectionStrategy = selectionStrategy, targets = additionalGeneMutationInfo.targets, additionalGeneMutationInfo = additionalGeneMutationInfo, changeSpecSetting = PROB_CHANGE_SPEC
335+
this,
336+
allGenes = allGenes,
337+
selectionStrategy = selectionStrategy,
338+
targets = additionalGeneMutationInfo.targets,
339+
additionalGeneMutationInfo = additionalGeneMutationInfo,
340+
changeSpecSetting = PROB_CHANGE_SPEC
317341
)
318342
return true
319343
}
320344

321345
val didSpecializationMutation = standardSpecializationMutation(
322346
randomness, apc, mwc, selectionStrategy, allGenes, enableAdaptiveGeneMutation, additionalGeneMutationInfo
323347
)
348+
324349
if (!didSpecializationMutation){
325-
standardValueMutation(
326-
randomness, allGenes, apc
327-
)
350+
standardValueMutation(randomness, allGenes, apc)
328351
}
352+
329353
return true
330354
}
331355

@@ -401,6 +425,11 @@ class StringGene(
401425
if(TaintInputName.isTaintInput(value)){
402426
//standard mutation on a tainted value makes little sense, so randomize instead
403427
randomize(randomness, true)
428+
/*
429+
TODO weird... if we return here (as logically we should do), few tests start failing...
430+
is there really a logical bug? or those are just brittle, seed-dependent tests?
431+
*/
432+
//return
404433
}
405434

406435
val p = randomness.nextDouble()
@@ -473,6 +502,9 @@ class StringGene(
473502
return false
474503
}
475504

505+
// we start with a high value for probability of using a tained value.
506+
// however, during search we decrease it, but not lower than specified minimum here, until start of focused search.
507+
// when focus search starts, we no longer use taint values
476508
val minPforTaint = 0.1
477509
val tp = apc.getBaseTaintAnalysisProbability(minPforTaint)
478510

@@ -1079,7 +1111,8 @@ class StringGene(
10791111
}
10801112

10811113
override fun evolve() {
1082-
//TODO need refactoring
1114+
//there are a lot of edge cases here...
1115+
throw IllegalStateException("String genes should not be evolved directly, but via mutation")
10831116
}
10841117

10851118
/**

core/src/main/kotlin/org/evomaster/core/search/service/Archive.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -700,5 +700,4 @@ class Archive<T> where T : Individual {
700700
it.individual.seeTopGenes().all { g-> g.isLocallyValid() }
701701
}
702702
}
703-
704703
}

core/src/main/kotlin/org/evomaster/core/search/service/mutator/StandardMutator.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,9 @@ open class StandardMutator<T> : Mutator<T>() where T : Individual {
165165

166166
if(config.taintForceSelectionOfGenesWithSpecialization){
167167
/*
168-
FIXME this should be removed, and rather handled with an "evolve".
169-
but need refactoring of StringGene mutation
168+
Note that some gene are directly "evolved()" in TaintAnalysis.
169+
however, StringGene is very special, and treated ad-hoc, by forcing
170+
selection here
170171
*/
171172
TaintAnalysis.dormantGenes(individual)
172173
.forEach {

0 commit comments

Comments
 (0)