Skip to content

Commit 555f27f

Browse files
authored
Merge branch 'main' into dependabot/uv/authlib-1.6.11
2 parents e96787c + 3b672c0 commit 555f27f

53 files changed

Lines changed: 3486 additions & 211 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dist/
2828
*-test/
2929
*-tests/
3030
graphqler-output/
31+
graphqler-output-no-llm/
3132

3233
# Codeql
3334
codeql/

graphqler/__main__.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from graphqler.graph import GraphGenerator
1212
from graphqler.utils.stats import Stats
1313
from graphqler.utils.cli_utils import set_auth_token_constant, set_idor_auth_token_constant, is_compiled
14-
from graphqler.utils.config_handler import parse_config, set_config, generate_new_config, does_config_file_exist_in_path
14+
from graphqler.utils.config_handler import parse_config, set_config, generate_new_config, does_config_file_exist_in_path, write_config_to_toml
1515
from graphqler.utils.file_utils import get_or_create_directory
1616
from graphqler import config
1717

@@ -187,6 +187,12 @@ def main(args: dict):
187187
if args.get('use_llm'):
188188
config.USE_LLM = True
189189
print("(P) LLM mode enabled via CLI flag")
190+
if args.get('no_llm_compilation'):
191+
config.LLM_USE_FOR_COMPILATION = False
192+
print("(P) LLM disabled for compilation phase")
193+
if args.get('no_llm_fuzzing'):
194+
config.LLM_USE_FOR_FUZZING = False
195+
print("(P) LLM disabled for fuzzing phase")
190196
if args.get('llm_report'):
191197
config.LLM_ENABLE_REPORTER = True
192198
if args.get('llm_model'):
@@ -203,6 +209,14 @@ def main(args: dict):
203209
config.DISABLE_MUTATIONS = True
204210
print("(P) Mutation fuzzing disabled — only Query chains will be generated")
205211

212+
# Apply detections CLI override
213+
if args.get('no_detections'):
214+
config.SKIP_INJECTION_ATTACKS = True
215+
config.SKIP_MISC_ATTACKS = True
216+
config.SKIP_DOS_ATTACKS = True
217+
config.SKIP_ENUMERATION_ATTACKS = True
218+
print("(P) All detections disabled")
219+
206220
# Apply ablation CLI overrides
207221
if args.get('no_objects_bucket'):
208222
config.USE_OBJECTS_BUCKET = False
@@ -220,6 +234,19 @@ def main(args: dict):
220234
config.SKIP_SUBSCRIPTIONS = False
221235
print("(P) Subscription fuzzing enabled")
222236

237+
if args.get('no_endpoint_results'):
238+
config.SAVE_ENDPOINT_RESULTS = False
239+
print("(P) Endpoint results writing disabled")
240+
241+
if args.get('classic_coverage'):
242+
config.NO_DATA_COUNT_AS_SUCCESS = True
243+
if args.get('debug'):
244+
config.DEBUG = True
245+
print("(P) Classic coverage mode enabled — all non-error responses count as successes")
246+
247+
# Persist the final resolved config (file defaults + CLI overrides) back to disk
248+
write_config_to_toml(f"{args['path']}/{config.CONFIG_FILE_NAME}")
249+
223250
# Initialize the compiler and fuzzer
224251
compiler = Compiler(args['path'], args['url'])
225252
# Start the program
@@ -298,12 +325,15 @@ def main(args: dict):
298325
parser.add_argument("--node", help="node to run (only used in single mode)", required=False)
299326
parser.add_argument("--plugins-path", help="path to plugins directory", required=False)
300327
parser.add_argument("--use-llm", help="enable LLM-powered features: dependency graph inference, endpoint classification, IDOR chain classification, and UAF chain classification (requires LLM_MODEL and credentials)", action="store_true", default=False)
328+
parser.add_argument("--no-llm-compilation", help="disable LLM during the compilation phase (dependency resolver, IDOR/UAF chain classifiers) — overrides --use-llm for that phase", action="store_true", default=False)
329+
parser.add_argument("--no-llm-fuzzing", help="disable LLM during the fuzzing phase (payload generation, error retry, endpoint classification, report) — overrides --use-llm for that phase", action="store_true", default=False)
301330
parser.add_argument("--llm-report", help="generate an LLM vulnerability report (report.md) after fuzzing completes — requires --use-llm", action="store_true", default=False)
302331
parser.add_argument("--llm-model", help="litellm model string, e.g. 'gpt-4o-mini', 'ollama/llama3', 'anthropic/claude-3-5-haiku-20241022'", required=False)
303332
parser.add_argument("--llm-api-key", help="API key for the LLM provider (or set OPENAI_API_KEY / ANTHROPIC_API_KEY env var)", required=False)
304333
parser.add_argument("--llm-base-url", help="custom base URL for LLM endpoint (required for Ollama and LiteLLM proxies)", required=False)
305334
parser.add_argument("--llm-max-retries", help="number of retries when LLM returns non-JSON (default: 2)", type=int, required=False)
306335
parser.add_argument("--disable-mutations", help="only generate and run Query chains — all Mutation nodes are excluded from fuzzing", action="store_true", default=False)
336+
parser.add_argument("--no-detections", help="disable all vulnerability detections (injection, misc, DoS, enumeration, API-level checks)", action="store_true", default=False)
307337

308338
# Ablation / research flags
309339
parser.add_argument("--no-objects-bucket", help="ablation: disable the objects bucket — requests carry no state from prior responses", action="store_true", default=False)
@@ -312,7 +342,9 @@ def main(args: dict):
312342
parser.add_argument("--allow-deletion", help="remove objects from the bucket when a DELETE mutation succeeds (default: off)", action="store_true", default=False)
313343
parser.add_argument("--subscriptions", help="enable fuzzing of GraphQL subscriptions via WebSocket (disabled by default — requires WebSocket support on the target)", action="store_true", default=False)
314344

315-
parser.add_argument("--version", help="display version", action="store_true")
345+
parser.add_argument("--no-endpoint-results", help="skip writing per-endpoint result files to disk (useful when results are very large)", action="store_true", default=False)
346+
parser.add_argument("--classic-coverage", help="count responses with no data as successes (sets NO_DATA_COUNT_AS_SUCCESS=true)", action="store_true", default=False)
347+
parser.add_argument("--debug", help="enable debug mode: runs the fuzzer in a thread instead of a subprocess so pdb/breakpoint() work", action="store_true", default=False)
316348

317349
# MCP server flags (handled before argument parsing; registered here for --help visibility)
318350
parser.add_argument("--mcp", help="launch the GraphQLer MCP server (requires pip install GraphQLer[mcp])", action="store_true", default=False)

graphqler/chains/chain.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Chain dataclass representing an ordered sequence of nodes to execute."""
22

3+
import uuid
34
from dataclasses import dataclass, field
45
from graphqler.graph.node import Node
56

@@ -32,6 +33,7 @@ class Chain:
3233

3334
steps: list[ChainStep] = field(default_factory=list)
3435
name: str = ""
36+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
3537
confidence: float = 1.0 # classifier score; 1.0 for statically-derived chains, heuristic score for IDOR candidates
3638
reason: str = "" # human-readable explanation (if applicable)
3739

graphqler/chains/chain_generator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""ChainGenerator: generates, saves, and loads dependency chains."""
22

3+
import uuid
34
from pathlib import Path
45

56
import networkx
@@ -70,6 +71,7 @@ def save_to_yaml(self, save_path: str) -> None:
7071
data = []
7172
for chain in chains:
7273
entry: dict = {
74+
"id": chain.id,
7375
"steps": [{"node": step.node.name, "profile": step.profile_name} for step in chain.steps],
7476
"confidence": round(chain.confidence, 4),
7577
"reason": chain.reason,
@@ -120,6 +122,7 @@ def load_from_yaml(self, save_path: str, graph: networkx.DiGraph) -> list[Chain]
120122

121123
if steps:
122124
chains.append(Chain(
125+
id=entry.get("id", str(uuid.uuid4())),
123126
steps=steps,
124127
confidence=entry.get("idor_confidence", entry.get("confidence", 1.0)),
125128
reason=entry.get("idor_reason", entry.get("reason", "")),

graphqler/chains/strategies/idor_strategy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def generate(self, graph: networkx.DiGraph | None, starter_nodes: list[Node],
6161
f"heuristic: {reason}"))
6262
continue
6363

64-
if config.IDOR_USE_LLM_FALLBACK and config.USE_LLM:
64+
if config.IDOR_USE_LLM_FALLBACK and config.USE_LLM and config.LLM_USE_FOR_COMPILATION:
6565
is_candidate, llm_reason = llm_idor_classifier.classify(chain, split_index)
6666
if is_candidate:
6767
effective_split = split_index if split_index > 0 else self._last_create_split(chain)

graphqler/chains/strategies/uaf_strategy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def generate(self, graph: networkx.DiGraph | None, starter_nodes: list[Node],
8787
f"heuristic: {reason}"))
8888
continue
8989

90-
if config.UAF_USE_LLM_FALLBACK and config.USE_LLM:
90+
if config.UAF_USE_LLM_FALLBACK and config.USE_LLM and config.LLM_USE_FOR_COMPILATION:
9191
is_candidate, llm_reason = llm_uaf_classifier.classify(chain, split_index)
9292
if is_candidate:
9393
effective_split = split_index if split_index > 0 else self._last_delete_split(chain)

graphqler/compiler/compiler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def run_resolvers_and_save(self, introspection_result: dict):
193193
objects = ObjectDependencyResolver().resolve(objects)
194194
objects = ObjectMethodResolver().resolve(objects, queries, mutations)
195195

196-
if config.USE_LLM:
196+
if config.USE_LLM and config.LLM_USE_FOR_COMPILATION:
197197
print(f"(C) Using LLM resolver ({config.LLM_MODEL}) for dependency graph inference …")
198198
mut_resolver = LLMMutationObjectResolver()
199199
qry_resolver = LLMQueryObjectResolver()

graphqler/compiler/resolvers/mutation_object_resolver.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
hardDependsOn: A dictionary of inputname-object name that is required
55
in the input (NON-NULL), depends on, ie: {'userId': 'User'}
66
softDependsOn: A dictionary of inputname-object name, depends on, ie: {'userId': 'User'}
7+
produces: A string containing the inner object type that a list/connection mutation produces
8+
(e.g. 'Country'). Populated when the output type is a connection/wrapper
9+
whose items/nodes/edges field holds OBJECT elements.
710
"""
811

912
import re
@@ -35,12 +38,12 @@ def resolve(
3538
for mutation_name, mutation in mutations.items():
3639
mutation_type = self.get_mutation_action(mutation_name, mutation["description"])
3740
inputs_related_to_ids = self.get_inputs_related_to_ids(mutation["inputs"], input_objects)
38-
resolved_objects_to_inputs = self.resolve_inputs_related_to_ids_to_objects(mutation_name, inputs_related_to_ids, objects)
41+
resolved_objects_to_inputs = self.resolve_inputs_related_to_ids_to_objects(mutation_name, inputs_related_to_ids, objects, operation=mutation)
3942

40-
# Assign the enrichments
4143
mutations[mutation_name]["hardDependsOn"] = resolved_objects_to_inputs["hardDependsOn"]
4244
mutations[mutation_name]["softDependsOn"] = resolved_objects_to_inputs["softDependsOn"]
4345
mutations[mutation_name]["mutationType"] = mutation_type
46+
mutations[mutation_name]["produces"] = self._resolve_produces(mutation, objects)
4447

4548
return mutations
4649

graphqler/compiler/resolvers/query_object_resolver.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
hardDependsOn: A dictionary of inputname-object name that is required
44
in the input (NON-NULL), depends on, ie: {'userId': 'User'}
55
softDependsOn: A dictionary of inputname-object name, depends on, ie: {'userId': 'User'}
6+
produces: A string containing the inner object type that a list/connection query produces
7+
(e.g. 'Country'). Populated when the output type is a connection/wrapper
8+
whose items/nodes/edges field holds OBJECT elements — used to drive scheduling
9+
in the dependency graph so list queries run before singular ID-argument queries.
610
"""
711

812
from .resolver import Resolver
@@ -18,22 +22,25 @@ def resolve(
1822
queries: dict,
1923
input_objects: dict,
2024
) -> dict:
21-
"""Resolve query inputs to queries based on semantical understanding of IDs
25+
"""Resolve query inputs to queries based on semantical understanding of IDs.
26+
Also annotates list/connection queries with a ``produces`` field containing
27+
the inner item type so the dependency graph can order list queries before
28+
singular ID-argument queries.
2229
2330
Args:
24-
objects (dict): Objects to link the mutations to
31+
objects (dict): Objects to link the queries to
2532
queries (dict): Queries to parse through
2633
input_objects (dict): Input objects to recursively search through different input object inputs
2734
2835
Returns:
29-
dict: The mutations enriched with aforementioned fields
36+
dict: The queries enriched with aforementioned fields
3037
"""
3138
for query_name, query in queries.items():
3239
inputs_related_to_ids = self.get_inputs_related_to_ids(query["inputs"], input_objects)
33-
resolved_objects_to_inputs = self.resolve_inputs_related_to_ids_to_objects(query_name, inputs_related_to_ids, objects)
40+
resolved_objects_to_inputs = self.resolve_inputs_related_to_ids_to_objects(query_name, inputs_related_to_ids, objects, operation=query)
3441

35-
# Assign the enrichments
3642
queries[query_name]["hardDependsOn"] = resolved_objects_to_inputs["hardDependsOn"]
3743
queries[query_name]["softDependsOn"] = resolved_objects_to_inputs["softDependsOn"]
44+
queries[query_name]["produces"] = self._resolve_produces(query, objects)
3845

3946
return queries

0 commit comments

Comments
 (0)