diff --git a/README.md b/README.md
index 6a2f850..b976032 100644
--- a/README.md
+++ b/README.md
@@ -5,12 +5,11 @@
# `ffufai`

-
[](https://opensource.org/licenses/MIT)
-ffufai is an AI-powered wrapper for the popular web fuzzer ffuf. It automatically suggests file extensions for fuzzing based on the target URL and its headers, using either OpenAI's GPT or Anthropic's Claude AI models.
+ffufai is an AI-powered wrapper for the popular web fuzzer ffuf. It automatically suggests file extensions for fuzzing based on the target URL and its headers, using either OpenAI's GPT or Anthropic's Claude AI models and Free Hugging Face models on local.
@@ -21,60 +20,73 @@ ffufai is an AI-powered wrapper for the popular web fuzzer ffuf. It automaticall
- Seamlessly integrates with ffuf
- Automatically suggests relevant file extensions for fuzzing
-- Supports both OpenAI and Anthropic AI models
+- Supports both OpenAI, Anthropic AI models and Free Hugging Face AI models
- Passes through all ffuf parameters
## Prerequisites
- Python 3.6+
- ffuf (installed and accessible in your PATH)
-- An OpenAI API key or Anthropic API key
+- An OpenAI API key | Anthropic API key | Gemini API key | Free Hugging Face API key
## Installation
-1. Clone this repository:
+1. Clone this repository :
```
- git clone https://github.com/jthack/ffufai
+ git clone https://github.com/ikajakam/ffufai.git
cd ffufai
```
-2. Install the required Python packages:
+2. Install the required Python packages :
```
pip install requests openai anthropic
```
-3. Make the script executable:
- ```
- chmod +x ffufai.py
- ```
+3. To load and run `Qwen/Qwen2.5-1.5B-Instruct` model locally via Hugging Face :
+ ```
+ pip install transformers torch accelerate
+ ```
+ ```
+ poetry add accelerate
+ ```
+- Models are cached in `~/.cache/huggingface`
-4. (Optional) To use ffufai from anywhere, you can create a symbolic link in a directory that's in your PATH. For example:
+4. To use ffufai globally from terminal, create a symbolic link in a directory that's in your PATH.
```
sudo ln -s /full/path/to/ffufai.py /usr/local/bin/ffufai
```
Replace "/full/path/to/ffufai.py" with the actual full path to where you cloned the repository.
-5. Set up your API key as an environment variable:
- For OpenAI:
+6. Set up your API key as an environment variable :
+
+ OpenAI :
```
export OPENAI_API_KEY='your-api-key-here'
```
- Or for Anthropic:
+ Anthropic :
```
export ANTHROPIC_API_KEY='your-api-key-here'
```
-
- You can add these lines to your `~/.bashrc` or `~/.zshrc` file to make them permanent.
+ Gemini :
+ ```
+ export GEMINI_API_KEY="your-api-key-here"
+ ```
+ Hugging Face :
+ ```
+ export HUGGINGFACE_API_KEY=your-api-key-here ## no ''
+ ```
+
+ You can add these lines to your `~/.bashrc` or `~/.zshrc` file to make them permanent and reload your `~/.bashrc` or `~/.zshrc`
## Usage
-Use ffufai just like you would use ffuf, but replace `ffuf` with `python3 ffufai.py` (or just `ffufai` if you've created the symbolic link):
+Use ffufai just like you would use ffuf, but replace `ffuf` with `python3 ffufai.py` (or just `ffufai` if you've created the symbolic link) :
```
python3 ffufai.py -u https://example.com/FUZZ -w /path/to/wordlist.txt
```
-Or if you've created the symbolic link:
+Or if you've created the symbolic link :
```
ffufai -u https://example.com/FUZZ -w /path/to/wordlist.txt
@@ -82,6 +94,7 @@ ffufai -u https://example.com/FUZZ -w /path/to/wordlist.txt
ffufai will automatically suggest extensions based on the URL and add them to the ffuf command.
+
## Parameters
ffufai accepts all the parameters that ffuf does, plus a few additional ones:
@@ -104,16 +117,8 @@ All other ffuf parameters can be used as normal. For a full list of ffuf paramet
- ffufai requires the FUZZ keyword to be at the end of the URL path for accurate extension suggestion. It will warn you if this is not the case.
- All ffuf parameters are passed through to ffuf, so you can use any ffuf option with ffufai.
-- If both OpenAI and Anthropic API keys are set, ffufai will prefer the OpenAI key.
-HUGE Shoutout to zlz, aka Sam Curry, for the amazing idea to make this project. He suggested it and 2 hours later, here it is :)
-
-## Troubleshooting
-
-- If you encounter a "command not found" error, make sure you're using `python3 ffufai.py` or that you've correctly set up the symbolic link.
-- If you get an API key error, ensure you've correctly set up your OPENAI_API_KEY or ANTHROPIC_API_KEY environment variable.
-- If you see "import: command not found" errors, it means the script is being interpreted by the shell instead of Python. Make sure you're running it with `python3 ffufai.py` or that the shebang line at the top of the script is correct.
## Contributing
@@ -122,3 +127,4 @@ Contributions are welcome! Please feel free to submit a Pull Request.
## License
This project is licensed under the MIT License - see the LICENSE file for details.
+
diff --git a/ffufai.py b/ffufai.py
index 46d16c3..e123c3f 100755
--- a/ffufai.py
+++ b/ffufai.py
@@ -5,264 +5,94 @@
import subprocess
import requests
import json
-from openai import OpenAI
-import anthropic
+import shutil
+import re
from urllib.parse import urlparse
-import tempfile
-import os
-from bs4 import BeautifulSoup
-
-def get_api_key():
- openai_key = os.getenv('OPENAI_API_KEY')
- anthropic_key = os.getenv('ANTHROPIC_API_KEY')
- if anthropic_key:
- return ('anthropic', anthropic_key)
- elif openai_key:
- return ('openai', openai_key)
- else:
- raise ValueError("No API key found. Please set OPENAI_API_KEY or ANTHROPIC_API_KEY.")
-
-
-def get_response(url):
- try:
- response = requests.get(url, allow_redirects=True)
- soup = BeautifulSoup(response.content, 'html.parser')
+from providers.gemini import GeminiProvider
+from providers.openai import OpenAIProvider
+from providers.anthropic import AnthropicProvider
+from providers.huggingface_local import HuggingFaceLocalProvider
- for tag in soup.select('style, link[rel="stylesheet"]'):
- tag.decompose()
- for tag in soup.find_all(True):
- if hasattr(tag, 'attrs') and tag.attrs is not None:
- tag.attrs.pop('style', None)
+def get_provider():
+ if os.getenv("GEMINI_API_KEY"):
+ return GeminiProvider(os.getenv("GEMINI_API_KEY"))
+ if os.getenv("ANTHROPIC_API_KEY"):
+ return AnthropicProvider(os.getenv("ANTHROPIC_API_KEY"))
+ if os.getenv("OPENAI_API_KEY"):
+ return OpenAIProvider(os.getenv("OPENAI_API_KEY"))
+ if os.getenv("HUGGINGFACE_API_KEY"):
+ return HuggingFaceLocalProvider()
- if tag.name == 'svg':
- tag.decompose()
- if tag.name == 'img':
- tag.decompose()
+ raise RuntimeError("No AI provider API key found")
- content = soup.prettify()
-
- return {
- "url": response.url,
- "headers": dict(response.headers),
- "cookies": dict(response.cookies),
- "content": content[:2500]
- }
-
- except requests.RequestException as e:
- print(f"Error fetching content: {e}")
- return {"error": "Error fetching content."}
def get_headers(url):
try:
- response = requests.head(url, allow_redirects=True)
- return dict(response.headers)
- except requests.RequestException as e:
- print(f"Error fetching headers: {e}")
- return {"Header": "Error fetching headers."}
-
-def get_ai_extensions(url, headers, api_type, api_key, max_extensions):
- prompt = f"""
- Given the following URL and HTTP headers, suggest the most likely file extensions for fuzzing this endpoint.
- Respond with a JSON object containing a list of extensions. The response will be parsed with json.loads(),
- so it must be valid JSON. No preamble or yapping. Use the format: {{"extensions": [".ext1", ".ext2", ...]}}.
- Do not suggest more than {max_extensions}, but only suggest extensions that make sense. For example, if the path is
- /js/ then don't suggest .css as the extension. Also, if limited, prefer the extensions which are more interesting.
- The URL path is great to look at for ideas. For example, if it says presentations, then it's likely there
- are powerpoints or pdfs in there. If the path is /js/ then it's good to use js as an extension.
-
- Examples:
- 1. URL: https://example.com/presentations/FUZZ
- Headers: {{"Content-Type": "application/pdf", "Content-Length": "1234567"}}
- JSON Response: {{"extensions": [".pdf", ".ppt", ".pptx"]}}
-
- 2. URL: https://example.com/FUZZ
- Headers: {{"Server": "Microsoft-IIS/10.0", "X-Powered-By": "ASP.NET"}}
- JSON Response: {{"extensions": [".aspx", ".asp", ".exe", ".dll"]}}
-
- URL: {url}
- Headers: {headers}
-
- JSON Response:
- """
-
- if api_type == 'openai':
- client = OpenAI(api_key=api_key)
- response = client.chat.completions.create(
- model="gpt-4o",
- messages=[
- {"role": "system", "content": "You are a helpful assistant that suggests file extensions for fuzzing based on URL and headers."},
- {"role": "user", "content": prompt}
- ]
- )
- return json.loads(response.choices[0].message.content.strip())
- elif api_type == 'anthropic':
- client = anthropic.Anthropic(api_key=api_key)
- message = client.messages.create(
- model="claude-sonnet-4-20250514",
- max_tokens=1000,
- temperature=0,
- system="You are a helpful assistant that suggests file extensions for fuzzing based on URL and headers.",
- messages=[
- {"role": "user", "content": prompt}
- ]
- )
-
-
- return json.loads(message.content[0].text)
-
-def get_contextual_wordlist(url, headers, api_type, api_key, max_size, cookies=None, content=None):
- prompt = f"""
- Given the following URL and HTTP headers, suggest the most likely contextual wordlist for content discovery on this endpoint.
- Be as extensive as possible, provide the maximum number of directories and files that make sense for the endpoint.
- Try to create a list of size {max_size}.
- Respond with a JSON object containing a list of directories and files. The response will be parsed with json.loads(),
- so it must be valid JSON. No preamble or yapping. Use the format: { {"wordlist": ["dir1", "dir2", "file1", "file2"]} }.
- Only make suggestions that make sense. For example, if domain is for a book shop
- then don't suggest footbal as a directory. Also, if limited, prefer the files and directories which are more interesting.
- The URL path is great to look at for ideas, and so is the brand behind the URL.
- Focus on contents relevant to the identified industry and technology stack. Include technology-specific files.
- For example, if it says presentations, then it's likely there are powerpoints or pdfs in there. If the path is /js/ then it's good to fuzz for JS files.
-
- Example 1: WordPress Blog
- URL: https://blog.techstartup.io/wp-content/uploads/2024/FUZZ
- Headers: {{
- "Server": "nginx/1.22.1",
- "X-Powered-By": "PHP/8.1.2",
- "Link": "; rel=\"https://api.w.org/\"",
- "Content-Type": "image/jpeg"
- }}
-
- Response:
- {{
- "wordlist": ["wp-content", "wp-includes", "wp-admin", "uploads", "themes", "plugins", "2024", "2023", "backup", "cache", "wp-config.php", "xmlrpc.php", "wp-login.php", "readme.html", ".htaccess", "wp-config.php.bak", "debug.log"],
- }}
-
- Example 2: E-commerce Platform
- URL: https://shop.globalretail.com/checkout/payment/FUZZ
- Headers:
- {{
- "Server": "Microsoft-IIS/10.0",
- "X-Powered-By": "ASP.NET",
- "X-AspNet-Version": "4.0.30319",
- "X-Frame-Options": "SAMEORIGIN",
- "Strict-Transport-Security": "max-age=31536000"
- }}
-
- Response:
- {{
- "wordlist": ["checkout", "payment", "api", "admin", "account", "orders", "products", "cart", "invoice", "App_Data", "bin", "Content", "web.config", "Global.asax", "payment.aspx", "checkout.aspx", "web.config.bak", "App_Data.mdf", "connectionstrings.config"],
- }}
-
- URL: {url}
- Headers: {headers}
- Cookies: {cookies}
- Content: {content}
-
- JSON Response:
- """
-
- if api_type == 'openai':
- client = OpenAI(api_key=api_key)
- response = client.chat.completions.create(
- model="gpt-4o",
- messages=[
- {"role": "system", "content": "You are a helpful assistant that suggests wordlists for fuzzing based on URL and headers."},
- {"role": "user", "content": prompt}
- ]
- )
- return json.loads(response.choices[0].message.content.strip())
-
- elif api_type == 'anthropic':
- client = anthropic.Anthropic(api_key=api_key)
- message = client.messages.create(
- model="claude-sonnet-4-20250514",
- max_tokens=10000,
- temperature=0,
- system="You are a helpful assistant that suggests wordlists for fuzzing based on URL and headers.",
- messages=[
- {"role": "user", "content": prompt}
- ]
+ r = requests.head(url, allow_redirects=True, timeout=5)
+ return dict(r.headers)
+ except Exception:
+ return {}
+
+
+def colorize_ffuf_output(line):
+ line = line.decode(errors="ignore").strip()
+ m = re.search(r"\[Status: (\d{3})", line)
+ if m:
+ code = int(m.group(1))
+ color = (
+ "\033[92m" if code == 200 else
+ "\033[94m" if code in (301, 302) else
+ "\033[91m" if code == 403 else
+ "\033[0m"
)
+ print(f"{color}{line}\033[0m")
+ else:
+ print(line)
- return json.loads(message.content[0].text)
def main():
- parser = argparse.ArgumentParser(description='ffufai - AI-powered ffuf wrapper')
- parser.add_argument('--ffuf-path', default='ffuf', help='Path to ffuf executable')
- parser.add_argument('--max-extensions', type=int, default=4, help='Maximum number of extensions to suggest')
- parser.add_argument('--wordlists', action='store_true', help='Generate contextual wordlists')
- parser.add_argument('--max-wordlist-size', type=int, help="The maximum size of the generated wordlist")
- parser.add_argument('--include-response', action='store_true', help='Makes a GET request and uses the Response as context for better wordlist generation (Uses More tokens)')
+ parser = argparse.ArgumentParser(
+ description="ffufai ā AI-powered ffuf wrapper"
+ )
+ parser.add_argument("--ffuf-path", default="ffuf")
+ parser.add_argument("--max-extensions", type=int, default=5)
args, unknown = parser.parse_known_args()
- # Find the -u argument in the unknown args
try:
- url_index = unknown.index('-u') + 1
- url = unknown[url_index]
- except (ValueError, IndexError):
- print("Error: -u URL argument is required.")
+ url = unknown[unknown.index("-u") + 1]
+ except Exception:
+ print("ā You must provide -u URL")
return
- parsed_url = urlparse(url)
- path_parts = parsed_url.path.split('/')
- base_url = url.replace('FUZZ', '')
-
- if 'FUZZ' not in path_parts[-1]:
- print("Warning: FUZZ keyword is not at the end of the URL path. Extension fuzzing may not work as expected.")
-
+ base_url = url.replace("FUZZ", "")
headers = get_headers(base_url)
- api_type, api_key = get_api_key()
+ provider = get_provider()
+ print(f"š Using AI backend: {provider.name}")
+ try:
+ data = provider.get_extensions(url, headers, args.max_extensions)
+ exts = data.get("extensions", [])
+ if not exts:
+ raise ValueError
+ except Exception:
+ exts = [".php", ".html", ".json", ".bak"]
- if args.wordlists:
- try:
- if args.max_wordlist_size:
- size = args.max_wordlist_size
- else:
- size = 200
-
- if args.include_response:
- response = get_response(base_url)
- headers = response['headers']
- cookies = response['cookies']
- content = response['content']
- wordlists_data = get_contextual_wordlist(url, headers, api_type, api_key, size, cookies=cookies, content=content)
-
- else:
- wordlists_data = get_contextual_wordlist(url, headers, api_type, api_key, size)
-
- print(wordlists_data)
- wordlist = '\n'.join(wordlists_data['wordlist'])
-
- except (json.JSONDecodeError, KeyError) as e:
- print(f"Error parsing AI response. The Wordlist size may have been too big for your max_tokens. Try again. Error: {e}")
- return
-
- if wordlist:
- file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt')
- file.write(wordlist)
- file.close()
- ffuf_command = [args.ffuf_path] + unknown + ['-w', file.name]
- subprocess.run(ffuf_command)
-
+ exts = ",".join(e if e.startswith(".") else f".{e}" for e in exts[:args.max_extensions])
- else:
- try:
- extensions_data = get_ai_extensions(url, headers, api_type, api_key, args.max_extensions)
- print(extensions_data)
- extensions = ','.join(extensions_data['extensions'][:args.max_extensions])
-
- except (json.JSONDecodeError, KeyError) as e:
- print(f"Error parsing AI response. Try again. Error: {e}")
- return
+ if not shutil.which(args.ffuf_path):
+ print("ā ffuf binary not found")
+ return
- ffuf_command = [args.ffuf_path] + unknown + ['-e', extensions]
+ cmd = [args.ffuf_path] + unknown + ["-e", exts]
+ print("ā¶ļø Running:", " ".join(cmd))
- subprocess.run(ffuf_command)
+ with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as proc:
+ for line in proc.stdout:
+ colorize_ffuf_output(line)
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/providers/__init__.py b/providers/__init__.py
new file mode 100644
index 0000000..3177351
--- /dev/null
+++ b/providers/__init__.py
@@ -0,0 +1,4 @@
+from providers.gemini import GeminiProvider
+from providers.openai import OpenAIProvider
+from providers.anthropic import AnthropicProvider
+from providers.huggingface_local import HuggingFaceLocalProvider
diff --git a/providers/anthropic.py b/providers/anthropic.py
new file mode 100644
index 0000000..d01da37
--- /dev/null
+++ b/providers/anthropic.py
@@ -0,0 +1,41 @@
+# providers/anthropic.py
+
+import json
+import anthropic
+from .base import AIProvider
+
+SYSTEM_MSG = (
+ "You are a security assistant helping with web fuzzing. "
+ "Return only valid JSON. No prose."
+)
+
+class AnthropicProvider(AIProvider):
+ name = "anthropic"
+
+ def __init__(self, api_key):
+ self.client = anthropic.Anthropic(api_key=api_key)
+
+ def get_extensions(self, url, headers, max_extensions):
+ prompt = f"""
+Given the following URL and HTTP headers, suggest the most likely file extensions
+for fuzzing this endpoint.
+
+Respond with valid JSON only.
+Format:
+{{"extensions": [".php", ".json", ".bak"]}}
+
+Limit to at most {max_extensions} extensions.
+
+URL: {url}
+Headers: {headers}
+"""
+
+ msg = self.client.messages.create(
+ model="claude-3-5-sonnet-20240620",
+ max_tokens=500,
+ temperature=0,
+ system=SYSTEM_MSG,
+ messages=[{"role": "user", "content": prompt}],
+ )
+
+ return json.loads(msg.content[0].text.strip())
diff --git a/providers/base.py b/providers/base.py
new file mode 100644
index 0000000..802b5c7
--- /dev/null
+++ b/providers/base.py
@@ -0,0 +1,7 @@
+# providers/base.py
+
+class AIProvider:
+ name = "base"
+
+ def get_extensions(self, url, headers, max_extensions):
+ raise NotImplementedError
diff --git a/providers/gemini.py b/providers/gemini.py
new file mode 100644
index 0000000..68760e2
--- /dev/null
+++ b/providers/gemini.py
@@ -0,0 +1,75 @@
+import json
+from google import genai
+from google.genai import types
+from .base import AIProvider
+
+SYSTEM_MSG = (
+ "You are a security assistant helping with web fuzzing. "
+ "Return only valid JSON. No prose."
+)
+
+class GeminiProvider(AIProvider):
+ name = "gemini"
+
+ def __init__(self, api_key):
+ self.client = genai.Client(api_key=api_key)
+
+ def get_extensions(self, url, headers, max_extensions):
+ prompt = f"""
+Given the following URL and HTTP headers, suggest the most likely file extensions
+for fuzzing this endpoint.
+
+Respond with valid JSON only.
+Format:
+{{"extensions": [".php", ".json", ".bak"]}}
+
+Limit to at most {max_extensions} extensions.
+
+URL: {url}
+Headers: {headers}
+"""
+
+ config = types.GenerateContentConfig(
+ system_instruction=SYSTEM_MSG,
+ response_mime_type="application/json",
+ temperature=0.0
+ )
+
+ # Updated based on your diagnostic output
+ # Prioritizing 2.0 Flash as it is fast and available to you
+ model_candidates = [
+ "gemini-2.0-flash",
+ "gemini-2.5-flash",
+ "gemini-flash-latest"
+ ]
+
+ for model_name in model_candidates:
+ try:
+ resp = self.client.models.generate_content(
+ model=model_name,
+ contents=[
+ {"role": "user", "parts": [{"text": prompt}]},
+ ],
+ config=config
+ )
+ return json.loads(resp.text.strip())
+
+ except Exception as e:
+ # If it's the last candidate, we need to handle the error
+ if model_name == model_candidates[-1]:
+ print(f"DEBUG: All model attempts failed. Last error: {e}")
+
+ # SELF-DIAGNOSIS (Kept for future safety)
+ if "404" in str(e):
+ print("\nš DIAGNOSTIC: Fetching list of available models for your key...")
+ try:
+ for m in self.client.models.list():
+ if "generateContent" in (m.supported_actions or []):
+ print(f" - {m.name}")
+ except Exception as list_err:
+ print(f" Could not list models: {list_err}")
+ print("\nš Please update the 'model=' line in gemini.py with one of the above.\n")
+
+ return {"extensions": [".php", ".html", ".json"]}
+ else:
+ continue
\ No newline at end of file
diff --git a/providers/huggingface_local.py b/providers/huggingface_local.py
new file mode 100644
index 0000000..6b7569f
--- /dev/null
+++ b/providers/huggingface_local.py
@@ -0,0 +1,60 @@
+# providers/huggingface_local.py
+
+import json
+from transformers import AutoModelForCausalLM, AutoTokenizer
+import torch
+from .base import AIProvider
+
+SYSTEM_MSG = (
+ "You are a security assistant helping with web fuzzing. "
+ "Return only valid JSON. No prose."
+)
+
+class HuggingFaceLocalProvider(AIProvider):
+ name = "huggingface"
+
+ def __init__(self):
+ model_name = "Qwen/Qwen2.5-1.5B-Instruct"
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name)
+ self.model = AutoModelForCausalLM.from_pretrained(
+ model_name,
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
+ device_map="auto",
+ )
+
+ def get_extensions(self, url, headers, max_extensions):
+ prompt = f"""
+Given the following URL and HTTP headers, suggest the most likely file extensions
+for fuzzing this endpoint.
+
+Respond with valid JSON only.
+Format:
+{{"extensions": [".php", ".json", ".bak"]}}
+
+Limit to at most {max_extensions} extensions.
+
+URL: {url}
+Headers: {headers}
+"""
+
+ messages = [
+ {"role": "system", "content": SYSTEM_MSG},
+ {"role": "user", "content": prompt},
+ ]
+
+ text = self.tokenizer.apply_chat_template(
+ messages, tokenize=False, add_generation_prompt=True
+ )
+
+ inputs = self.tokenizer([text], return_tensors="pt").to(self.model.device)
+
+ output = self.model.generate(
+ **inputs,
+ max_new_tokens=300,
+ do_sample=False,
+ )
+
+ output = output[:, inputs["input_ids"].shape[1]:]
+ raw = self.tokenizer.decode(output[0], skip_special_tokens=True)
+
+ return json.loads(raw.strip())
diff --git a/providers/openai.py b/providers/openai.py
new file mode 100644
index 0000000..36e4925
--- /dev/null
+++ b/providers/openai.py
@@ -0,0 +1,42 @@
+# providers/openai.py
+
+import json
+from openai import OpenAI
+from .base import AIProvider
+
+SYSTEM_MSG = (
+ "You are a security assistant helping with web fuzzing. "
+ "Return only valid JSON. No prose."
+)
+
+class OpenAIProvider(AIProvider):
+ name = "openai"
+
+ def __init__(self, api_key):
+ self.client = OpenAI(api_key=api_key)
+
+ def get_extensions(self, url, headers, max_extensions):
+ prompt = f"""
+Given the following URL and HTTP headers, suggest the most likely file extensions
+for fuzzing this endpoint.
+
+Respond with valid JSON only.
+Format:
+{{"extensions": [".php", ".json", ".bak"]}}
+
+Limit to at most {max_extensions} extensions.
+
+URL: {url}
+Headers: {headers}
+"""
+
+ resp = self.client.chat.completions.create(
+ model="gpt-4o",
+ messages=[
+ {"role": "system", "content": SYSTEM_MSG},
+ {"role": "user", "content": prompt},
+ ],
+ temperature=0,
+ )
+
+ return json.loads(resp.choices[0].message.content.strip())