August 22, 2026
Automating Pentesting with AI: From Nmap & Gobuster to a Complete HTML Security Report
Building a Python-based penetration testing automation pipeline that combines reconnaissance, AI-assisted analysis, and automated HTML…
By Dhondi Sai Karthik
10 min read
Building a Python-based penetration testing automation pipeline that combines reconnaissance, AI-assisted analysis, and automated HTML reporting with Groq.
Penetration testing often involves switching between multiple tools for service discovery, vulnerability enumeration, web directory discovery, analysis, and reporting.
This project combines several of those stages into a single Python-based workflow using Nmap, Gobuster, Python, and Groq AI.
The goal is simple:
Provide an authorized target, automatically collect reconnaissance evidence, send that evidence to an AI model for analysis, and generate a structured HTML penetration-testing report.
This project is intended for authorized security labs, CTFs, training environments, and systems where you have explicit permission to test.
What We're Building
The finished workflow looks like this:
Target
↓
Nmap Service Detection
↓
Nmap Vulnerability Enumeration
↓
Gobuster Web Enumeration
↓
Collect Scan Evidence
↓
AI Analysis
↓
Generate HTML Report
↓
Open Report in FirefoxTarget
↓
Nmap Service Detection
↓
Nmap Vulnerability Enumeration
↓
Gobuster Web Enumeration
↓
Collect Scan Evidence
↓
AI Analysis
↓
Generate HTML Report
↓
Open Report in FirefoxInstead of manually running every tool and then copying the results into an AI prompt, Python coordinates the entire workflow.
1. Create the Project Directory
Start by creating a dedicated project directory:
mkdir -p ~/ai-pentest-lab
cd ~/ai-pentest-labmkdir -p ~/ai-pentest-lab
cd ~/ai-pentest-labThe directory will eventually contain:
ai-pentest-lab/
├── pentest_ai.py
├── targets.txt
├── nmap_results.txt
├── vuln_results.txt
├── gobuster_80.txt
├── gobuster_8180.txt
└── pentest_report.htmlai-pentest-lab/
├── pentest_ai.py
├── targets.txt
├── nmap_results.txt
├── vuln_results.txt
├── gobuster_80.txt
├── gobuster_8180.txt
└── pentest_report.htmlThe scan-result files and HTML report are generated automatically by the Python application.
2. Create the Target File
Create a file to store the authorized target:
nano targets.txtnano targets.txtFor example:
192.168.1.100192.168.1.100Save the file:
CTRL + O
ENTER
CTRL + XCTRL + O
ENTER
CTRL + XThe target can be an IP address or hostname, for example:
192.168.1.100192.168.1.100or:
lab.example.locallab.example.localThe current implementation reads the target from this file and uses it during the Nmap and web-enumeration stages.
Important:_ Only enter systems you own or have explicit authorization to test._
3. Install the Required Tools
Update the package index:
sudo apt updatesudo apt updateInstall Nmap:
sudo apt install nmap -ysudo apt install nmap -yInstall Gobuster:
sudo apt install gobuster -ysudo apt install gobuster -yInstall the wordlists:
sudo apt install wordlists -ysudo apt install wordlists -yThe project uses the following wordlist:
/usr/share/wordlists/dirb/common.txt/usr/share/wordlists/dirb/common.txtVerify the installations:
nmap --version
gobuster versionnmap --version
gobuster version4. Install Python 3
Check whether Python 3 is installed:
python3 --versionpython3 --versionIf required:
sudo apt install python3 python3-pip -ysudo apt install python3 python3-pip -yThe project uses Python for:
- Running security tools
- Reading scan results
- Sending evidence to the AI API
- Processing the AI response
- Generating the HTML report
- Opening the final report
Install the OpenAI Python client:
pip3 install openai --break-system-packagespip3 install openai --break-system-packagesThe OpenAI client library is used because Groq provides an OpenAI-compatible API endpoint.
5. Configure the Groq API Key
The API key should not be written directly into the Python source code.
Instead, create an environment variable:
export GROQ_API_KEY="YOUR_GROQ_API_KEY"export GROQ_API_KEY="YOUR_GROQ_API_KEY"The Python application retrieves it with:
api_key = os.getenv("GROQ_API_KEY")api_key = os.getenv("GROQ_API_KEY")This means the API key remains separate from the source code.
If the project is published publicly, the actual API key should never be included in the article, source code, screenshots, or Git repository.
6. Create the Python Source File
Create the main application:
nano pentest_ai.pynano pentest_ai.pyPaste the complete source code below:
#!/usr/bin/env python3
import os
import subprocess
import shutil
import html
from pathlib import Path
from datetime import datetime
from openai import OpenAI
# ============================================================
# AI PENTEST AUTOMATION
# Authorized laboratory / training environments only
# ============================================================
TARGET_FILE = "targets.txt"
NMAP_FILE = "nmap_results.txt"
VULN_FILE = "vuln_results.txt"
GOBUSTER_80_FILE = "gobuster_80.txt"
GOBUSTER_8180_FILE = "gobuster_8180.txt"
REPORT_FILE = "/root/ai-pentest-lab/pentest_report.html"
USER_REPORT_FILE = "/home/kali/pentest_report.html"
MODEL = "openai/gpt-oss-120b"
MAX_EVIDENCE_CHARS = 5000
MAX_OUTPUT_TOKENS = 4500
# ============================================================
# Terminal formatting
# ============================================================
def banner():
print()
print("=" * 70)
print(" AI PENTEST AUTOMATION")
print("=" * 70)
print()
# ============================================================
# File helpers
# ============================================================
def read_file(filename):
path = Path(filename)
if not path.exists():
print(f"[!] {filename} not found")
return ""
return path.read_text(errors="ignore")
def write_file(filename, content):
Path(filename).write_text(content, encoding="utf-8")
def trim_text(text, max_chars):
if len(text) <= max_chars:
return text
return text[:max_chars] + "\n...[evidence truncated]..."
# ============================================================
# Run command
# ============================================================
def run_command(command, output_file):
print()
print("[+] Running:")
print(" " + " ".join(command))
try:
with open(output_file, "w", encoding="utf-8") as outfile:
process = subprocess.run(
command,
stdout=outfile,
stderr=subprocess.STDOUT,
text=True
)
if process.returncode != 0:
print(
f"[!] Command returned exit code "
f"{process.returncode}"
)
print(f"[+] Saved output to {output_file}")
except Exception as e:
print(f"[!] Error running command: {e}")
# ============================================================
# Get target
# ============================================================
target = read_file(TARGET_FILE).strip()
if not target:
raise SystemExit(
"[!] targets.txt is empty or missing"
)
# ============================================================
# API key
# ============================================================
api_key = os.getenv("GROQ_API_KEY")
if not api_key:
raise SystemExit(
"GROQ_API_KEY is not set"
)
# ============================================================
# Start
# ============================================================
banner()
print(f"[+] Target: {target}")
# ============================================================
# 1. Nmap service detection
# ============================================================
run_command(
[
"nmap",
"-sV",
"-oN",
NMAP_FILE,
"-iL",
TARGET_FILE
],
NMAP_FILE
)
# ============================================================
# 2. Nmap NSE vulnerability scan
# ============================================================
run_command(
[
"nmap",
"-sV",
"--script",
"vuln",
"-iL",
TARGET_FILE,
"-oN",
VULN_FILE
],
VULN_FILE
)
# ============================================================
# 3. Gobuster HTTP port 80
# ============================================================
run_command(
[
"gobuster",
"dir",
"-u",
f"http://{target}/",
"-w",
"/usr/share/wordlists/dirb/common.txt",
"-o",
GOBUSTER_80_FILE
],
GOBUSTER_80_FILE
)
# ============================================================
# 4. Gobuster HTTP port 8180
# ============================================================
run_command(
[
"gobuster",
"dir",
"-u",
f"http://{target}:8180/",
"-w",
"/usr/share/wordlists/dirb/common.txt",
"-o",
GOBUSTER_8180_FILE
],
GOBUSTER_8180_FILE
)
# ============================================================
# Load evidence
# ============================================================
print()
print("[+] Loading pentest evidence...")
nmap_results = read_file(NMAP_FILE)
vuln_results = read_file(VULN_FILE)
gobuster_80 = read_file(GOBUSTER_80_FILE)
gobuster_8180 = read_file(GOBUSTER_8180_FILE)
print(
f"[+] Nmap results: "
f"{len(nmap_results)} characters"
)
print(
f"[+] NSE results: "
f"{len(vuln_results)} characters"
)
print(
f"[+] Gobuster :80: "
f"{len(gobuster_80)} characters"
)
print(
f"[+] Gobuster :8180: "
f"{len(gobuster_8180)} characters"
)
# ============================================================
# Build AI evidence
# ============================================================
evidence = f"""
TARGET
------
{target}
NMAP SERVICE DETECTION
----------------------
{nmap_results}
GOBUSTER HTTP PORT 80
---------------------
{gobuster_80}
GOBUSTER HTTP PORT 8180
-----------------------
{gobuster_8180}
NMAP NSE VULNERABILITY RESULTS
------------------------------
{vuln_results}
"""
evidence = trim_text(
evidence,
MAX_EVIDENCE_CHARS
)
print(
f"[+] Evidence sent to Groq: "
f"{len(evidence)} characters"
)
# ============================================================
# AI system prompt
# ============================================================
system_prompt = """
You are a cybersecurity penetration-testing assistant.
The target is an AUTHORIZED laboratory/training machine.
Analyze ONLY the evidence supplied by the user.
Do NOT invent:
- vulnerabilities
- CVEs
- credentials
- exploit results
- successful compromises
- service versions
- authentication results
CRITICAL EVIDENCE RULE:
A service being open does NOT mean it was exploited.
A version being old does NOT prove exploitation.
An HTTP 200/301/302 response proves accessibility or
redirection, but does NOT prove that the endpoint is
vulnerable.
A vulnerability should be classified as CONFIRMED only when
the supplied evidence explicitly demonstrates it.
Use these categories:
CONFIRMED
LIKELY
POSSIBLE
INFORMATIONAL
For every finding include:
- Finding
- Severity
- Evidence
- Explanation
- Validation status
Use the exact evidence where possible.
Do not claim that default credentials work unless the supplied
evidence demonstrates that they work.
Do not claim root access unless the supplied evidence
demonstrates root access.
The assessment should be useful for an authorized lab exercise.
Produce a COMPLETE report.
Do not stop in the middle of a sentence.
Use exactly these sections:
1. Executive Summary
2. Attack Surface
3. Confirmed Findings
4. Likely Findings
5. Possible Findings
6. Web Enumeration Findings
7. Risk Prioritization
8. Recommended Validation Steps
9. Final Assessment
At the end include:
"End of Report"
Keep the report concise enough to fit within the response limit.
"""
# ============================================================
# AI user prompt
# ============================================================
user_prompt = f"""
Analyze this authorized penetration-testing evidence.
TARGET:
{target}
EVIDENCE:
{evidence}
Generate the complete penetration-test assessment now.
"""
# ============================================================
# Groq client
# ============================================================
client = OpenAI(
api_key=api_key,
base_url="https://api.groq.com/openai/v1"
)
# ============================================================
# Send to AI
# ============================================================
print()
print("[+] Sending evidence to Groq...")
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": user_prompt
}
],
max_tokens=MAX_OUTPUT_TOKENS,
temperature=0.1
)
except Exception as e:
print()
print("[!] Groq API request failed:")
print(e)
raise SystemExit(1)
# ============================================================
# Get AI report
# ============================================================
report = response.choices[0].message.content
if not report:
raise SystemExit(
"[!] Groq returned an empty report"
)
print()
print(
f"[+] AI report received: "
f"{len(report)} characters"
)
# ============================================================
# Convert simple Markdown to HTML
# ============================================================
def markdown_to_html(text):
lines = text.splitlines()
output = []
in_table = False
table_header_done = False
for line in lines:
stripped = line.strip()
if not stripped:
if in_table:
output.append("</table>")
in_table = False
table_header_done = False
continue
if stripped in ["---", "***", "___"]:
if in_table:
output.append("</table>")
in_table = False
table_header_done = False
output.append("<hr>")
continue
if stripped.startswith("### "):
if in_table:
output.append("</table>")
in_table = False
table_header_done = False
heading = html.escape(
stripped[4:]
)
output.append(
f"<h3>{heading}</h3>"
)
continue
if stripped.startswith("## "):
if in_table:
output.append("</table>")
in_table = False
table_header_done = False
heading = html.escape(
stripped[3:]
)
output.append(
f"<h2>{heading}</h2>"
)
continue
if stripped.startswith("# "):
if in_table:
output.append("</table>")
in_table = False
table_header_done = False
heading = html.escape(
stripped[2:]
)
output.append(
f"<h1>{heading}</h1>"
)
continue
if (
len(stripped) > 3
and stripped[0].isdigit()
and ". " in stripped[:4]
):
if in_table:
output.append("</table>")
in_table = False
table_header_done = False
output.append(
"<h2>" +
html.escape(stripped) +
"</h2>"
)
continue
if stripped.startswith("|") and stripped.endswith("|"):
cells = [
cell.strip()
for cell in stripped.strip("|").split("|")
]
if all(
set(cell) <= set("-: ")
for cell in cells
):
continue
if not in_table:
output.append(
"<table>"
)
in_table = True
table_header_done = False
tag = (
"th"
if not table_header_done
else "td"
)
row = "<tr>"
for cell in cells:
cell_html = html.escape(
cell
)
row += (
f"<{tag}>"
f"{cell_html}"
f"</{tag}>"
)
row += "</tr>"
output.append(row)
if not table_header_done:
table_header_done = True
continue
if stripped.startswith("* "):
output.append(
"<li>" +
html.escape(
stripped[2:]
) +
"</li>"
)
continue
if stripped.startswith("- "):
output.append(
"<li>" +
html.escape(
stripped[2:]
) +
"</li>"
)
continue
paragraph = html.escape(
stripped
)
paragraph = paragraph.replace(
"**",
"<strong>",
1
) if paragraph.count("**") >= 2 else paragraph
if "<strong>" in paragraph:
paragraph = paragraph.replace(
"**",
"</strong>",
1
)
output.append(
f"<p>{paragraph}</p>"
)
if in_table:
output.append("</table>")
return "\n".join(output)
# ============================================================
# Create HTML
# ============================================================
generated_time = datetime.now().strftime(
"%Y-%m-%d %H:%M:%S"
)
report_html = markdown_to_html(
report
)
full_html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width,
initial-scale=1.0">
<title>
AI Penetration Test Report - {html.escape(target)}
</title>
<style>
body {{
font-family:
Arial,
Helvetica,
sans-serif;
background: #f4f6f8;
color: #222;
margin: 0;
padding: 0;
line-height: 1.6;
}}
.container {{
max-width: 1200px;
margin: 30px auto;
background: white;
padding: 40px;
border-radius: 10px;
box-shadow:
0 4px 20px
rgba(0,0,0,0.08);
}}
.header {{
border-bottom:
3px solid #222;
padding-bottom: 20px;
margin-bottom: 30px;
}}
.header h1 {{
margin-bottom: 5px;
}}
.meta {{
color: #666;
font-size: 14px;
}}
h1 {{
color: #111;
}}
h2 {{
margin-top: 35px;
padding-bottom: 8px;
border-bottom:
1px solid #ddd;
}}
h3 {{
margin-top: 25px;
}}
table {{
width: 100%;
border-collapse:
collapse;
margin: 20px 0;
font-size: 14px;
}}
th {{
background: #222;
color: white;
padding: 10px;
border: 1px solid #ccc;
text-align: left;
}}
td {{
padding: 10px;
border: 1px solid #ccc;
vertical-align: top;
}}
tr:nth-child(even) {{
background: #f7f7f7;
}}
li {{
margin-bottom: 8px;
}}
hr {{
border: 0;
border-top:
1px solid #ddd;
margin: 30px 0;
}}
.footer {{
margin-top: 40px;
padding-top: 20px;
border-top:
1px solid #ddd;
color: #777;
font-size: 13px;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>
AI Penetration Test Report
</h1>
<div class="meta">
<strong>Target:</strong>
{html.escape(target)}
<br>
<strong>Generated:</strong>
{generated_time}
<br>
Authorized laboratory /
training assessment.
</div>
</div>
{report_html}
<div class="footer">
AI Pentest Automation<br>
Findings are based only on
the collected scan evidence.
AI-generated classifications
should be manually validated.
</div>
</div>
</body>
</html>
"""
# ============================================================
# Save HTML report
# ============================================================
write_file(
REPORT_FILE,
full_html
)
print()
print("=" * 70)
print(" PENTEST COMPLETE")
print("=" * 70)
print()
print("[+] HTML report created:")
print(f" {REPORT_FILE}")
# ============================================================
# Copy report to desktop user
# ============================================================
try:
print()
print("[+] Preparing report for Firefox...")
shutil.copy2(
REPORT_FILE,
USER_REPORT_FILE
)
subprocess.run(
[
"chown",
"kali:kali",
USER_REPORT_FILE
],
check=True
)
print(
f"[+] User report created:"
)
print(
f" {USER_REPORT_FILE}"
)
except Exception as e:
print()
print(
"[!] Could not copy report "
"to /home/kali:"
)
print(e)
# ============================================================
# Open Firefox as normal desktop user
# ============================================================
try:
print()
print(
"[+] Opening report in Firefox..."
)
subprocess.Popen(
[
"sudo",
"-u",
"kali",
"firefox",
USER_REPORT_FILE
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
print(
"[+] Firefox launch command sent."
)
except Exception as e:
print()
print(
"[!] Could not automatically "
"open Firefox:"
)
print(e)
print()#!/usr/bin/env python3
import os
import subprocess
import shutil
import html
from pathlib import Path
from datetime import datetime
from openai import OpenAI
# ============================================================
# AI PENTEST AUTOMATION
# Authorized laboratory / training environments only
# ============================================================
TARGET_FILE = "targets.txt"
NMAP_FILE = "nmap_results.txt"
VULN_FILE = "vuln_results.txt"
GOBUSTER_80_FILE = "gobuster_80.txt"
GOBUSTER_8180_FILE = "gobuster_8180.txt"
REPORT_FILE = "/root/ai-pentest-lab/pentest_report.html"
USER_REPORT_FILE = "/home/kali/pentest_report.html"
MODEL = "openai/gpt-oss-120b"
MAX_EVIDENCE_CHARS = 5000
MAX_OUTPUT_TOKENS = 4500
# ============================================================
# Terminal formatting
# ============================================================
def banner():
print()
print("=" * 70)
print(" AI PENTEST AUTOMATION")
print("=" * 70)
print()
# ============================================================
# File helpers
# ============================================================
def read_file(filename):
path = Path(filename)
if not path.exists():
print(f"[!] {filename} not found")
return ""
return path.read_text(errors="ignore")
def write_file(filename, content):
Path(filename).write_text(content, encoding="utf-8")
def trim_text(text, max_chars):
if len(text) <= max_chars:
return text
return text[:max_chars] + "\n...[evidence truncated]..."
# ============================================================
# Run command
# ============================================================
def run_command(command, output_file):
print()
print("[+] Running:")
print(" " + " ".join(command))
try:
with open(output_file, "w", encoding="utf-8") as outfile:
process = subprocess.run(
command,
stdout=outfile,
stderr=subprocess.STDOUT,
text=True
)
if process.returncode != 0:
print(
f"[!] Command returned exit code "
f"{process.returncode}"
)
print(f"[+] Saved output to {output_file}")
except Exception as e:
print(f"[!] Error running command: {e}")
# ============================================================
# Get target
# ============================================================
target = read_file(TARGET_FILE).strip()
if not target:
raise SystemExit(
"[!] targets.txt is empty or missing"
)
# ============================================================
# API key
# ============================================================
api_key = os.getenv("GROQ_API_KEY")
if not api_key:
raise SystemExit(
"GROQ_API_KEY is not set"
)
# ============================================================
# Start
# ============================================================
banner()
print(f"[+] Target: {target}")
# ============================================================
# 1. Nmap service detection
# ============================================================
run_command(
[
"nmap",
"-sV",
"-oN",
NMAP_FILE,
"-iL",
TARGET_FILE
],
NMAP_FILE
)
# ============================================================
# 2. Nmap NSE vulnerability scan
# ============================================================
run_command(
[
"nmap",
"-sV",
"--script",
"vuln",
"-iL",
TARGET_FILE,
"-oN",
VULN_FILE
],
VULN_FILE
)
# ============================================================
# 3. Gobuster HTTP port 80
# ============================================================
run_command(
[
"gobuster",
"dir",
"-u",
f"http://{target}/",
"-w",
"/usr/share/wordlists/dirb/common.txt",
"-o",
GOBUSTER_80_FILE
],
GOBUSTER_80_FILE
)
# ============================================================
# 4. Gobuster HTTP port 8180
# ============================================================
run_command(
[
"gobuster",
"dir",
"-u",
f"http://{target}:8180/",
"-w",
"/usr/share/wordlists/dirb/common.txt",
"-o",
GOBUSTER_8180_FILE
],
GOBUSTER_8180_FILE
)
# ============================================================
# Load evidence
# ============================================================
print()
print("[+] Loading pentest evidence...")
nmap_results = read_file(NMAP_FILE)
vuln_results = read_file(VULN_FILE)
gobuster_80 = read_file(GOBUSTER_80_FILE)
gobuster_8180 = read_file(GOBUSTER_8180_FILE)
print(
f"[+] Nmap results: "
f"{len(nmap_results)} characters"
)
print(
f"[+] NSE results: "
f"{len(vuln_results)} characters"
)
print(
f"[+] Gobuster :80: "
f"{len(gobuster_80)} characters"
)
print(
f"[+] Gobuster :8180: "
f"{len(gobuster_8180)} characters"
)
# ============================================================
# Build AI evidence
# ============================================================
evidence = f"""
TARGET
------
{target}
NMAP SERVICE DETECTION
----------------------
{nmap_results}
GOBUSTER HTTP PORT 80
---------------------
{gobuster_80}
GOBUSTER HTTP PORT 8180
-----------------------
{gobuster_8180}
NMAP NSE VULNERABILITY RESULTS
------------------------------
{vuln_results}
"""
evidence = trim_text(
evidence,
MAX_EVIDENCE_CHARS
)
print(
f"[+] Evidence sent to Groq: "
f"{len(evidence)} characters"
)
# ============================================================
# AI system prompt
# ============================================================
system_prompt = """
You are a cybersecurity penetration-testing assistant.
The target is an AUTHORIZED laboratory/training machine.
Analyze ONLY the evidence supplied by the user.
Do NOT invent:
- vulnerabilities
- CVEs
- credentials
- exploit results
- successful compromises
- service versions
- authentication results
CRITICAL EVIDENCE RULE:
A service being open does NOT mean it was exploited.
A version being old does NOT prove exploitation.
An HTTP 200/301/302 response proves accessibility or
redirection, but does NOT prove that the endpoint is
vulnerable.
A vulnerability should be classified as CONFIRMED only when
the supplied evidence explicitly demonstrates it.
Use these categories:
CONFIRMED
LIKELY
POSSIBLE
INFORMATIONAL
For every finding include:
- Finding
- Severity
- Evidence
- Explanation
- Validation status
Use the exact evidence where possible.
Do not claim that default credentials work unless the supplied
evidence demonstrates that they work.
Do not claim root access unless the supplied evidence
demonstrates root access.
The assessment should be useful for an authorized lab exercise.
Produce a COMPLETE report.
Do not stop in the middle of a sentence.
Use exactly these sections:
1. Executive Summary
2. Attack Surface
3. Confirmed Findings
4. Likely Findings
5. Possible Findings
6. Web Enumeration Findings
7. Risk Prioritization
8. Recommended Validation Steps
9. Final Assessment
At the end include:
"End of Report"
Keep the report concise enough to fit within the response limit.
"""
# ============================================================
# AI user prompt
# ============================================================
user_prompt = f"""
Analyze this authorized penetration-testing evidence.
TARGET:
{target}
EVIDENCE:
{evidence}
Generate the complete penetration-test assessment now.
"""
# ============================================================
# Groq client
# ============================================================
client = OpenAI(
api_key=api_key,
base_url="https://api.groq.com/openai/v1"
)
# ============================================================
# Send to AI
# ============================================================
print()
print("[+] Sending evidence to Groq...")
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": user_prompt
}
],
max_tokens=MAX_OUTPUT_TOKENS,
temperature=0.1
)
except Exception as e:
print()
print("[!] Groq API request failed:")
print(e)
raise SystemExit(1)
# ============================================================
# Get AI report
# ============================================================
report = response.choices[0].message.content
if not report:
raise SystemExit(
"[!] Groq returned an empty report"
)
print()
print(
f"[+] AI report received: "
f"{len(report)} characters"
)
# ============================================================
# Convert simple Markdown to HTML
# ============================================================
def markdown_to_html(text):
lines = text.splitlines()
output = []
in_table = False
table_header_done = False
for line in lines:
stripped = line.strip()
if not stripped:
if in_table:
output.append("</table>")
in_table = False
table_header_done = False
continue
if stripped in ["---", "***", "___"]:
if in_table:
output.append("</table>")
in_table = False
table_header_done = False
output.append("<hr>")
continue
if stripped.startswith("### "):
if in_table:
output.append("</table>")
in_table = False
table_header_done = False
heading = html.escape(
stripped[4:]
)
output.append(
f"<h3>{heading}</h3>"
)
continue
if stripped.startswith("## "):
if in_table:
output.append("</table>")
in_table = False
table_header_done = False
heading = html.escape(
stripped[3:]
)
output.append(
f"<h2>{heading}</h2>"
)
continue
if stripped.startswith("# "):
if in_table:
output.append("</table>")
in_table = False
table_header_done = False
heading = html.escape(
stripped[2:]
)
output.append(
f"<h1>{heading}</h1>"
)
continue
if (
len(stripped) > 3
and stripped[0].isdigit()
and ". " in stripped[:4]
):
if in_table:
output.append("</table>")
in_table = False
table_header_done = False
output.append(
"<h2>" +
html.escape(stripped) +
"</h2>"
)
continue
if stripped.startswith("|") and stripped.endswith("|"):
cells = [
cell.strip()
for cell in stripped.strip("|").split("|")
]
if all(
set(cell) <= set("-: ")
for cell in cells
):
continue
if not in_table:
output.append(
"<table>"
)
in_table = True
table_header_done = False
tag = (
"th"
if not table_header_done
else "td"
)
row = "<tr>"
for cell in cells:
cell_html = html.escape(
cell
)
row += (
f"<{tag}>"
f"{cell_html}"
f"</{tag}>"
)
row += "</tr>"
output.append(row)
if not table_header_done:
table_header_done = True
continue
if stripped.startswith("* "):
output.append(
"<li>" +
html.escape(
stripped[2:]
) +
"</li>"
)
continue
if stripped.startswith("- "):
output.append(
"<li>" +
html.escape(
stripped[2:]
) +
"</li>"
)
continue
paragraph = html.escape(
stripped
)
paragraph = paragraph.replace(
"**",
"<strong>",
1
) if paragraph.count("**") >= 2 else paragraph
if "<strong>" in paragraph:
paragraph = paragraph.replace(
"**",
"</strong>",
1
)
output.append(
f"<p>{paragraph}</p>"
)
if in_table:
output.append("</table>")
return "\n".join(output)
# ============================================================
# Create HTML
# ============================================================
generated_time = datetime.now().strftime(
"%Y-%m-%d %H:%M:%S"
)
report_html = markdown_to_html(
report
)
full_html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width,
initial-scale=1.0">
<title>
AI Penetration Test Report - {html.escape(target)}
</title>
<style>
body {{
font-family:
Arial,
Helvetica,
sans-serif;
background: #f4f6f8;
color: #222;
margin: 0;
padding: 0;
line-height: 1.6;
}}
.container {{
max-width: 1200px;
margin: 30px auto;
background: white;
padding: 40px;
border-radius: 10px;
box-shadow:
0 4px 20px
rgba(0,0,0,0.08);
}}
.header {{
border-bottom:
3px solid #222;
padding-bottom: 20px;
margin-bottom: 30px;
}}
.header h1 {{
margin-bottom: 5px;
}}
.meta {{
color: #666;
font-size: 14px;
}}
h1 {{
color: #111;
}}
h2 {{
margin-top: 35px;
padding-bottom: 8px;
border-bottom:
1px solid #ddd;
}}
h3 {{
margin-top: 25px;
}}
table {{
width: 100%;
border-collapse:
collapse;
margin: 20px 0;
font-size: 14px;
}}
th {{
background: #222;
color: white;
padding: 10px;
border: 1px solid #ccc;
text-align: left;
}}
td {{
padding: 10px;
border: 1px solid #ccc;
vertical-align: top;
}}
tr:nth-child(even) {{
background: #f7f7f7;
}}
li {{
margin-bottom: 8px;
}}
hr {{
border: 0;
border-top:
1px solid #ddd;
margin: 30px 0;
}}
.footer {{
margin-top: 40px;
padding-top: 20px;
border-top:
1px solid #ddd;
color: #777;
font-size: 13px;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>
AI Penetration Test Report
</h1>
<div class="meta">
<strong>Target:</strong>
{html.escape(target)}
<br>
<strong>Generated:</strong>
{generated_time}
<br>
Authorized laboratory /
training assessment.
</div>
</div>
{report_html}
<div class="footer">
AI Pentest Automation<br>
Findings are based only on
the collected scan evidence.
AI-generated classifications
should be manually validated.
</div>
</div>
</body>
</html>
"""
# ============================================================
# Save HTML report
# ============================================================
write_file(
REPORT_FILE,
full_html
)
print()
print("=" * 70)
print(" PENTEST COMPLETE")
print("=" * 70)
print()
print("[+] HTML report created:")
print(f" {REPORT_FILE}")
# ============================================================
# Copy report to desktop user
# ============================================================
try:
print()
print("[+] Preparing report for Firefox...")
shutil.copy2(
REPORT_FILE,
USER_REPORT_FILE
)
subprocess.run(
[
"chown",
"kali:kali",
USER_REPORT_FILE
],
check=True
)
print(
f"[+] User report created:"
)
print(
f" {USER_REPORT_FILE}"
)
except Exception as e:
print()
print(
"[!] Could not copy report "
"to /home/kali:"
)
print(e)
# ============================================================
# Open Firefox as normal desktop user
# ============================================================
try:
print()
print(
"[+] Opening report in Firefox..."
)
subprocess.Popen(
[
"sudo",
"-u",
"kali",
"firefox",
USER_REPORT_FILE
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
print(
"[+] Firefox launch command sent."
)
except Exception as e:
print()
print(
"[!] Could not automatically "
"open Firefox:"
)
print(e)
print()Save the file:
CTRL + O
ENTER
CTRL + XCTRL + O
ENTER
CTRL + XThen make it executable:
chmod +x pentest_ai.pychmod +x pentest_ai.py7. Run the Automation
Everything is now ready.
From the project directory:
cd ~/ai-pentest-labcd ~/ai-pentest-labMake sure the API key is available:
export GROQ_API_KEY="YOUR_GROQ_API_KEY"export GROQ_API_KEY="YOUR_GROQ_API_KEY"Then run:
python3 pentest_ai.pypython3 pentest_ai.pyThe application will execute the complete workflow automatically.
8. What Happens When the Script Runs?
The Python application performs several stages.
Stage 1 — Target Loading
The script reads:
targets.txttargets.txtand obtains the target that will be assessed.
Stage 2 — Service Detection
Nmap performs service and version detection:
nmap -sVnmap -sVThe results are saved to:
nmap_results.txtnmap_results.txtStage 3 — Vulnerability Enumeration
Nmap's vulnerability NSE scripts are executed:
nmap -sV --script vulnnmap -sV --script vulnThe results are saved to:
vuln_results.txtvuln_results.txtStage 4 — Web Enumeration
Gobuster checks the configured HTTP services using the supplied wordlist.
The results are saved to:
gobuster_80.txt
gobuster_8180.txtgobuster_80.txt
gobuster_8180.txtStage 5 — Evidence Collection
Python reads all four result files and combines them into a single evidence package.
The evidence is deliberately limited before being sent to the AI model so that the request stays within the configured token budget.
Stage 6 — AI Analysis
The collected evidence is sent to the Groq API.
The AI is instructed to:
- Analyze only supplied evidence
- Avoid inventing vulnerabilities
- Distinguish confirmed and unconfirmed findings
- Include evidence for findings
- Prioritize risks
- Recommend validation steps
Stage 7 — HTML Generation
The AI response is converted into HTML.
The Python application creates a complete report with:
- Header
- Target information
- Timestamp
- Findings
- Tables
- Sections
- Footer
- Basic responsive styling
Stage 8 — Report Opening
Finally, the generated report is copied to:
/home/kali/pentest_report.html/home/kali/pentest_report.htmland Firefox is launched to display it.
9. The Automated Report
The final report is generated automatically at:
/root/ai-pentest-lab/pentest_report.html/root/ai-pentest-lab/pentest_report.htmlA copy is also placed at:
/home/kali/pentest_report.html/home/kali/pentest_report.htmlYou can open it manually with:
firefox /home/kali/pentest_report.htmlfirefox /home/kali/pentest_report.htmlNo separate HTML template needs to be created.
The Python program generates the HTML document itself.
10. Final Architecture
The complete project can be visualized as:
targets.txt
│
▼
┌───────────────┐
│ Nmap │
│ Service Scan │
└───────┬───────┘
│
▼
┌───────────────┐
│ Nmap │
│ NSE Vuln │
└───────┬───────┘
│
▼
┌───────────────┐
│ Gobuster │
│ Web Discovery │
└───────┬───────┘
│
▼
┌───────────────┐
│ Scan Evidence │
└───────┬───────┘
│
▼
┌───────────────┐
│ Groq AI │
│ Analysis │
└───────┬───────┘
│
▼
┌───────────────┐
│ HTML Report │
└───────┬───────┘
│
▼
Firefoxtargets.txt
│
▼
┌───────────────┐
│ Nmap │
│ Service Scan │
└───────┬───────┘
│
▼
┌───────────────┐
│ Nmap │
│ NSE Vuln │
└───────┬───────┘
│
▼
┌───────────────┐
│ Gobuster │
│ Web Discovery │
└───────┬───────┘
│
▼
┌───────────────┐
│ Scan Evidence │
└───────┬───────┘
│
▼
┌───────────────┐
│ Groq AI │
│ Analysis │
└───────┬───────┘
│
▼
┌───────────────┐
│ HTML Report │
└───────┬───────┘
│
▼
FirefoxThe result is a repeatable security-assessment pipeline where reconnaissance output becomes structured evidence, AI performs evidence-based analysis, and the final assessment is automatically presented as an HTML report.
Quick Start — Everything in One Place
After the initial installation and source-code setup, the workflow becomes very simple.
Go to the project
cd ~/ai-pentest-labcd ~/ai-pentest-labSet your authorized target
nano targets.txtnano targets.txtAdd:
192.168.1.100192.168.1.100Save:
CTRL + O
ENTER
CTRL + XCTRL + O
ENTER
CTRL + XSet the API key
export GROQ_API_KEY="YOUR_GROQ_API_KEY"export GROQ_API_KEY="YOUR_GROQ_API_KEY"Run the tool
python3 pentest_ai.pypython3 pentest_ai.pyThat's it.
The application automatically performs:
Target
↓
Nmap
↓
Nmap NSE
↓
Gobuster
↓
Evidence Collection
↓
Groq AI
↓
HTML Report
↓
FirefoxTarget
↓
Nmap
↓
Nmap NSE
↓
Gobuster
↓
Evidence Collection
↓
Groq AI
↓
HTML Report
↓
FirefoxQuick Reference
The entire workflow can therefore be reduced to:
cd ~/ai-pentest-lab
nano targets.txt
export GROQ_API_KEY="YOUR_GROQ_API_KEY"
python3 pentest_ai.pycd ~/ai-pentest-lab
nano targets.txt
export GROQ_API_KEY="YOUR_GROQ_API_KEY"
python3 pentest_ai.pyTarget → API key → run.
Everything else is automated.
Final Notes
This project demonstrates how traditional security tooling can be combined with AI-assisted analysis and automated reporting.
Nmap and Gobuster perform the reconnaissance and enumeration, Python acts as the orchestration layer, Groq provides the AI analysis, and the Python HTML generator turns the resulting assessment into a readable report.
Most importantly, the AI is instructed to base its conclusions on actual collected evidence rather than assumptions.
The same architecture can later be extended with additional authorized security tools, richer evidence processing, vulnerability databases, dashboards, or additional reporting formats.
Always use automated security tooling only against systems you own or have explicit permission to assess.