Skip to content

Commit 7b9c564

Browse files
committed
feat: Add explain sub-commant to get details about response codes, error reasons, etc.
1 parent e4cd5b9 commit 7b9c564

File tree

5 files changed

+152
-14
lines changed

5 files changed

+152
-14
lines changed

src/main/java/com/endava/cats/command/CatsCommand.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@
105105
StatsCommand.class,
106106
ValidateCommand.class,
107107
RandomCommand.class,
108-
GenerateCommand.class
108+
GenerateCommand.class,
109+
ExplainCommand.class
109110
})
110111
public class CatsCommand implements Runnable, CommandLine.IExitCodeGenerator {
111112

@@ -447,5 +448,4 @@ private List<FuzzingData> filterFuzzingData(List<FuzzingData> fuzzingDataListWit
447448
public int getExitCode() {
448449
return exitCodeDueToErrors + executionStatisticsListener.getErrors();
449450
}
450-
451451
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package com.endava.cats.command;
2+
3+
import com.endava.cats.fuzzer.api.Fuzzer;
4+
import com.endava.cats.fuzzer.special.mutators.api.Mutator;
5+
import com.endava.cats.model.CatsResponse;
6+
import com.endava.cats.model.CatsResultFactory;
7+
import com.endava.cats.util.VersionProvider;
8+
import io.github.ludovicianul.prettylogger.PrettyLogger;
9+
import io.github.ludovicianul.prettylogger.PrettyLoggerFactory;
10+
import io.quarkus.arc.Unremovable;
11+
import jakarta.enterprise.inject.Instance;
12+
import jakarta.inject.Inject;
13+
import picocli.CommandLine;
14+
15+
import java.util.Arrays;
16+
import java.util.Locale;
17+
18+
@CommandLine.Command(
19+
name = "explain",
20+
mixinStandardHelpOptions = true,
21+
usageHelpAutoWidth = true,
22+
exitCodeOnInvalidInput = 191,
23+
exitCodeOnExecutionException = 192,
24+
exitCodeListHeading = "%n@|bold,underline Exit Codes:|@%n",
25+
exitCodeList = {"@|bold 0|@:Successful program execution",
26+
"@|bold 191|@:Usage error: user input for the command was incorrect",
27+
"@|bold 192|@:Internal execution error: an exception occurred when executing command"},
28+
footerHeading = "%n@|bold,underline Examples:|@%n",
29+
footer = {" Explain a 9XX response code:",
30+
" cats explain --type response_code 953",
31+
"", " Get more information about an error reason:",
32+
" cats explain --type error_reason \"Error details leak\"",},
33+
description = "Provides detailed information about a fuzzer, mutator, response code or error reason.",
34+
versionProvider = VersionProvider.class)
35+
@Unremovable
36+
public class ExplainCommand implements Runnable {
37+
private final PrettyLogger logger = PrettyLoggerFactory.getConsoleLogger();
38+
39+
@CommandLine.Option(names = {"-t", "--type"},
40+
description = "Output to console in JSON format.", required = true)
41+
private Type type;
42+
43+
@CommandLine.Parameters(index = "0",
44+
paramLabel = "<info>",
45+
description = "The information you want to get details about.")
46+
String info;
47+
48+
@Inject
49+
Instance<Fuzzer> fuzzers;
50+
51+
@Inject
52+
Instance<Mutator> mutators;
53+
54+
55+
@Override
56+
public void run() {
57+
switch (type) {
58+
case FUZZER -> displayFuzzerInfo();
59+
case MUTATOR -> displayMutatorInfo();
60+
case RESPONSE_CODE -> displayResponseCodeInfo();
61+
case ERROR_REASON -> displayErrorReason();
62+
}
63+
}
64+
65+
private void displayErrorReason() {
66+
Arrays.stream(CatsResultFactory.Reason.values()).filter(reason -> reason.name()
67+
.toLowerCase(Locale.ROOT).contains(info.toLowerCase(Locale.ROOT)))
68+
.sorted()
69+
.forEach(reason -> logger.noFormat("* Reason {} - {}", reason.reason(), reason.description()));
70+
}
71+
72+
private void displayFuzzerInfo() {
73+
fuzzers.stream().filter(fuzzer -> fuzzer.getClass().getSimpleName()
74+
.toLowerCase(Locale.ROOT).contains(info.toLowerCase(Locale.ROOT)))
75+
.sorted()
76+
.forEach(fuzzer -> logger.noFormat("* Fuzzer {} - {}", fuzzer.getClass().getSimpleName(), fuzzer.description()));
77+
}
78+
79+
private void displayMutatorInfo() {
80+
mutators.stream().filter(mutator -> mutator.getClass().getSimpleName()
81+
.toLowerCase(Locale.ROOT).contains(info.toLowerCase(Locale.ROOT)))
82+
.sorted()
83+
.forEach(mutator -> logger.noFormat("* Mutator {} - {}", mutator.getClass().getSimpleName(), mutator.description()));
84+
}
85+
86+
private void displayResponseCodeInfo() {
87+
Arrays.stream(CatsResponse.ExceptionalResponse.values())
88+
.map(CatsResponse.ExceptionalResponse::asString)
89+
.filter(response -> response.toLowerCase(Locale.ROOT).contains(info.toLowerCase(Locale.ROOT)))
90+
.toList().stream().sorted()
91+
.forEach(logger::noFormat);
92+
}
93+
94+
public enum Type {
95+
FUZZER,
96+
MUTATOR,
97+
RESPONSE_CODE,
98+
ERROR_REASON
99+
}
100+
}

src/main/java/com/endava/cats/fuzzer/special/mutators/api/Mutator.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* Implementations of this interface provide methods to apply various types of mutations
1010
* to input strings, producing modified output strings.
1111
*/
12-
public interface Mutator {
12+
public interface Mutator extends Comparable<Mutator> {
1313

1414
/**
1515
* Applies a mutation to the input string.
@@ -35,4 +35,8 @@ public interface Mutator {
3535
* @return the description of the mutator
3636
*/
3737
String description();
38+
39+
default int compareTo(Mutator o) {
40+
return this.getClass().getSimpleName().compareTo(o.getClass().getSimpleName());
41+
}
3842
}

src/main/java/com/endava/cats/model/CatsResponse.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ public int responseCode() {
216216
public String responseBody() {
217217
return responseBody;
218218
}
219+
220+
public String asString() {
221+
return responseCode + " - " + this.name().toLowerCase(Locale.ROOT) + " - " + responseBody;
222+
}
219223
}
220224

221225
public static class CatsResponseBuilder {

src/main/java/com/endava/cats/model/CatsResultFactory.java

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public interface CatsResultFactory {
1515
*/
1616
static CatsResult createExpectedResponse(String receivedResponseCode) {
1717
String message = "Response matches expected result. Response code [%s] is documented and response body matches the corresponding schema.".formatted(receivedResponseCode);
18-
String reason = "All Good!";
18+
String reason = Reason.ALL_GOOD.description();
1919

2020
return new CatsResult(message, reason);
2121
}
@@ -28,7 +28,7 @@ static CatsResult createExpectedResponse(String receivedResponseCode) {
2828
*/
2929
static CatsResult createNotMatchingResponseSchema(String receivedResponseCode) {
3030
String message = "Response does NOT match expected result. Response code [%s] is documented, but response body does NOT match the corresponding schema.".formatted(receivedResponseCode);
31-
String reason = "Not matching response schema";
31+
String reason = Reason.NOT_MATCHING_RESPONSE_SCHEMA.description();
3232

3333
return new CatsResult(message, reason);
3434
}
@@ -42,7 +42,7 @@ static CatsResult createNotMatchingResponseSchema(String receivedResponseCode) {
4242
*/
4343
static CatsResult createNotMatchingContentType(List<String> expected, String actual) {
4444
String message = "Response content type not matching the contract: expected %s, actual [%s]".formatted(expected, actual);
45-
String reason = "Response content type not matching the contract";
45+
String reason = Reason.RESPONSE_CONTENT_TYPE_NOT_MATCHING.description();
4646
return new CatsResult(message, reason);
4747
}
4848

@@ -52,7 +52,7 @@ static CatsResult createNotMatchingContentType(List<String> expected, String act
5252
* @return a CatsResult to use in reports
5353
*/
5454
static CatsResult createNotImplemented() {
55-
return new CatsResult("Response HTTP code 501: you forgot to implement this functionality!", "Not implemented");
55+
return new CatsResult("Response HTTP code 501: you forgot to implement this functionality!", Reason.NOT_IMPLEMENTED.description());
5656
}
5757

5858
/**
@@ -61,7 +61,7 @@ static CatsResult createNotImplemented() {
6161
* @return a CatsResult to use in reports
6262
*/
6363
static CatsResult createNotFound() {
64-
return new CatsResult("Response HTTP code 404: you might need to provide business context using --refData or --urlParams", "Not found");
64+
return new CatsResult("Response HTTP code 404: you might need to provide business context using --refData or --urlParams", Reason.NOT_FOUND.description());
6565
}
6666

6767
/**
@@ -73,7 +73,7 @@ static CatsResult createNotFound() {
7373
*/
7474
static CatsResult createResponseTimeExceedsMax(long receivedResponseTime, long maxResponseTime) {
7575
String message = "Test case executed successfully, but response time exceeds --maxResponseTimeInMs: actual %d, max %d".formatted(receivedResponseTime, maxResponseTime);
76-
String reason = "Response time exceeds max";
76+
String reason = Reason.RESPONSE_TIME_EXCEEDS_MAX.description();
7777

7878
return new CatsResult(message, reason);
7979
}
@@ -87,7 +87,7 @@ static CatsResult createResponseTimeExceedsMax(long receivedResponseTime, long m
8787
*/
8888
static CatsResult createUnexpectedException(String fuzzer, String errorMessage) {
8989
String message = "Fuzzer [%s] failed due to [%s]".formatted(fuzzer, errorMessage);
90-
String reason = "Unexpected exception";
90+
String reason = Reason.UNEXPECTED_EXCEPTION.description();
9191

9292
return new CatsResult(message, reason);
9393
}
@@ -100,7 +100,7 @@ static CatsResult createUnexpectedException(String fuzzer, String errorMessage)
100100
*/
101101
static CatsResult createErrorLeaksDetectedInResponse(List<String> keywords) {
102102
String message = "The following keywords were detected in the response which might suggest an error details leak: %s".formatted(keywords);
103-
String reason = "Error details leak";
103+
String reason = Reason.ERROR_LEAKS_DETECTED.description();
104104

105105
return new CatsResult(message, reason);
106106
}
@@ -115,7 +115,7 @@ static CatsResult createErrorLeaksDetectedInResponse(List<String> keywords) {
115115
*/
116116
static CatsResult createUnexpectedBehaviour(String receivedResponseCode, String expectedResponseCode) {
117117
String message = "Unexpected behaviour: expected %s, actual [%s]".formatted(expectedResponseCode, receivedResponseCode);
118-
String reason = "Unexpected behaviour: %s".formatted(receivedResponseCode);
118+
String reason = Reason.UNEXPECTED_BEHAVIOUR.description() + " %s".formatted(receivedResponseCode);
119119

120120
return new CatsResult(message, reason);
121121
}
@@ -129,7 +129,7 @@ static CatsResult createUnexpectedBehaviour(String receivedResponseCode, String
129129
*/
130130
static CatsResult createUnexpectedResponseCode(String receivedResponseCode, String expectedResponseCode) {
131131
String message = "Response does NOT match expected result. Response code is NOT from a list of expected codes for this FUZZER: expected %s, actual [%s]".formatted(expectedResponseCode, receivedResponseCode);
132-
String reason = "Unexpected response code: %s".formatted(receivedResponseCode);
132+
String reason = Reason.UNEXPECTED_RESPONSE_CODE.description() + ": %s".formatted(receivedResponseCode);
133133

134134
return new CatsResult(message, reason);
135135
}
@@ -144,7 +144,7 @@ static CatsResult createUnexpectedResponseCode(String receivedResponseCode, Stri
144144
*/
145145
static CatsResult createUndocumentedResponseCode(String receivedResponseCode, String expectedResponseCode, String documentedResponseCodes) {
146146
String message = "Response does NOT match expected result. Response code is from a list of expected codes for this FUZZER, but it is undocumented: expected %s, actual [%s], documented response codes: %s".formatted(expectedResponseCode, receivedResponseCode, documentedResponseCodes);
147-
String reason = "Undocumented response code: %s".formatted(receivedResponseCode);
147+
String reason = Reason.UNDOCUMENTED_RESPONSE_CODE.description() + ": %s".formatted(receivedResponseCode);
148148

149149
return new CatsResult(message, reason);
150150
}
@@ -158,4 +158,34 @@ static CatsResult createUndocumentedResponseCode(String receivedResponseCode, St
158158
*/
159159
record CatsResult(String message, String reason) {
160160
}
161+
162+
enum Reason {
163+
ALL_GOOD("All Good!", "The response matches the expected result"),
164+
NOT_MATCHING_RESPONSE_SCHEMA("Not matching response schema", "The response body does NOT match the corresponding schema defined in the OpenAPI contract"),
165+
NOT_IMPLEMENTED("Not implemented", "You forgot to implement this functionality!"),
166+
NOT_FOUND("Not found", "You might need to provide business context using --refData or --urlParams"),
167+
RESPONSE_TIME_EXCEEDS_MAX("Response time exceeds max", "The response time exceeds the maximum configured response time supplied using --maxResponseTimeInMs, default is 0 i.e no limit"),
168+
UNEXPECTED_EXCEPTION("Unexpected exception", "An unexpected exception occurred. This might suggest an issue with CATS itself"),
169+
ERROR_LEAKS_DETECTED("Error details leak", "The response contains error messages that might expose sensitive information"),
170+
UNEXPECTED_RESPONSE_CODE("Unexpected response code", "The response code is documented inside the contract, but not expected for the current fuzzer"),
171+
UNDOCUMENTED_RESPONSE_CODE("Undocumented response code", "The response code is expected for the current fuzzer, but not documented inside the contract"),
172+
RESPONSE_CONTENT_TYPE_NOT_MATCHING("Response content type not matching the contract", "The response content type does not match the one defined in the OpenAPI contract"),
173+
UNEXPECTED_BEHAVIOUR("Unexpected behaviour", "CATS run the test case successfully, but the response code was not expected, nor documented, nor known to typically be documented");
174+
175+
private final String reason;
176+
private final String description;
177+
178+
Reason(String reason, String description) {
179+
this.reason = reason;
180+
this.description = description;
181+
}
182+
183+
public String description() {
184+
return description;
185+
}
186+
187+
public String reason() {
188+
return reason;
189+
}
190+
}
161191
}

0 commit comments

Comments
 (0)