Skip to content

Commit

Permalink
feat: Allow continuous fuzzing when running in template fuzzing
Browse files Browse the repository at this point in the history
  • Loading branch information
en-milie committed Oct 14, 2024
1 parent 05b8d82 commit dc1fb58
Show file tree
Hide file tree
Showing 31 changed files with 247 additions and 82 deletions.
20 changes: 18 additions & 2 deletions src/main/java/com/endava/cats/command/TemplateFuzzCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
import com.endava.cats.args.MatchArguments;
import com.endava.cats.args.ProcessingArguments;
import com.endava.cats.args.ReportingArguments;
import com.endava.cats.args.StopArguments;
import com.endava.cats.args.UserArguments;
import com.endava.cats.model.CatsConfiguration;
import com.endava.cats.context.CatsGlobalContext;
import com.endava.cats.dsl.CatsDSLParser;
import com.endava.cats.fuzzer.special.TemplateFuzzer;
import com.endava.cats.http.HttpMethod;
import com.endava.cats.model.CatsConfiguration;
import com.endava.cats.model.CatsHeader;
import com.endava.cats.model.FuzzingData;
import com.endava.cats.report.TestCaseListener;
Expand All @@ -26,6 +27,7 @@
import io.swagger.v3.oas.models.OpenAPI;
import jakarta.inject.Inject;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import picocli.CommandLine;
Expand Down Expand Up @@ -100,6 +102,10 @@ public class TemplateFuzzCommand implements Runnable {
@CommandLine.ArgGroup(heading = "%n@|bold,underline Ignore Options:|@%n", exclusive = false)
IgnoreArguments ignoreArguments;

@Inject
@CommandLine.ArgGroup(heading = "%n@|bold,underline Stop Options:|@%n", exclusive = false)
StopArguments stopArguments;

@Inject
@CommandLine.ArgGroup(heading = "%n@|bold,underline Dictionary Options:|@%n", exclusive = false)
UserArguments userArguments;
Expand Down Expand Up @@ -136,6 +142,13 @@ public class TemplateFuzzCommand implements Runnable {
@CommandLine.Option(names = {"--targetFields", "-t"},
description = "A comma separated list of fully qualified request fields, HTTP headers, path and/or query parameters that the Fuzzers will apply to", split = ",")
Set<String> targetFields;

@Setter
@CommandLine.Option(names = {"--random"},
description = "When set to @|bold true|@, it will run continuous fuzzing. You must also supply a `--stopXXX` argument in order for the execution to stop. Default: @|bold,underline ${DEFAULT-VALUE}|@")
private boolean random;


@Inject
ProcessingArguments processingArguments;

Expand All @@ -153,7 +166,7 @@ public void run() {
.headers(this.getHeaders())
.targetFields(fieldsToFuzz)
.build();

templateFuzzer.setRandom(random);
beforeFuzz(fuzzingData.getContractPath(), fuzzingData.getMethod().name());
templateFuzzer.fuzz(fuzzingData);
afterFuzz(fuzzingData.getContractPath());
Expand Down Expand Up @@ -229,6 +242,9 @@ private void validateRequiredFields() throws CommandLine.ParameterException {
if (!matchArguments.isAnyMatchArgumentSupplied()) {
throw new CommandLine.ParameterException(spec.commandLine(), "At least one --matchXXX argument is required");
}
if (random && !stopArguments.isAnyStopConditionProvided()) {
throw new CommandLine.ParameterException(spec.commandLine(), "When running in continuous fuzzing mode, at least one --stopXXX argument must be provided");
}
}

private String loadPayload() throws IOException {
Expand Down
112 changes: 93 additions & 19 deletions src/main/java/com/endava/cats/fuzzer/special/TemplateFuzzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,31 @@

import com.endava.cats.annotations.SpecialFuzzer;
import com.endava.cats.args.MatchArguments;
import com.endava.cats.args.StopArguments;
import com.endava.cats.args.UserArguments;
import com.endava.cats.fuzzer.api.Fuzzer;
import com.endava.cats.fuzzer.special.mutators.api.BodyMutator;
import com.endava.cats.fuzzer.special.mutators.api.Mutator;
import com.endava.cats.generator.simple.StringGenerator;
import com.endava.cats.generator.simple.UnicodeGenerator;
import com.endava.cats.io.ServiceCaller;
import com.endava.cats.model.CatsHeader;
import com.endava.cats.model.CatsRequest;
import com.endava.cats.model.CatsResponse;
import com.endava.cats.model.FuzzingData;
import com.endava.cats.report.ExecutionStatisticsListener;
import com.endava.cats.report.TestCaseListener;
import com.endava.cats.strategy.FuzzingStrategy;
import com.endava.cats.util.CatsUtil;
import com.endava.cats.util.ConsoleUtils;
import com.endava.cats.util.JsonUtils;
import com.endava.cats.util.KeyValuePair;
import com.jayway.jsonpath.JsonPathException;
import io.github.ludovicianul.prettylogger.PrettyLogger;
import io.github.ludovicianul.prettylogger.PrettyLoggerFactory;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Singleton;
import lombok.Setter;

import java.io.IOException;
import java.net.URI;
Expand All @@ -35,18 +42,27 @@
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static com.endava.cats.util.JsonUtils.NOT_SET;

/**
* Fuzzer that will do fuzzing based on a supplied template, rather than an OpenAPI Spec.
*/
@Singleton
@SpecialFuzzer
public class TemplateFuzzer implements Fuzzer {
public static final String EMPTY = "";
private static final String FAKE_FUZZ = "FAKE_FUZZ";
private final PrettyLogger logger = PrettyLoggerFactory.getLogger(TemplateFuzzer.class);
private final ServiceCaller serviceCaller;
private final TestCaseListener testCaseListener;
private final UserArguments userArguments;
private final MatchArguments matchArguments;
private final StopArguments stopArguments;
private final Instance<BodyMutator> mutators;
private final ExecutionStatisticsListener executionStatisticsListener;

@Setter
private boolean random;

/**
* Constructs a new TemplateFuzzer instance.
Expand All @@ -56,16 +72,66 @@ public class TemplateFuzzer implements Fuzzer {
* @param ua The UserArguments object containing the user-specified arguments for the fuzzer.
* @param ma The MatchArguments object containing the match criteria for identifying valid responses.
*/
public TemplateFuzzer(ServiceCaller sc, TestCaseListener lr, UserArguments ua, MatchArguments ma) {
public TemplateFuzzer(ServiceCaller sc, TestCaseListener lr, UserArguments ua,
MatchArguments ma, StopArguments sa, Instance<BodyMutator> mutators,
ExecutionStatisticsListener el) {
this.serviceCaller = sc;
this.testCaseListener = lr;
this.userArguments = ua;
this.matchArguments = ma;
this.stopArguments = sa;
this.mutators = mutators;
this.executionStatisticsListener = el;
}

@Override
public void fuzz(FuzzingData data) {
testCaseListener.updateUnknownProgress(data);
if (random) {
runRandomFuzzing(data);
} else {
runNormalFuzzing(data);
}
}

private void runRandomFuzzing(FuzzingData data) {
long startTime = System.currentTimeMillis();

boolean shouldStop = false;
Set<String> allCatsFields = data.getTargetFields();
String fakePayloadToMutate = """
{"FAKE_FUZZ": "someValue"}
"""; // this is a fake payload that will be mutated

while (!shouldStop) {
String targetField = CatsUtil.selectRandom(allCatsFields);
logger.debug("Selected field to be mutated: [{}]", targetField);

Mutator selectedRandomMutator = CatsUtil.selectRandom(mutators);
logger.debug("Selected mutator [{}]", selectedRandomMutator.getClass().getSimpleName());

String fakeMutatedPayload = selectedRandomMutator.mutate(fakePayloadToMutate, FAKE_FUZZ);
Object mutatedValue = JsonUtils.getVariableFromJson(fakeMutatedPayload, FAKE_FUZZ);
if (JsonUtils.isNotSet(String.valueOf(mutatedValue))) {
mutatedValue = fakeMutatedPayload.substring(1, fakeMutatedPayload.length() - 1);
}

String mutatedPayload = data.getPayload();
if (data.getPayload().contains(targetField)) {
if (userArguments.isNameReplace()) {
mutatedPayload = data.getPayload().replace(targetField, String.valueOf(mutatedValue));
} else {
mutatedPayload = selectedRandomMutator.mutate(data.getPayload(), targetField);
}
}

createRequestAndExecuteTest(data, targetField, String.valueOf(mutatedValue), mutatedPayload, selectedRandomMutator.description());

shouldStop = stopArguments.shouldStop(executionStatisticsListener.getErrors(), testCaseListener.getCurrentTestCaseNumber(), startTime);
}
}

private void runNormalFuzzing(FuzzingData data) {
for (String targetField : Optional.ofNullable(data.getTargetFields()).orElse(Collections.emptySet())) {
int payloadSize = this.getPayloadSize(data, targetField);

Expand All @@ -76,37 +142,46 @@ public void fuzz(FuzzingData data) {
logger.info("Running {} payloads for field [{}]", payloads.size(), targetField);

for (String payload : payloads) {
List<KeyValuePair<String, Object>> replacedHeaders = this.replaceHeaders(data, payload, targetField);
String replacedPayload = this.replacePayload(data, payload, targetField);
String replacedPath = this.replacePath(data, payload, targetField);

CatsRequest catsRequest = CatsRequest.builder()
.payload(replacedPayload)
.headers(replacedHeaders)
.httpMethod(data.getMethod().name())
.url(replacedPath)
.build();

testCaseListener.createAndExecuteTest(logger, this, () -> process(data, catsRequest, targetField, payload), data);
testCaseListener.updateUnknownProgress(data);
createRequestAndExecuteTest(data, targetField, payload, replacedPayload, FuzzingStrategy.replace().withData(payload).truncatedValue());
}
}
}
}

private void createRequestAndExecuteTest(FuzzingData data, String targetField, Object payload, String replacedPayload, String fuzzDescription) {
List<KeyValuePair<String, Object>> replacedHeaders = this.replaceHeaders(data, payload, targetField);
String replacedPath = this.replacePath(data, String.valueOf(payload), targetField);

CatsRequest catsRequest = CatsRequest.builder()
.payload(replacedPayload)
.headers(replacedHeaders)
.httpMethod(data.getMethod().name())
.url(replacedPath)
.build();

testCaseListener.createAndExecuteTest(logger, this, () -> process(data, catsRequest, targetField, payload, fuzzDescription), data);
testCaseListener.updateUnknownProgress(data);
}

String replacePath(FuzzingData data, String withData, String targetField) {
if (userArguments.isNameReplace()) {
return data.getPath().replace(targetField, Optional.ofNullable(withData).orElse(EMPTY));
}

String finalPath = data.getPath();
try {
//handle http://localhost:8080/
URL url = URI.create(data.getPath()).toURL();
String replacedPath = Arrays.stream(url.getPath().split("/"))
.map(pathElement -> pathElement.equalsIgnoreCase(targetField) ? withData : pathElement)
.collect(Collectors.joining("/"));

finalPath = finalPath.replace(url.getPath(), replacedPath);
if (!"/".equals(url.getPath())) {
finalPath = finalPath.replace(url.getPath(), replacedPath);
}

if (url.getQuery() != null) {
String replacedQuery = Arrays.stream(url.getQuery().split("&"))
.map(queryParam -> replaceQueryParam(targetField, queryParam, withData))
Expand All @@ -122,7 +197,7 @@ String replacePath(FuzzingData data, String withData, String targetField) {
return finalPath;
}

String replaceQueryParam(String targetField, String queryPair, String withValue) {
static String replaceQueryParam(String targetField, String queryPair, String withValue) {
String[] queryPairArr = queryPair.split("=", -1);
if (queryPairArr[0].equalsIgnoreCase(targetField) && queryPairArr.length == 2) {
return queryPairArr[0] + "=" + withValue;
Expand All @@ -139,7 +214,6 @@ private List<String> getAllPayloads(int payloadSize) {
payloads.add(UnicodeGenerator.getBadPayload());
payloads.add(UnicodeGenerator.getZalgoText());
payloads.add(StringGenerator.generateLargeString(20000));
payloads.add(null);
payloads.add(EMPTY);
return payloads;
} else {
Expand Down Expand Up @@ -181,7 +255,7 @@ private int getPayloadSize(FuzzingData data, String field) {
if (oldValue.isEmpty()) {
oldValue = String.valueOf(JsonUtils.getVariableFromJson(data.getPayload(), field));
}
if (oldValue.equalsIgnoreCase(JsonUtils.NOT_SET)) {
if (oldValue.equalsIgnoreCase(NOT_SET)) {
oldValue = data.getHeaders().stream()
.filter(header -> header.getName().equalsIgnoreCase(field))
.map(CatsHeader::getValue)
Expand All @@ -195,8 +269,8 @@ private int getPayloadSize(FuzzingData data, String field) {
return oldValue.length();
}

private void process(FuzzingData data, CatsRequest catsRequest, String targetField, String fuzzedValue) {
testCaseListener.addScenario(logger, "Replace request field, header or path/query param [{}], with [{}]", targetField, FuzzingStrategy.replace().withData(fuzzedValue).truncatedValue());
private void process(FuzzingData data, CatsRequest catsRequest, String targetField, Object fuzzedValue, String fuzzDescription) {
testCaseListener.addScenario(logger, "Replace request field, header or path/query param [{}], with [{}]", targetField, fuzzDescription);
testCaseListener.addExpectedResult(logger, "Should get a response that doesn't match given arguments");
testCaseListener.addRequest(catsRequest);
testCaseListener.addPath(catsRequest.getUrl());
Expand Down Expand Up @@ -225,7 +299,7 @@ private void process(FuzzingData data, CatsRequest catsRequest, String targetFie
}
}

private void checkResponse(CatsResponse catsResponse, FuzzingData data, String fuzzedValue) {
private void checkResponse(CatsResponse catsResponse, FuzzingData data, Object fuzzedValue) {
if (matchArguments.isMatchResponse(catsResponse) || matchArguments.isInputReflected(catsResponse, fuzzedValue) || !matchArguments.isAnyMatchArgumentSupplied()) {
testCaseListener.addResponse(catsResponse);
testCaseListener.reportResultError(logger, data, "Response matches arguments", "Response matches" + matchArguments.getMatchString());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.endava.cats.fuzzer.special.mutators.api;

import com.endava.cats.model.CatsHeader;

import java.util.Collection;

/**
* Marker interface for mutators that mutate the request body.
*/
public interface BodyMutator extends Mutator {


@Override
default Collection<CatsHeader> mutate(Collection<CatsHeader> headers) {
return headers;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.endava.cats.fuzzer.special.mutators.api;

import com.endava.cats.util.JsonUtils;
import com.endava.cats.util.CatsUtil;
import com.endava.cats.util.JsonUtils;

/**
* Executes mutator logic from a custom mutator file.
Expand All @@ -11,7 +11,7 @@
* then transformed into {@code CustomMutator} instances.
* </p>
*/
public class CustomMutator implements Mutator {
public class CustomMutator implements BodyMutator {
private final CustomMutatorConfig customMutatorConfig;

public CustomMutator(CustomMutatorConfig customMutatorConfig) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.endava.cats.fuzzer.special.mutators.api;

/**
* Marker interface for mutators that mutate the request headers.
*/
public interface HeadersMutator extends Mutator {

@Override
default String mutate(String inputJson, String selectedField) {
return inputJson;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ public interface Mutator {
* @param selectedField The field within the JSON which is the primary target of mutation
* @return The mutated output string
*/
default String mutate(String inputJson, String selectedField) {
return inputJson;
}
String mutate(String inputJson, String selectedField);


/**
Expand All @@ -29,9 +27,7 @@ default String mutate(String inputJson, String selectedField) {
* @param headers the request headers
* @return a list of headers with at least one mutated
*/
default Collection<CatsHeader> mutate(Collection<CatsHeader> headers) {
return headers;
}
Collection<CatsHeader> mutate(Collection<CatsHeader> headers);

/**
* The name of the mutator.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.endava.cats.fuzzer.special.mutators.impl;

import com.endava.cats.fuzzer.special.mutators.api.Mutator;
import com.endava.cats.fuzzer.special.mutators.api.BodyMutator;
import com.endava.cats.util.CatsUtil;
import jakarta.inject.Singleton;

/**
* Sends null value in the target field.
*/
@Singleton
public class NullStringMutator implements Mutator {
public class NullStringMutator implements BodyMutator {

@Override
public String mutate(String inputJson, String selectedField) {
Expand Down
Loading

0 comments on commit dc1fb58

Please sign in to comment.