diff --git a/README.md b/README.md index 03d2bab..95c9c3b 100644 --- a/README.md +++ b/README.md @@ -116,3 +116,5 @@ There are also varaibles that can be modified with the `--config` flag as a TOML | MAX_FUZZING_ITERATIONS | Maximum number of fuzzing payloads to run on a node | Integer | 5 | | MAX_TIME | The maximum time to run in seconds | Integer | 3600 | | TIME_BETWEEN_REQUESTS | Max time to wait between requests in seconds | Integer | 0.001 | +| DEBUG | Debug mode | Boolean | False | +| Custom Headers| Custom headers to be sent along with each request | Object | `Accept = "application/json"` | diff --git a/graphqler/compiler/parsers/union_list_parser.py b/graphqler/compiler/parsers/union_list_parser.py index 4350b89..abf256a 100644 --- a/graphqler/compiler/parsers/union_list_parser.py +++ b/graphqler/compiler/parsers/union_list_parser.py @@ -27,11 +27,17 @@ def extract_union_values(self, union_values: List[dict]) -> List[dict]: union_valies (List[dict]): List of possible values of the UNION object Returns: - List[dict]: List of possible balues of the UNION object but filtered for only relavent fields + List[dict]: List of possible balues of the UNION object but filtered for only relavent fields. + We also add the type field to the object if the kind if an object """ list_of_union_values = [] for union_value in union_values: - filtered_union_value = {"kind": union_value["kind"], "name": union_value["name"], "ofType": union_value["ofType"]} + filtered_union_value = { + "kind": union_value["kind"], + "name": union_value["name"], + "ofType": union_value["ofType"], + "type": union_value["name"] if union_value["kind"] == "OBJECT" else None, + } list_of_union_values.append(filtered_union_value) return list_of_union_values diff --git a/graphqler/constants.py b/graphqler/constants.py index 5b3b691..51d4ed3 100644 --- a/graphqler/constants.py +++ b/graphqler/constants.py @@ -40,8 +40,8 @@ BUILT_IN_TYPE_KINDS = ["SCALAR", "OBJECT", "INTERFACE", "UNION", "ENUM", "INPUT_OBJECT", "LIST", "NON_NULL"] """For materializers""" -MAX_OBJECT_CYCLES = 3 -MAX_OUTPUT_SELECTOR_DEPTH = 3 +MAX_OBJECT_CYCLES = 5 +MAX_OUTPUT_SELECTOR_DEPTH = 5 HARD_CUTOFF_DEPTH = 20 MAX_INPUT_DEPTH = 20 diff --git a/graphqler/fuzzer/fengine/fengine.py b/graphqler/fuzzer/fengine/fengine.py index 46b8c88..9ab516b 100644 --- a/graphqler/fuzzer/fengine/fengine.py +++ b/graphqler/fuzzer/fengine/fengine.py @@ -27,7 +27,15 @@ @singleton class FEngine(object): - def __init__(self, queries: dict, objects: dict, mutations: dict, input_objects: dict, enums: dict, url: str, save_path: str): + def __init__(self, + queries: dict, + objects: dict, + mutations: dict, + input_objects: dict, + enums: dict, + unions: dict, + url: str, + save_path: str): """The intiialization of the FEnginer Args: @@ -36,6 +44,7 @@ def __init__(self, queries: dict, objects: dict, mutations: dict, input_objects: mutations (dict): The possible mutations input_objects (dict): The possible input_objects enums (dict): The possible enums + unions (dict): The possible union types url (str): The string of the URL save_path (str): The path the user is currently working with """ @@ -44,6 +53,7 @@ def __init__(self, queries: dict, objects: dict, mutations: dict, input_objects: self.mutations = mutations self.input_objects = input_objects self.enums = enums + self.unions = unions self.url = url self.logger = Logger().get_fuzzer_logger() @@ -65,6 +75,7 @@ def run_regular_payload(self, name: str, objects_bucket: dict, graphql_type: str self.mutations, self.input_objects, self.enums, + self.unions, fail_on_hard_dependency_not_met=check_hard_depends_on ) return self.__run_payload(name, objects_bucket, materializer, graphql_type) @@ -87,6 +98,7 @@ def run_dos_payload(self, name: str, objects_bucket: dict, graphql_type: str, ma self.mutations, self.input_objects, self.enums, + self.unions, fail_on_hard_dependency_not_met=False, max_depth=max_depth ) diff --git a/graphqler/fuzzer/fengine/materializers/dos_payload_materializer.py b/graphqler/fuzzer/fengine/materializers/dos_payload_materializer.py index c52c5a1..b483f6e 100644 --- a/graphqler/fuzzer/fengine/materializers/dos_payload_materializer.py +++ b/graphqler/fuzzer/fengine/materializers/dos_payload_materializer.py @@ -7,14 +7,22 @@ class DOSPayloadMaterializer(Materializer): - def __init__(self, objects: dict, queries: dict, mutations: dict, input_objects: dict, enums: dict, fail_on_hard_dependency_not_met: bool = False, max_depth: int = 20): - super().__init__(objects, mutations, input_objects, enums) + def __init__(self, + objects: dict, + queries: dict, + mutations: dict, + input_objects: dict, + enums: dict, + unions: dict, + fail_on_hard_dependency_not_met: bool = False, + max_depth: int = 20): + super().__init__(objects, mutations, input_objects, enums, unions) self.objects = objects self.queries = queries self.mutations = mutations self.input_objects = input_objects self.enums = enums - + self.unions = unions self.fail_on_hard_dependency_not_met = fail_on_hard_dependency_not_met self.max_depth = max_depth diff --git a/graphqler/fuzzer/fengine/materializers/materializer.py b/graphqler/fuzzer/fengine/materializers/materializer.py index 571d403..f12e1b0 100644 --- a/graphqler/fuzzer/fengine/materializers/materializer.py +++ b/graphqler/fuzzer/fengine/materializers/materializer.py @@ -2,16 +2,23 @@ Base class for a regular materializer """ -from .utils import get_random_scalar, get_random_enum_value +from graphqler import constants +from .utils import get_random_scalar, get_random_enum_value, clean_output_selectors from graphqler.utils.logging_utils import Logger from ..exceptions.hard_dependency_not_met_exception import HardDependencyNotMetException + import random import logging -from graphqler import constants class Materializer: - def __init__(self, objects: dict, operator_info: dict, input_objects: dict, enums: dict, fail_on_hard_dependency_not_met: bool = True): + def __init__(self, + objects: dict, + operator_info: dict, + input_objects: dict, + enums: dict, + unions: dict, + fail_on_hard_dependency_not_met: bool = True): """Default constructor for a regular materializer Args: @@ -19,6 +26,7 @@ def __init__(self, objects: dict, operator_info: dict, input_objects: dict, enum operator_info (dict): All information about the operator (either all QUERYs or all MUTATIONs) that we want to materialize input_objects (dict): The input objects that exist in the Graphql schema enums (dict): The enums that exist in the Graphql schema + unions (dict): The unions that exist in the Graphql schema logger (logging.Logger): The logger """ self.objects = objects @@ -42,7 +50,7 @@ def get_payload(self, name: str, objects_bucket: dict, graphql_type: str = '') - """ pass - def materialize_output(self, output_info: dict, used_objects: list[str], include_name: bool, max_depth: int = 3) -> str: + def materialize_output(self, output_info: dict, used_objects: list[str], include_name: bool, max_depth: int = 5) -> str: """Materializes the output. If returns empty string, then tries to get at least something, bypassing the max depth until the hard cutoff. @@ -64,7 +72,8 @@ def materialize_output(self, output_info: dict, used_objects: list[str], include if max_depth > constants.HARD_CUTOFF_DEPTH: break max_depth += 1 - return output_selectors + cleaned_output_selectors = clean_output_selectors(output_selectors) + return cleaned_output_selectors def materialize_output_recursive(self, output: dict, used_objects: list[str], include_name: bool, max_depth: int, current_depth: int = 0) -> str: """Materializes the output recursively. Some interesting cases: @@ -82,34 +91,42 @@ def materialize_output_recursive(self, output: dict, used_objects: list[str], in Returns: str: The built output payload """ + built_str = "" # Case: if we are already at max depth, just return none if current_depth >= max_depth: - return "" + return built_str + + # When we are including names (IE. fields of an object), we need to include the name + if include_name: + built_str += output["name"] - built_str = "" if output["kind"] == "OBJECT": materialized_object_fields = self.materialize_output_object_fields(output["type"], used_objects, max_depth, current_depth) if materialized_object_fields != "": - if include_name: - built_str += output["name"] built_str += " {" built_str += materialized_object_fields built_str += "}," - elif output["kind"] == "NON_NULL" or output["kind"] == "LIST": + elif output["kind"] == "UNION": # For a UNION type, loop through all the UNION types and materialize them into fragments + union_types = self.unions[output["type"]]["possibleTypes"] + built_str += " {" + for union_type in union_types: + materialized_fragment = self.materialize_output_recursive(union_type, used_objects, False, max_depth, current_depth) + if materialized_fragment != "": + built_str += f"... on {union_type['name']} " + materialized_fragment + built_str += "}," + elif output["kind"] == "NON_NULL" or output["kind"] == "LIST": # For a NON_NULL / LIST kind: Don't +1 here because it is an oftype (which doesn't add depth), or else we will double count oftype = output["ofType"] - if oftype["kind"] == "SCALAR": - built_str += f"{output['name']}, " - else: - # Don't +1 here because it is an oftype (which doesn't add depth), or else we will double count - materialized_output = self.materialize_output_recursive(oftype, used_objects, False, max_depth, current_depth) - if materialized_output != "": - if include_name: - built_str += f"{output['name']}" + materialized_output + ", " - else: - built_str += materialized_output + materialized_output = self.materialize_output_recursive(oftype, used_objects, False, max_depth, current_depth) + if materialized_output != "": + built_str += materialized_output + ", " else: - if include_name: - built_str += f"{output['name']}, " + built_str += "," + + # A bit of post processing on the built payload + if include_name and built_str[-1] != ",": + built_str += "," + elif not include_name and built_str.strip() == "{}": + built_str = "" return built_str def materialize_output_object_fields(self, object_name: str, used_objects: list[str], max_depth: int, current_depth: int) -> str: diff --git a/graphqler/fuzzer/fengine/materializers/regular_payload_materializer.py b/graphqler/fuzzer/fengine/materializers/regular_payload_materializer.py index d7abde4..79a5487 100644 --- a/graphqler/fuzzer/fengine/materializers/regular_payload_materializer.py +++ b/graphqler/fuzzer/fengine/materializers/regular_payload_materializer.py @@ -14,13 +14,15 @@ def __init__(self, mutations: dict, input_objects: dict, enums: dict, + unions: dict, fail_on_hard_dependency_not_met: bool = True): - super().__init__(objects, mutations, input_objects, enums) + super().__init__(objects, mutations, input_objects, enums, unions) self.objects = objects self.queries = queries self.mutations = mutations self.input_objects = input_objects self.enums = enums + self.unions = unions self.fail_on_hard_dependency_not_met = fail_on_hard_dependency_not_met def get_payload(self, name: str, objects_bucket: dict, graphql_type: str) -> tuple[str, dict]: diff --git a/graphqler/fuzzer/fengine/materializers/utils.py b/graphqler/fuzzer/fengine/materializers/utils.py index dd14b60..7f90b17 100644 --- a/graphqler/fuzzer/fengine/materializers/utils.py +++ b/graphqler/fuzzer/fengine/materializers/utils.py @@ -166,3 +166,26 @@ def prettify_graphql_payload(payload: str) -> str: parsed_query = parse(payload) formatted_query = print_ast(parsed_query).strip() return formatted_query + + +def clean_output_selectors(output_selectors: str) -> str: + """Cleans the output selectors by doing the following:L + - Removing any extra commas + - Removing Removing keys that don't have an object (ie. {stuff {}, otherstuff} -> {otherstuff}) + + Args: + output_selectors (str): _description_ + + Returns: + str: _description_ + """ + # Removing any extra commas + while ',,' in output_selectors: + output_selectors = output_selectors.replace(",,", ",") + + # Removing keys that don't have an object + while "{}" in output_selectors: + output_selectors = output_selectors.replace("{},", "") + output_selectors = output_selectors.replace(",{}", "") + + return output_selectors diff --git a/graphqler/fuzzer/fuzzer.py b/graphqler/fuzzer/fuzzer.py index 325b081..2538640 100644 --- a/graphqler/fuzzer/fuzzer.py +++ b/graphqler/fuzzer/fuzzer.py @@ -42,15 +42,26 @@ def __init__(self, save_path: str, url: str): self.compiled_mutations_save_path = Path(save_path) / constants.COMPILED_MUTATIONS_FILE_NAME self.extracted_enums_save_path = Path(save_path) / constants.ENUM_LIST_FILE_NAME self.extracted_input_objects_save_path = Path(save_path) / constants.INPUT_OBJECT_LIST_FILE_NAME + self.extracted_unions_save_path = Path(save_path) / constants.UNION_LIST_FILE_NAME self.queries = read_yaml_to_dict(self.compiled_queries_save_path) self.objects = read_yaml_to_dict(self.compiled_objects_save_path) self.mutations = read_yaml_to_dict(self.compiled_mutations_save_path) self.input_objects = read_yaml_to_dict(self.extracted_input_objects_save_path) self.enums = read_yaml_to_dict(self.extracted_enums_save_path) + self.unions = read_yaml_to_dict(self.extracted_unions_save_path) self.dependency_graph = GraphGenerator(save_path).get_dependency_graph() - self.fengine = FEngine(self.queries, self.objects, self.mutations, self.input_objects, self.enums, self.url, self.save_path) + self.fengine = FEngine( + self.queries, + self.objects, + self.mutations, + self.input_objects, + self.enums, + self.unions, + self.url, + self.save_path + ) self.objects_bucket = {} diff --git a/pyproject.toml b/pyproject.toml index 542ceb2..90d5859 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ exclude = ''' [tool.poetry] name = "GraphQLer" -version = "2.1.3" +version = "2.1.5" description = "A dependency-aware GraphQL API fuzzing tool" authors = ["Omar2535 "] license = "MIT"