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

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""" 

5 

6import bdb 

7import traceback 

8 

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 

17 

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 

23 

24 

25@singleton 

26class FEngine(object): 

27 def __init__(self, api: API): 

28 """The intiialization of the FEnginer 

29 

30 Args: 

31 api (API): The API object 

32 """ 

33 self.api = api 

34 self.logger = Logger().get_fuzzer_logger() 

35 

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 

38 

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 

44 

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) 

51 

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 

54 

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 

60 

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) 

67 

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 

70 

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. 

76 

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 

86 

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 

89 

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) 

95 

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)) 

106 

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 

116 

117 Args: 

118 endpoint_name(str): Name of the mutation 

119 objects_bucket (dict): The current objects bucket 

120 

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 

131 

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 

137 

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 

144 

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) 

169 

170 # Step 3 

171 self.logger.info(f"Response: {graphql_response}") 

172 

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) 

190 

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) 

206 

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 

209 

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 

214 

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 

225 

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 

231 

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 

238 

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) 

263 

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"]) 

268 

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)