Coverage for graphqler / fuzzer / engine / fengine.py: 66%
155 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-20 10:09 -0400
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-20 10:09 -0400
1"""FEngine: Responsible for getting the materialized query, running it against the API, and returning if it succeeds
2 and storing any new objects that were returned to the objects_bucket (if any were updated).
3Note: The run_regular_mutation and run_regular_query functions are very similar, but they are kept separate for clarity purposes
4"""
6import bdb
7import traceback
9from graphqler import config
10from graphqler.utils.api import API
11from graphqler.utils.logging_utils import Logger
12from graphqler.utils.objects_bucket import ObjectsBucket
13from graphqler.utils.parser_utils import get_output_type
14from graphqler.utils import plugins_handler
15from graphqler.utils.singleton import singleton
16from graphqler.utils.stats import Stats
18from .exceptions import HardDependencyNotMetException
19from .materializers import Materializer, RegularPayloadMaterializer, MaximalPayloadMaterializer, dos_materializers
20from .retrier import Retrier
21from .types import Result, ResultEnum
22from .utils import check_is_data_empty
25@singleton
26class FEngine(object):
27 def __init__(self, api: API):
28 """The intiialization of the FEnginer
30 Args:
31 api (API): The API object
32 """
33 self.api = api
34 self.logger = Logger().get_fuzzer_logger()
36 def run_minimal_payload(self, name: str, objects_bucket: ObjectsBucket, graphql_type: str, check_hard_depends_on: bool = True) -> tuple[dict, Result]:
37 """Runs the regular payload (either Query or Mutation), and returns a new objects bucket
39 Args:
40 name (str): The name of the query or mutation
41 objects_bucket (dict): The objects bucket
42 graphql_type (str): The GraphQL type (either query or mutation)
43 check_hard_depends_on (bool): Whether to check the hard depends on of the query's input - if it's not met, we fail. Defaults to True
45 Returns:
46 tuple[Response, Result]: The response dict, and the result of the query
47 """
48 self.logger.info(f"Running minimal payload: {name}")
49 materializer = RegularPayloadMaterializer(self.api, fail_on_hard_dependency_not_met=check_hard_depends_on)
50 return self.__run_payload(name, objects_bucket, materializer, graphql_type)
52 def run_maximal_payload(self, name: str, objects_bucket: ObjectsBucket, graphql_type: str, check_hard_depends_on: bool = True) -> tuple[dict, Result]:
53 """Runs the maximal payload (either Query or Mutation), and returns a new objects bucket
55 Args:
56 name (str): The name of the query or mutation
57 objects_bucket (dict): The objects bucket
58 graphql_type (str): The GraphQL type (either query or mutation)
59 check_hard_depends_on (bool): Whether to check the hard depends on of the query's input - if it's not met, we fail. Defaults to True
61 Returns:
62 tuple[Response, Result]: The response dict, and the result of the query
63 """
64 self.logger.info(f"Running maximal payload: {name}")
65 materializer = MaximalPayloadMaterializer(self.api, fail_on_hard_dependency_not_met=check_hard_depends_on)
66 return self.__run_payload(name, objects_bucket, materializer, graphql_type)
68 def run_dos_payloads(self, name: str, objects_bucket: ObjectsBucket, graphql_type: str, max_depth: int = 20) -> list[tuple[dict, Result]]:
69 """Runs all DOS payload (either Query or Mutation), and returns a new objects bucket
71 Args:
72 name (str): The name of the node
73 objects_bucket (dict): The objects bucket
74 graphql_type (str): The GraphQL type (either query or mutation)
75 max_depth (int, optional): The maximum recursion depth. Defaults to 20.
77 Returns:
78 list[tuple[Response, Result]]: A list of results of (The response dict, and the result of the query)
79 """
80 results = []
81 for dos_materializer in dos_materializers:
82 self.logger.info(f"Running DOS materializer: {dos_materializer.__name__} on {name}")
83 materializer = dos_materializer(self.api, fail_on_hard_dependency_not_met=False, max_depth=max_depth)
84 results += [self.__run_payload(name, objects_bucket, materializer, graphql_type)]
85 return results
87 def __run_payload(self, name: str, objects_bucket: ObjectsBucket, materializer: Materializer, graphql_type: str) -> tuple[dict, Result]:
88 """Runs the payload (either Query or Mutation), and returns a new objects bucket
90 Args:
91 name (str): The name of the query or mutation
92 objects_bucket (ObjectsBucket): The objects bucket
93 materializer (QueryMaterializer | MutationMaterializer): The materializer to use
94 graphql_type (str): The GraphQL type (either query or mutation)
96 Returns:
97 tuple[Response, Result]: The GraphQL response dict, and the result of the query
98 """
99 if graphql_type == "Query":
100 return self.__run_query(name, objects_bucket, materializer)
101 elif graphql_type == "Mutation":
102 return self.__run_mutation(name, objects_bucket, materializer)
103 else:
104 self.logger.warning(f"Unknown GraphQL type: {graphql_type} for {name}")
105 return ({}, Result(ResultEnum.INTERNAL_FAILURE))
107 def __run_mutation(self, endpoint_name: str, objects_bucket: ObjectsBucket, materializer: Materializer) -> tuple[dict, Result]:
108 """Runs the mutation, and returns a new objects bucket. Performs a few things:
109 1. Materializes the mutation with its parameters (resolving any dependencies from the object_bucket)
110 2. Send the mutation against the server and gets the parses the object from the response
111 3. Process the result in the objects_bucket if it's an object with an ID
112 - if we have a delete operation, remove it from the bucket
113 - if we have a create operation, add it to the bucket
114 - if we have an update operation, update it in the bucket
115 - if we have an unknown, don't do anything
117 Args:
118 endpoint_name(str): Name of the mutation
119 objects_bucket (dict): The current objects bucket
121 Returns:
122 tuple[dict, Result]: The graphql response dict, and the result of the mutation,
123 """
124 result = Result()
125 try:
126 # Step 1
127 self.logger.info(f"[{endpoint_name}] Running mutation: {endpoint_name}")
128 self.logger.debug(f"[{endpoint_name}] Objects bucket: {objects_bucket}")
129 payload_string, used_objects = materializer.get_payload(endpoint_name, objects_bucket, "Mutation")
130 result.payload = payload_string
132 # Step 2: Send the request & handle response
133 self.logger.info(f"[{endpoint_name}] Sending mutation payload string:\n {payload_string}")
134 request_utils = plugins_handler.get_request_utils()
135 graphql_response, request_response = request_utils.send_graphql_request(self.api.url, payload_string)
136 status_code = request_response.status_code
138 # Stats tracking stuff, results
139 self.logger.info(f"Request Response code: {status_code}")
140 Stats().add_http_status_code(endpoint_name, status_code)
141 result.status_code = status_code
142 result.graphql_response = graphql_response
143 result.raw_response_text = request_response.text
145 # For the GraphQL reponse
146 if not graphql_response:
147 result.result_enum = ResultEnum.EXTERNAL_FAILURE
148 return (graphql_response, result)
149 if result.has_errors:
150 self.logger.info(f"[{endpoint_name}] Mutation failed: {graphql_response['errors'][0]}")
151 self.logger.info(f"[{endpoint_name}] Retrying ---")
152 graphql_response, retry_success = Retrier(self.logger).retry(self.api.url, payload_string, graphql_response, 0)
153 if not retry_success:
154 result.result_enum = ResultEnum.EXTERNAL_FAILURE
155 return (graphql_response, result)
156 if not result.has_data:
157 self.logger.error(f"[{endpoint_name}] No data in response: {graphql_response}")
158 result.result_enum = ResultEnum.EXTERNAL_FAILURE
159 return (graphql_response, result)
160 if result.data[endpoint_name] is None or check_is_data_empty(result.data):
161 # Special case, this could indicate a failure or could also not, based on how GraphQLer is configured
162 self.logger.info(f"[{endpoint_name}] Mutation returned no data: {graphql_response} -- returning early")
163 if config.NO_DATA_COUNT_AS_SUCCESS:
164 result.result_enum = ResultEnum.NO_DATA_SUCCESS
165 return (graphql_response, result)
166 else:
167 result.result_enum = ResultEnum.EXTERNAL_FAILURE
168 return (graphql_response, result)
170 # Step 3
171 self.logger.info(f"Response: {graphql_response}")
173 # Process the response into the objects bucket
174 if type(result.data[endpoint_name]) is dict:
175 mutation_output_type = get_output_type(endpoint_name, self.api.mutations)
176 mutation_type = self.api.mutations[endpoint_name]["mutationType"]
177 if mutation_type == "CREATE":
178 objects_bucket.put_in_bucket(result.data)
179 elif mutation_type == "UPDATE":
180 objects_bucket.update_object_in_bucket(result.data)
181 elif mutation_type == "DELETE" and config.ALLOW_DELETION_OF_OBJECTS:
182 if mutation_output_type in used_objects:
183 used_object_value = used_objects[mutation_output_type]
184 objects_bucket.delete_object_from_bucket(mutation_output_type, used_object_value)
185 else:
186 pass # UNKNOWN mutation type — nothing to do
187 else:
188 # For non-dict responses (scalars, lists), still capture any data into the bucket
189 objects_bucket.put_in_bucket(result.data)
191 result.result_enum = ResultEnum.GENERAL_SUCCESS
192 return (graphql_response, result)
193 except HardDependencyNotMetException as e:
194 self.logger.info(f"[{endpoint_name}] Hard dependency not met: {e}")
195 result.result_enum = ResultEnum.INTERNAL_FAILURE
196 return ({}, result)
197 except bdb.BdbQuit as exc:
198 raise exc
199 except Exception as e:
200 # print(f"Exception when running: {mutation_name}: {e}, {traceback.print_exc()}")
201 self.logger.info(f"[{endpoint_name}] Exception when running: {endpoint_name}")
202 self.logger.info(f"[{endpoint_name}] {e}")
203 self.logger.debug(f"[{endpoint_name}] {traceback.format_exc()}")
204 result.result_enum = ResultEnum.INTERNAL_FAILURE
205 return ({}, result)
207 def __run_query(self, endpoint_name: str, objects_bucket: ObjectsBucket, materializer: Materializer) -> tuple[dict, Result]:
208 """Runs the query, and returns a new objects bucket
210 Args:
211 endpoint_name (str): The name of the query
212 objects_bucket (ObjectsBucket): The objects bucket
213 materializer (QueryMaterializer): The materializer to use
215 Returns:
216 tuple[dict, Result]: The graphql response as a dict, and the result of the query
217 """
218 result = Result()
219 try:
220 # Step 1
221 self.logger.info(f"[{endpoint_name}] Running query: {endpoint_name}")
222 self.logger.debug(f"[{endpoint_name}] Objects bucket: {objects_bucket}")
223 payload_string, used_objects = materializer.get_payload(endpoint_name, objects_bucket, "Query")
224 result.payload = payload_string
226 # Step 2
227 self.logger.info(f"[{endpoint_name}] Sending query payload string:\n {payload_string}")
228 request_utils = plugins_handler.get_request_utils()
229 graphql_response, request_response = request_utils.send_graphql_request(self.api.url, payload_string)
230 status_code = request_response.status_code
232 # Stats tracking stuff
233 self.logger.info(f"Request Response code: {status_code}")
234 Stats().add_http_status_code(endpoint_name, status_code)
235 result.status_code = status_code
236 result.graphql_response = graphql_response
237 result.raw_response_text = request_response.text
239 # For the GraphQL reponse
240 if not graphql_response:
241 result.result_enum = ResultEnum.EXTERNAL_FAILURE
242 return (graphql_response, result)
243 if result.has_errors:
244 self.logger.info(f"[{endpoint_name}] Query failed: {graphql_response['errors'][0]}")
245 self.logger.info(f"[{endpoint_name}] Retrying ---")
246 graphql_response, retry_success = Retrier(self.logger).retry(self.api.url, payload_string, graphql_response, 0)
247 if not retry_success:
248 result.result_enum = ResultEnum.EXTERNAL_FAILURE
249 return (graphql_response, result)
250 if not result.has_data:
251 self.logger.error(f"[{endpoint_name}] No data in response: {graphql_response}")
252 result.result_enum = ResultEnum.EXTERNAL_FAILURE
253 return (graphql_response, result)
254 if endpoint_name not in result.data or result.data[endpoint_name] is None or check_is_data_empty(result.data):
255 # Special case, this could indicate a failure or could also not, based on how GraphQLer is configured
256 self.logger.info(f"[{endpoint_name}] Query returned no data: {graphql_response} -- returning early")
257 if config.NO_DATA_COUNT_AS_SUCCESS:
258 result.result_enum = ResultEnum.NO_DATA_SUCCESS
259 return (graphql_response, result)
260 else:
261 result.result_enum = ResultEnum.EXTERNAL_FAILURE
262 return (graphql_response, result)
264 # Step 3
265 self.logger.info(f"Response: {graphql_response}")
266 if type(graphql_response["data"][endpoint_name]) is dict:
267 objects_bucket.put_in_bucket(graphql_response["data"])
269 result.result_enum = ResultEnum.GENERAL_SUCCESS
270 return (graphql_response, result)
271 except bdb.BdbQuit as exc:
272 raise exc
273 except Exception as e:
274 self.logger.info(f"[{endpoint_name}]Exception when running: {endpoint_name}: {e}, {traceback.format_exc()}")
275 result.result_enum = ResultEnum.INTERNAL_FAILURE
276 return ({}, result)