From 936cc42d12ec6fe14fa302c203122ff021d8ef08 Mon Sep 17 00:00:00 2001 From: Mukesh Sai Kumar Date: Tue, 1 Jul 2025 18:21:29 +0530 Subject: [PATCH 1/7] Add graphql introspection module --- bbot/modules/graphql_introspection.py | 145 ++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 bbot/modules/graphql_introspection.py diff --git a/bbot/modules/graphql_introspection.py b/bbot/modules/graphql_introspection.py new file mode 100644 index 0000000000..76170034f4 --- /dev/null +++ b/bbot/modules/graphql_introspection.py @@ -0,0 +1,145 @@ +import json +from pathlib import Path +from bbot.modules.base import BaseModule + + +class graphql_introspection(BaseModule): + watched_events = ["URL"] + produced_events = ["FINDING"] + flags = ["safe", "active"] + meta = { + "description": "Perform GraphQL introspection on a target", + "created_date": "2025-07-01", + "author": "@mukesh-dream11", + } + options = { + "graphql_endpoint_urls": ["", "graphql", "v1/graphql"], + "output_folder": "", + } + options_desc = { + "graphql_endpoint_urls": "List of GraphQL endpoint to suffix to the target URL", + "output_folder": "Folder to save the GraphQL schemas to", + } + per_domain_only = True + + async def setup(self): + output_folder = self.config.get("output_folder", "") + if output_folder: + self.output_dir = Path(output_folder) / "graphql-schemas" + else: + self.output_dir = self.scan.home / "graphql-schemas" + self.helpers.mkdir(self.output_dir) + return True + + async def handle_event(self, event): + if self.helpers.url_depth(event.data) > 1: + return + + for endpoint_url in self.config.get("graphql_endpoint_urls", []): + url = f"{event.data}{endpoint_url}" + request_args = { + "url": url, + "method": "POST", + "json": { + "query": """\ +query IntrospectionQuery { + __schema { + queryType { + name + } + mutationType { + name + } + types { + name + kind + description + fields(includeDeprecated: true) { + name + description + type { + ... TypeRef + } + isDeprecated + deprecationReason + } + interfaces { + ... TypeRef + } + possibleTypes { + ... TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + ofType { + ... TypeRef + } + } + } +} + +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } +}""" + }, + } + response = await self.helpers.request(**request_args) + if response.status_code != 200: + self.debug( + f"Failed to get GraphQL schema for {url} (status code {response.status_code})" + ) + continue + try: + response_json = response.json() + except json.JSONDecodeError: + self.debug(f"Failed to parse JSON for {url}") + continue + if response_json.get("data", {}).get("__schema", {}).get("types", []): + schema_output_dir = url.rstrip("/").replace(":", "-").replace("/", "-") + schema_output_dir = self.output_dir / schema_output_dir + self.helpers.mkdir(schema_output_dir) + + filename = "schema.json" + with open(schema_output_dir / filename, "w") as f: + json.dump(response_json, f) + await self.emit_event( + {"url": url, "description": "GraphQL schema"}, + "FINDING", + event, + context=f"{{module}} found GraphQL schema at {url}", + ) + # return, because we only want to find one schema per target + return From 242b046503cdecf74a3e244ad2894a2db694ba66 Mon Sep 17 00:00:00 2001 From: Mukesh Sai Kumar Date: Tue, 1 Jul 2025 18:26:50 +0530 Subject: [PATCH 2/7] Fix minor formatting error --- bbot/modules/graphql_introspection.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bbot/modules/graphql_introspection.py b/bbot/modules/graphql_introspection.py index 76170034f4..8b86c61d43 100644 --- a/bbot/modules/graphql_introspection.py +++ b/bbot/modules/graphql_introspection.py @@ -118,9 +118,7 @@ async def handle_event(self, event): } response = await self.helpers.request(**request_args) if response.status_code != 200: - self.debug( - f"Failed to get GraphQL schema for {url} (status code {response.status_code})" - ) + self.debug(f"Failed to get GraphQL schema for {url} (status code {response.status_code})") continue try: response_json = response.json() From 6704332d7e54c0fc32c2568e3b288ba4c936dcd3 Mon Sep 17 00:00:00 2001 From: Mukesh Sai Kumar Date: Thu, 3 Jul 2025 14:30:53 +0530 Subject: [PATCH 3/7] Add unit tests for graphql introspection module --- bbot/modules/graphql_introspection.py | 7 ++-- .../test_module_graphql_introspection.py | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 bbot/test/test_step_2/module_tests/test_module_graphql_introspection.py diff --git a/bbot/modules/graphql_introspection.py b/bbot/modules/graphql_introspection.py index 8b86c61d43..db176470f4 100644 --- a/bbot/modules/graphql_introspection.py +++ b/bbot/modules/graphql_introspection.py @@ -20,7 +20,6 @@ class graphql_introspection(BaseModule): "graphql_endpoint_urls": "List of GraphQL endpoint to suffix to the target URL", "output_folder": "Folder to save the GraphQL schemas to", } - per_domain_only = True async def setup(self): output_folder = self.config.get("output_folder", "") @@ -117,8 +116,10 @@ async def handle_event(self, event): }, } response = await self.helpers.request(**request_args) - if response.status_code != 200: - self.debug(f"Failed to get GraphQL schema for {url} (status code {response.status_code})") + if not response or response.status_code != 200: + self.debug( + f"Failed to get GraphQL schema for {url} (status code {response.status_code})" + ) continue try: response_json = response.json() diff --git a/bbot/test/test_step_2/module_tests/test_module_graphql_introspection.py b/bbot/test/test_step_2/module_tests/test_module_graphql_introspection.py new file mode 100644 index 0000000000..f6a47671c7 --- /dev/null +++ b/bbot/test/test_step_2/module_tests/test_module_graphql_introspection.py @@ -0,0 +1,34 @@ +from .base import ModuleTestBase + + +class TestGraphQLIntrospectionNon200(ModuleTestBase): + targets = ["http://127.0.0.1:8888"] + modules_overrides = ["graphql_introspection"] + + async def setup_after_prep(self, module_test): + module_test.set_expect_requests( + expect_args={"method": "POST", "uri": "/"}, + respond_args={"response_data": "ok"}, + ) + + def check(self, module_test, events): + assert all(e.type != "FINDING" for e in events), "should have raised 0 events" + + +class TestGraphQLIntrospection(ModuleTestBase): + targets = ["http://127.0.0.1:8888"] + modules_overrides = ["graphql_introspection"] + + async def setup_after_prep(self, module_test): + module_test.set_expect_requests( + expect_args={"method": "POST", "uri": "/"}, + respond_args={ + "response_data": """{"data": {"__schema": {"types": ["dummy"]}}}""", + }, + ) + + def check(self, module_test, events): + finding = [e for e in events if e.type == "FINDING"] + assert finding, "should have raised 1 FINDING event" + assert finding[0].data["url"] == "http://127.0.0.1:8888/" + assert finding[0].data["description"] == "GraphQL schema" From 3025ae690e6d8ed18c31de64885f2bfd44789b18 Mon Sep 17 00:00:00 2001 From: Mukesh Sai Kumar Date: Thu, 3 Jul 2025 14:32:13 +0530 Subject: [PATCH 4/7] Fix minor formatting error --- bbot/modules/graphql_introspection.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bbot/modules/graphql_introspection.py b/bbot/modules/graphql_introspection.py index db176470f4..9789dbb0e6 100644 --- a/bbot/modules/graphql_introspection.py +++ b/bbot/modules/graphql_introspection.py @@ -117,9 +117,7 @@ async def handle_event(self, event): } response = await self.helpers.request(**request_args) if not response or response.status_code != 200: - self.debug( - f"Failed to get GraphQL schema for {url} (status code {response.status_code})" - ) + self.debug(f"Failed to get GraphQL schema for {url} (status code {response.status_code})") continue try: response_json = response.json() From a9fa97d73656109bdedaaa8c6adfa37122f2bd49 Mon Sep 17 00:00:00 2001 From: Mukesh Sai Kumar Date: Thu, 3 Jul 2025 17:27:39 +0530 Subject: [PATCH 5/7] Add web-basic flag to graphql_introspection module --- bbot/modules/graphql_introspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbot/modules/graphql_introspection.py b/bbot/modules/graphql_introspection.py index 9789dbb0e6..11df43ac9f 100644 --- a/bbot/modules/graphql_introspection.py +++ b/bbot/modules/graphql_introspection.py @@ -6,7 +6,7 @@ class graphql_introspection(BaseModule): watched_events = ["URL"] produced_events = ["FINDING"] - flags = ["safe", "active"] + flags = ["safe", "active", "web-basic"] meta = { "description": "Perform GraphQL introspection on a target", "created_date": "2025-07-01", From 28e1b6eed6b965303d83fad3ae2c1ef5a00eba30 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 17 Jul 2025 11:23:27 -0400 Subject: [PATCH 6/7] improve dedup filtering --- bbot/modules/graphql_introspection.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/bbot/modules/graphql_introspection.py b/bbot/modules/graphql_introspection.py index 11df43ac9f..d21ff1173e 100644 --- a/bbot/modules/graphql_introspection.py +++ b/bbot/modules/graphql_introspection.py @@ -13,7 +13,7 @@ class graphql_introspection(BaseModule): "author": "@mukesh-dream11", } options = { - "graphql_endpoint_urls": ["", "graphql", "v1/graphql"], + "graphql_endpoint_urls": ["/", "/graphql", "/v1/graphql"], "output_folder": "", } options_desc = { @@ -30,12 +30,15 @@ async def setup(self): self.helpers.mkdir(self.output_dir) return True - async def handle_event(self, event): - if self.helpers.url_depth(event.data) > 1: - return + async def filter_event(self, event): + # Dedup by the base URL + base_url = event.parsed_url._replace(path="/", query="", fragment="").geturl() + return hash(base_url) + async def handle_event(self, event): + base_url = event.parsed_url._replace(path="/", query="", fragment="").geturl().rstrip("/") for endpoint_url in self.config.get("graphql_endpoint_urls", []): - url = f"{event.data}{endpoint_url}" + url = f"{base_url}{endpoint_url}" request_args = { "url": url, "method": "POST", @@ -125,15 +128,16 @@ async def handle_event(self, event): self.debug(f"Failed to parse JSON for {url}") continue if response_json.get("data", {}).get("__schema", {}).get("types", []): - schema_output_dir = url.rstrip("/").replace(":", "-").replace("/", "-") + schema_output_dir = self.helpers.tagify(url) schema_output_dir = self.output_dir / schema_output_dir self.helpers.mkdir(schema_output_dir) filename = "schema.json" - with open(schema_output_dir / filename, "w") as f: + filename = schema_output_dir / filename + with open(filename, "w") as f: json.dump(response_json, f) await self.emit_event( - {"url": url, "description": "GraphQL schema"}, + {"url": url, "description": "GraphQL schema", "path": str(filename.relative_to(self.scan.home))}, "FINDING", event, context=f"{{module}} found GraphQL schema at {url}", From e984b37f23e9c71e16ffb87f0304d2a205ee32d6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 17 Jul 2025 11:25:44 -0400 Subject: [PATCH 7/7] tagify filename --- bbot/modules/graphql_introspection.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/bbot/modules/graphql_introspection.py b/bbot/modules/graphql_introspection.py index d21ff1173e..43852f73d8 100644 --- a/bbot/modules/graphql_introspection.py +++ b/bbot/modules/graphql_introspection.py @@ -128,12 +128,8 @@ async def handle_event(self, event): self.debug(f"Failed to parse JSON for {url}") continue if response_json.get("data", {}).get("__schema", {}).get("types", []): - schema_output_dir = self.helpers.tagify(url) - schema_output_dir = self.output_dir / schema_output_dir - self.helpers.mkdir(schema_output_dir) - - filename = "schema.json" - filename = schema_output_dir / filename + filename = f"schema-{self.helpers.tagify(url)}.json" + filename = self.output_dir / filename with open(filename, "w") as f: json.dump(response_json, f) await self.emit_event(