Coverage for graphqler / __main__.py: 36%
157 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"""
2Graphler - main start
3"""
5import sys
6import argparse
7import pprint
8import importlib.metadata
9import cloudpickle as pickle
11from graphqler.compiler.compiler import Compiler
12from graphqler.fuzzer import Fuzzer, IDORFuzzer
13from graphqler.graph import GraphGenerator
14from graphqler.utils.stats import Stats
15from graphqler.utils.cli_utils import set_auth_token_constant, is_compiled
16from graphqler.utils.config_handler import parse_config, set_config, generate_new_config, does_config_file_exist_in_path
17from graphqler.utils.file_utils import get_or_create_directory
18from graphqler import config
21def run_compile_mode(path: str, url: str):
22 """Runs the full compilation pipeline by delegating to compile-graph then compile-chains.
24 Args:
25 path (str): Directory for all compilation outputs to be saved to
26 url (str): URL of the target
27 """
28 print("(C) In compile mode!")
29 run_compile_graph_mode(path, url)
30 run_compile_chains_mode(path)
31 print("(C) Complete compilation phase")
34def run_compile_graph_mode(path: str, url: str):
35 """Runs only the introspection / parsing / resolving steps and generates the dependency graph.
37 Use this when you want to regenerate the dependency graph without re-running
38 chain generation, or when you plan to run ``compile-chains`` separately.
40 Args:
41 path (str): Directory for all compilation outputs to be saved to
42 url (str): URL of the target
43 """
44 print("(C) In compile-graph mode!")
45 Compiler(path, url).run()
47 print("(C) Finished compiling, starting graph generator")
48 graph_generator = GraphGenerator(path)
49 graph = graph_generator.get_dependency_graph()
50 graph_generator.draw_dependency_graph()
52 print("(C) Found", len(graph.nodes), "nodes and", len(graph.edges), "edges")
53 print("(C) Complete graph compilation phase (chains not generated)")
56def run_compile_chains_mode(path: str):
57 """Generates (or re-generates) fuzzing chains from an already-compiled graph.
59 Requires that ``compile`` or ``compile-graph`` has been run first so that the
60 compiled YAML files and dependency graph are present on disk.
62 Args:
63 path (str): Directory used during the original compilation.
64 """
65 from graphqler.chains import ChainGenerator
67 print("(C) In compile-chains mode!")
68 dependency_graph = GraphGenerator(path).get_dependency_graph()
69 in_degrees = dict(dependency_graph.in_degree())
70 if not in_degrees:
71 print("(C) Dependency graph is empty — no chains generated")
72 return
74 min_degree = min(in_degrees.values())
75 starter_nodes = [node for node, degree in in_degrees.items() if degree == min_degree]
77 chain_generator = ChainGenerator()
78 chain_generator.generate(dependency_graph, starter_nodes)
79 chain_generator.save_to_yaml(path)
80 print(f"(C) Generated {len(chain_generator.chains)} chains")
81 print("(C) Chain generation complete")
84def run_fuzz_mode(path: str, url: str):
85 """Runs the program in fuzz mode
87 Args:
88 path (str): Directory for all compilation outputs to be saved to
89 url (str): URL of the target
90 """
91 print("(F) Initializing stats file")
92 stats = Stats()
93 stats.set_file_paths(path)
95 print("(F) Starting fuzzer")
96 if not config.USE_OBJECTS_BUCKET:
97 print("(F) Not using Objects Bucket")
99 if config.USE_DEPENDENCY_GRAPH:
100 print("(F) Running in dependency graph mode")
101 Fuzzer(path, url).run()
102 else:
103 print("(F) Not using dependency graph")
104 Fuzzer(path, url).run_no_dfs()
106 print("(F) Complete fuzzing phase")
109def run_idor_mode(path: str, url: str):
110 print("(F) Running IDOR fuzzer")
111 try:
112 with open(f"{path}/{config.OBJECTS_BUCKET_PICKLE_FILE_NAME}", "rb") as f:
113 objects_bucket = pickle.load(f)
114 possible_idor_nodes = IDORFuzzer(path, url, objects_bucket).run()
115 print("Possible IDOR nodes:")
116 pprint.pprint(possible_idor_nodes)
117 except FileNotFoundError:
118 print("(F) Error: objects_bucket.pkl not found")
119 return
122def run_single_mode(path: str, url: str, name: str):
123 print("(F) Running single mode")
124 Fuzzer(path, url).run_single(name)
127def main(args: dict):
128 # Run either compilation or fuzzing mode
129 if 'mode' not in args or not args['mode']:
130 print("Please provide a mode to run the program in")
131 sys.exit(1)
133 # compile-chains works from disk — URL not needed; all other modes require it
134 if args['mode'] != "compile-chains" and not args.get('url'):
135 print(f"--url is required for mode '{args['mode']}'")
136 sys.exit(1)
138 # If not compile mode, check if compiled directory exists
139 if args['mode'] not in ["compile", "compile-graph", "compile-chains", "run", "single", "idor"] and (not is_compiled(args['path']) or not is_compiled(config.OUTPUT_DIRECTORY)):
140 print("(!) Compiled directory does not exist, please run in compile mode first")
141 sys.exit(1)
143 # Set the path if provided and create the directory if it doesn't exist
144 if 'path' in args and args['path']:
145 config.OUTPUT_DIRECTORY = args['path']
146 get_or_create_directory(config.OUTPUT_DIRECTORY)
148 # Set proxy if provided
149 if 'proxy' in args and args['proxy']:
150 config.PROXY = args['proxy']
152 # Set auth token if provided
153 if 'auth' in args and args['auth']:
154 set_auth_token_constant(args['auth'])
156 # Parse config if provided
157 if 'config' in args and args['config']:
158 print("(P) Using provided config file")
159 new_config = parse_config(args['config'])
160 set_config(new_config)
161 elif does_config_file_exist_in_path(args['path']):
162 print("(P) Using config file in path")
163 new_config = parse_config(f"{args['path']}/{config.CONFIG_FILE_NAME}")
164 set_config(new_config)
165 else:
166 print("(P) Generating new config")
167 generate_new_config(f"{args['path']}/{config.CONFIG_FILE_NAME}")
169 # Parse plugins if defined
170 if 'plugins_path' in args and args['plugins_path']:
171 config.PLUGINS_PATH = args['plugins_path']
172 print(f"(P) Using plugins from {config.PLUGINS_PATH}")
174 # Apply LLM CLI overrides — these take precedence over config file values
175 if args.get('use_llm'):
176 config.USE_LLM = True
177 print("(P) LLM mode enabled via CLI flag")
178 if args.get('llm_model'):
179 config.LLM_MODEL = args['llm_model']
180 if args.get('llm_api_key'):
181 config.LLM_API_KEY = args['llm_api_key']
182 if args.get('llm_base_url'):
183 config.LLM_BASE_URL = args['llm_base_url']
184 if args.get('llm_max_retries') is not None:
185 config.LLM_MAX_RETRIES = args['llm_max_retries']
187 # Apply mutation CLI override
188 if args.get('disable_mutations'):
189 config.DISABLE_MUTATIONS = True
190 print("(P) Mutation fuzzing disabled — only Query chains will be generated")
192 # Start the program
193 if args['mode'] == "compile":
194 run_compile_mode(config.OUTPUT_DIRECTORY, args['url'])
195 elif args['mode'] == "compile-graph":
196 run_compile_graph_mode(config.OUTPUT_DIRECTORY, args['url'])
197 elif args['mode'] == "compile-chains":
198 run_compile_chains_mode(config.OUTPUT_DIRECTORY)
199 elif args['mode'] == "fuzz":
200 run_fuzz_mode(config.OUTPUT_DIRECTORY, args['url'])
201 elif args['mode'] == "run":
202 run_compile_mode(config.OUTPUT_DIRECTORY, args['url'])
203 run_fuzz_mode(config.OUTPUT_DIRECTORY, args['url'])
204 elif args['mode'] == "idor":
205 run_idor_mode(config.OUTPUT_DIRECTORY, args['url'])
206 elif args['mode'] == "single":
207 if 'node' not in args or not args['node']:
208 print("Please provide a node to run in single mode")
209 sys.exit(1)
210 run_single_mode(args['path'], args['url'], args['node'])
213# If running as a CLI
214if __name__ == "__main__":
215 # If version, display version and exit
216 if "--version" in sys.argv:
217 version = importlib.metadata.version("GraphQLer")
218 print(version)
219 sys.exit(0)
221 # Parse arguments
222 parser = argparse.ArgumentParser()
223 parser.add_argument("--url", help="remote host URL (required for all modes except compile-chains)", required=False)
224 parser.add_argument("--path", help=f"directory location for files to be saved-to/used-from. Defaults to {config.OUTPUT_DIRECTORY}", required=False)
225 parser.add_argument("--config", help="TOML configuration file for the program", required=False)
226 parser.add_argument("--mode", help="mode to run the program in", choices=["compile", "compile-graph", "compile-chains", "fuzz", "idor", "run", "single"], required=True)
227 parser.add_argument("--auth", help="authentication token Example: 'Bearer arandompat-abcdefgh'", required=False)
228 parser.add_argument("--proxy", help="proxy to use for requests (ie. http://127.0.0.1:8080)", required=False)
229 parser.add_argument("--node", help="node to run (only used in single mode)", required=False)
230 parser.add_argument("--plugins-path", help="path to plugins directory", required=False)
231 parser.add_argument("--use-llm", help="enable LLM-based dependency graph inference (requires LLM_MODEL and credentials)", action="store_true", default=False)
232 parser.add_argument("--llm-model", help="litellm model string, e.g. 'gpt-4o-mini', 'ollama/llama3', 'anthropic/claude-3-5-haiku-20241022'", required=False)
233 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)
234 parser.add_argument("--llm-base-url", help="custom base URL for LLM endpoint (required for Ollama and LiteLLM proxies)", required=False)
235 parser.add_argument("--llm-max-retries", help="number of retries when LLM returns non-JSON (default: 2)", type=int, required=False)
236 parser.add_argument("--disable-mutations", help="only generate and run Query chains — all Mutation nodes are excluded from fuzzing", action="store_true", default=False)
237 parser.add_argument("--version", help="display version", action="store_true")
239 args = parser.parse_args()
240 args_as_dict = vars(args)
242 # Some massaging
243 if args_as_dict['path'] is None:
244 args_as_dict['path'] = config.OUTPUT_DIRECTORY
245 main(args_as_dict)