Skip to content

Commit e183846

Browse files
authored
Merge pull request #7573 from kingthorin/graphql-limit
graphql: Allow limiting the number of messages imported
2 parents cac4e37 + e92a271 commit e183846

11 files changed

Lines changed: 159 additions & 24 deletions

File tree

addOns/graphql/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this add-on will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
55

66
## Unreleased
7+
### Added
8+
- Allow Automation Framework users to limit the number of GraphQL messages to import (`maxMessages`). Ex: If testing authentication, access, etc.
9+
710
### Changed
811
- Update dependency.
912
- Maintenance changes.

addOns/graphql/src/main/java/org/zaproxy/addon/graphql/GraphQlGenerator.java

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,25 +64,34 @@ public enum RequestType {
6464
}
6565

6666
private final ValueProvider valueProvider;
67+
private final int maxMessages;
68+
private int messagesSent;
6769

6870
public GraphQlGenerator(
69-
ValueProvider valueProvider, String sdl, Requestor requestor, GraphQlParam param) {
71+
ValueProvider valueProvider,
72+
String sdl,
73+
Requestor requestor,
74+
GraphQlParam param,
75+
int maxMessages) {
7076
this(
7177
valueProvider,
7278
UnExecutableSchemaGenerator.makeUnExecutableSchema(new SchemaParser().parse(sdl)),
7379
requestor,
74-
param);
80+
param,
81+
maxMessages);
7582
}
7683

7784
public GraphQlGenerator(
7885
ValueProvider valueProvider,
7986
GraphQLSchema schema,
8087
Requestor requestor,
81-
GraphQlParam param) {
88+
GraphQlParam param,
89+
int maxMessages) {
8290
this.valueProvider = valueProvider;
8391
this.schema = schema;
8492
this.requestor = requestor;
8593
this.param = param;
94+
this.maxMessages = maxMessages;
8695
this.inlineArgsEnabled = param.getArgsType() == GraphQlParam.ArgsTypeOption.INLINE;
8796
}
8897

@@ -180,8 +189,7 @@ private void sendFull(RequestType requestType) {
180189
StringBuilder query = new StringBuilder();
181190
JSONObject variables = new JSONObject();
182191
generate(query, variables, getRequestTypeObject(requestType), 0);
183-
prefixRequestType(query, requestType);
184-
requestor.sendQuery(query.toString(), variables.toString(), param.getRequestMethod());
192+
sendGeneratedQuery(query, variables, requestType);
185193
} catch (InterruptedException e) {
186194
// Do nothing.
187195
}
@@ -222,9 +230,23 @@ private void sendByField(RequestType requestType) {
222230
return;
223231
}
224232
query.append('}');
225-
prefixRequestType(query, requestType);
226-
requestor.sendQuery(query.toString(), variables.toString(), param.getRequestMethod());
233+
try {
234+
sendGeneratedQuery(query, variables, requestType);
235+
} catch (InterruptedException e) {
236+
return;
237+
}
238+
}
239+
}
240+
241+
private void sendGeneratedQuery(
242+
StringBuilder query, JSONObject variables, RequestType requestType)
243+
throws InterruptedException {
244+
if (maxMessages > 0 && messagesSent >= maxMessages) {
245+
throw new InterruptedException();
227246
}
247+
prefixRequestType(query, requestType);
248+
requestor.sendQuery(query.toString(), variables.toString(), param.getRequestMethod());
249+
messagesSent++;
228250
}
229251

230252
private GraphQLObjectType getRequestTypeObject(RequestType requestType) {
@@ -285,9 +307,7 @@ private void generate(
285307
query.append(getFirstLeafQuery(type, variables, variableName));
286308
if (requestor != null) {
287309
query.append(StringUtils.repeat("} ", depth));
288-
prefixRequestType(query, requestType);
289-
requestor.sendQuery(
290-
query.toString(), variables.toString(), param.getRequestMethod());
310+
sendGeneratedQuery(query, variables, requestType);
291311
}
292312
} else if (getFirstLeafField(type) == null) {
293313
LOGGER.warn(
@@ -318,9 +338,7 @@ private void generate(
318338
variableName.setLength(variableName.length() - field.getName().length() - 1);
319339
if (requestor != null) {
320340
query.append("} ".repeat(depth + 1));
321-
prefixRequestType(query, requestType);
322-
requestor.sendQuery(
323-
query.toString(), variables.toString(), param.getRequestMethod());
341+
sendGeneratedQuery(query, variables, requestType);
324342
}
325343
} else {
326344
query.append(field.getName()).append(' ');

addOns/graphql/src/main/java/org/zaproxy/addon/graphql/GraphQlParser.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public class GraphQlParser {
7676
private final ExtensionGraphQl extensionGraphQl;
7777
private final GraphQlParam param;
7878
private boolean syncParse;
79+
private int maxMessages;
7980

8081
// For Unit Tests
8182
protected GraphQlParser(String endpointUrlStr) throws URIException {
@@ -176,9 +177,15 @@ public void parse(String sdl) {
176177
UnExecutableSchemaGenerator.makeUnExecutableSchema(new SchemaParser().parse(sdl));
177178
var generator =
178179
new GraphQlGenerator(
179-
extensionGraphQl.getValueGenerator(), schema, requestor, param);
180+
extensionGraphQl.getValueGenerator(),
181+
schema,
182+
requestor,
183+
param,
184+
maxMessages);
180185
if (syncParse) {
181-
fingerprint();
186+
if (maxMessages <= 0) {
187+
fingerprint();
188+
}
182189
detectCycles(schema, generator);
183190
if (param.getQueryGenEnabled()) {
184191
generate(generator);
@@ -189,7 +196,9 @@ public void parse(String sdl) {
189196
new ParserThread(THREAD_PREFIX + threadId.incrementAndGet()) {
190197
@Override
191198
public void run() {
192-
fingerprint();
199+
if (maxMessages <= 0) {
200+
fingerprint();
201+
}
193202
detectCycles(schema, generator);
194203
if (param.getQueryGenEnabled()) {
195204
generate(generator);
@@ -200,6 +209,10 @@ public void run() {
200209
t.startParser();
201210
}
202211

212+
public void setMaxMessages(int maxMessages) {
213+
this.maxMessages = maxMessages;
214+
}
215+
203216
private void fingerprint() {
204217
new GraphQlFingerprinter(endpointUrl, requestor).fingerprint();
205218
}
@@ -210,7 +223,9 @@ private void detectCycles(GraphQLSchema schema, GraphQlGenerator generator) {
210223

211224
private void generate(GraphQlGenerator generator) {
212225
try {
213-
generator.checkServiceMethods();
226+
if (maxMessages <= 0) {
227+
generator.checkServiceMethods();
228+
}
214229
generator.generateAndSend();
215230
} catch (Exception e) {
216231
LOGGER.error(e.getMessage(), e);

addOns/graphql/src/main/java/org/zaproxy/addon/graphql/automation/GraphQlJob.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public class GraphQlJob extends AutomationJob {
5050
private static final String PARAM_ENDPOINT = "endpoint";
5151
private static final String PARAM_SCHEMA_URL = "schemaUrl";
5252
private static final String PARAM_SCHEMA_FILE = "schemaFile";
53+
private static final String PARAM_MAX_MESSAGES = "maxMessages";
5354

5455
private static final String RESOURCES_DIR = "/org/zaproxy/addon/graphql/resources/";
5556

@@ -72,6 +73,14 @@ public void verifyParameters(AutomationProgress progress) {
7273
this.getName(),
7374
null,
7475
progress);
76+
77+
if (getParameters().getMaxMessages() < 0) {
78+
progress.warn(
79+
Constant.messages.getString(
80+
"graphql.automation.warn.maxMessages",
81+
getName(),
82+
getParameters().getMaxMessages()));
83+
}
7584
}
7685

7786
@Override
@@ -80,7 +89,9 @@ public void applyParameters(AutomationProgress progress) {
8089
this.parameters,
8190
JobUtils.getJobOptions(this, progress),
8291
this.getName(),
83-
new String[] {PARAM_ENDPOINT, PARAM_SCHEMA_URL, PARAM_SCHEMA_FILE},
92+
new String[] {
93+
PARAM_ENDPOINT, PARAM_SCHEMA_URL, PARAM_SCHEMA_FILE, PARAM_MAX_MESSAGES
94+
},
8495
progress,
8596
this.getPlan().getEnv());
8697
}
@@ -91,6 +102,7 @@ public Map<String, String> getCustomConfigParameters() {
91102
map.put(PARAM_ENDPOINT, "");
92103
map.put(PARAM_SCHEMA_URL, "");
93104
map.put(PARAM_SCHEMA_FILE, "");
105+
map.put(PARAM_MAX_MESSAGES, "0");
94106
return map;
95107
}
96108

@@ -108,6 +120,7 @@ public void runJob(AutomationEnvironment env, AutomationProgress progress) {
108120
GraphQlParser parser =
109121
new GraphQlParser(endpointUrl, HttpSender.MANUAL_REQUEST_INITIATOR, true);
110122
parser.addRequesterListener(new HistoryPersister());
123+
parser.setMaxMessages(getParameters().getMaxMessages());
111124

112125
String schemaFile = this.getParameters().getSchemaFile();
113126
String schemaUrl = this.getParameters().getSchemaUrl();
@@ -217,6 +230,7 @@ public static class Parameters extends AutomationData {
217230
private String endpoint;
218231
private String schemaUrl;
219232
private String schemaFile;
233+
private int maxMessages;
220234
private Boolean queryGenEnabled = GraphQlParam.DEFAULT_QUERY_GEN_ENABLED;
221235
private Integer maxQueryDepth = GraphQlParam.DEFAULT_MAX_QUERY_DEPTH;
222236
private Boolean lenientMaxQueryDepthEnabled = GraphQlParam.DEFAULT_LENIENT_MAX_QUERY_DEPTH;

addOns/graphql/src/main/java/org/zaproxy/addon/graphql/automation/GraphQlJobDialog.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public class GraphQlJobDialog extends StandardFieldsDialog {
4747
private static final String ENDPOINT_PARAM = "graphql.automation.dialog.endpoint";
4848
private static final String SCHEMA_URL_PARAM = "graphql.automation.dialog.schemaurl";
4949
private static final String SCHEMA_FILE_PARAM = "graphql.automation.dialog.schemafile";
50+
private static final String MAX_MESSAGES_PARAM = "graphql.automation.dialog.maxmessages";
5051

5152
private static final String QUERY_GEN_ENABLED_PARAM = "graphql.automation.dialog.querygen";
5253
private static final String MAX_QUERY_DEPTH_PARAM = "graphql.automation.dialog.maxquerydepth";
@@ -103,6 +104,12 @@ public GraphQlJobDialog(GraphQlJob job) {
103104
if (fileName != null && JobUtils.containsVars(fileName)) {
104105
setFieldValue(SCHEMA_FILE_PARAM, fileName);
105106
}
107+
this.addNumberField(
108+
0,
109+
MAX_MESSAGES_PARAM,
110+
0,
111+
Integer.MAX_VALUE,
112+
this.job.getParameters().getMaxMessages());
106113

107114
this.addCheckBoxField(
108115
0,
@@ -197,6 +204,7 @@ public void save() {
197204
this.job.getParameters().setEndpoint(this.getStringValue(ENDPOINT_PARAM));
198205
this.job.getParameters().setSchemaUrl(this.getStringValue(SCHEMA_URL_PARAM));
199206
this.job.getParameters().setSchemaFile(this.getStringValue(SCHEMA_FILE_PARAM));
207+
this.job.getParameters().setMaxMessages(this.getIntValue(MAX_MESSAGES_PARAM));
200208

201209
boolean queryGenEnabled = getBoolValue(QUERY_GEN_ENABLED_PARAM);
202210
this.job.getParameters().setQueryGenEnabled(queryGenEnabled);

addOns/graphql/src/main/javahelp/org/zaproxy/addon/graphql/resources/help/contents/automation.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ <H2>Job: graphql</H2>
3434
requestMethod: # Enum [post_json, post_graphql, get]: The request method, default: post_json
3535
cycleDetectionMode: # Enum [disabled, quick, exhaustive]: The cycle detection mode, default: quick
3636
maxCycleDetectionAlerts: # Int: The maximum number of alerts to raise for detected cycles, default: 100
37+
maxMessages: # Int: Maximum number of messages to import, default: 0, import all messages
3738
</pre>
39+
<p>
40+
When <code>maxMessages</code> is set (greater than 0), <a href="alerts.html">fingerprinting</a> and service method checks are skipped.
41+
</p>
42+
<p>
43+
This limit is soft: redirects are followed, so the number of messages persisted may exceed the configured value.
44+
</p>
3845

3946
<H2>See also</H2>
4047
<table>

addOns/graphql/src/main/resources/org/zaproxy/addon/graphql/resources/Messages.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ graphql.automation.dialog.lenientmaxquery = Lenient Max Query Depth Enabled:
4646
graphql.automation.dialog.maxCycleAlerts = Max Alerts:
4747
graphql.automation.dialog.maxaddquerydepth = Max Additional Query Depth:
4848
graphql.automation.dialog.maxargsdepth = Max Arguments Depth:
49+
graphql.automation.dialog.maxmessages = Max Messages:
4950
graphql.automation.dialog.maxquerydepth = Max Query Depth:
5051
graphql.automation.dialog.name = Job Name:
5152
graphql.automation.dialog.optargsenabled = Optional Arguments Enabled:
@@ -64,6 +65,7 @@ graphql.automation.info.import.file = Job graphql importing schema from file: {0
6465
graphql.automation.info.import.introspect = Job graphql importing schema using introspection from: {0}
6566
graphql.automation.info.import.url = Job graphql importing schema from URL: {0} target: {1}
6667
graphql.automation.name = GraphQL Automation
68+
graphql.automation.warn.maxMessages = Job {0} maxMessages must be zero or greater, was: {1}
6769

6870
graphql.cmdline.endurl.help = Sets the Endpoint URL
6971
graphql.cmdline.file.help = Imports a GraphQL Schema from a File

addOns/graphql/src/main/resources/org/zaproxy/addon/graphql/resources/graphql-max.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414
requestMethod: # Enum [post_json, post_graphql, get]: The request method, default: post_json
1515
cycleDetectionMode: # Enum [disabled, quick, exhaustive]: The cycle detection mode, default: quick
1616
maxCycleDetectionAlerts: # Int: The maximum number of alerts to raise for detected cycles, default: 100
17+
maxMessages: # Int: Maximum number of messages to import, default: 0, import all messages

addOns/graphql/src/test/java/org/zaproxy/addon/graphql/GraphQlCycleDetectorUnitTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ void shouldDetectCycles() {
7777
String sdl = getHtml("circularRelationship.graphql");
7878
GraphQLSchema schema =
7979
UnExecutableSchemaGenerator.makeUnExecutableSchema(new SchemaParser().parse(sdl));
80-
var generator = new GraphQlGenerator(valueProvider, schema, null, param);
80+
var generator = new GraphQlGenerator(valueProvider, schema, null, param, 0);
8181
var cyclesDetector = new GraphQlCycleDetector(schema, generator, null, param);
8282
List<GraphQlCycleDetectionResult> results = new ArrayList<>();
8383
// When
@@ -104,7 +104,7 @@ void shouldRaiseAlertsForDetectedCycles() throws Exception {
104104
String sdl = getHtml("circularRelationship.graphql");
105105
GraphQLSchema schema =
106106
UnExecutableSchemaGenerator.makeUnExecutableSchema(new SchemaParser().parse(sdl));
107-
var generator = new GraphQlGenerator(valueProvider, schema, null, param);
107+
var generator = new GraphQlGenerator(valueProvider, schema, null, param, 0);
108108
var queryMsgBuilder =
109109
new GraphQlQueryMessageBuilder(UrlBuilder.build("https://example.com/graphql"));
110110
var cyclesDetector = new GraphQlCycleDetector(schema, generator, queryMsgBuilder, param);

addOns/graphql/src/test/java/org/zaproxy/addon/graphql/GraphQlGeneratorUnitTest.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,20 @@
2020
package org.zaproxy.addon.graphql;
2121

2222
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
import static org.mockito.ArgumentMatchers.any;
24+
import static org.mockito.ArgumentMatchers.anyString;
2325
import static org.mockito.Mockito.mock;
26+
import static org.mockito.Mockito.times;
27+
import static org.mockito.Mockito.verify;
2428

2529
import graphql.schema.GraphQLSchema;
2630
import graphql.schema.idl.SchemaParser;
2731
import graphql.schema.idl.UnExecutableSchemaGenerator;
2832
import org.junit.jupiter.api.BeforeEach;
2933
import org.junit.jupiter.api.Test;
3034
import org.zaproxy.addon.commonlib.ValueProvider;
35+
import org.zaproxy.addon.graphql.GraphQlParam.ArgsTypeOption;
36+
import org.zaproxy.addon.graphql.GraphQlParam.QuerySplitOption;
3137
import org.zaproxy.zap.testutils.TestUtils;
3238

3339
class GraphQlGeneratorUnitTest extends TestUtils {
@@ -48,7 +54,39 @@ protected void setUpMessages() {
4854
}
4955

5056
private GraphQlGenerator createGraphQlGenerator(String sdl) {
51-
return new GraphQlGenerator(valueProvider, sdl, null, param);
57+
return new GraphQlGenerator(valueProvider, sdl, null, param, 0);
58+
}
59+
60+
@Test
61+
void shouldLimitMessagesWhenMaxMessagesSet() {
62+
// Given
63+
Requestor requestor = mock(Requestor.class);
64+
GraphQlParam limitedParam =
65+
new GraphQlParam(
66+
true,
67+
5,
68+
true,
69+
5,
70+
5,
71+
true,
72+
ArgsTypeOption.INLINE,
73+
QuerySplitOption.LEAF,
74+
GraphQlParam.RequestMethodOption.POST_JSON,
75+
GraphQlParam.CycleDetectionModeOption.DISABLED,
76+
0);
77+
GraphQlGenerator limitedGenerator =
78+
new GraphQlGenerator(
79+
valueProvider,
80+
getHtml("scalarFieldsOnly.graphql"),
81+
requestor,
82+
limitedParam,
83+
2);
84+
85+
// When
86+
limitedGenerator.generateAndSend();
87+
88+
// Then
89+
verify(requestor, times(2)).sendQuery(anyString(), anyString(), any());
5290
}
5391

5492
@Test
@@ -124,7 +162,7 @@ void nonNullableScalarArgumentsWithValueProvider() {
124162
};
125163
generator =
126164
new GraphQlGenerator(
127-
vg, getHtml("nonNullableScalarArguments.graphql"), null, param);
165+
vg, getHtml("nonNullableScalarArguments.graphql"), null, param, 0);
128166
// When
129167
String query = generator.generate(GraphQlGenerator.RequestType.QUERY);
130168
// Then

0 commit comments

Comments
 (0)