"""web_pentest_quick.py — OWASP Top 10 Quick Scan + PDF Report Generator
Usage: python web_pentest_quick.py https://target.com
Produces: pentest-report-<target>.pdf
"""
import sys, json, socket, ssl, datetime
from urllib.parse import urlparse
import requests
from fpdf import FPDF
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class QuickPentest:
def __init__(self, base_url):
self.base_url = base_url.rstrip("/")
self.domain = urlparse(base_url).netloc
self.report = {
"target": base_url,
"scanned_at": datetime.datetime.utcnow().isoformat(),
"findings": [],
"recommendations": [],
"summary": {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0},
}
self.session = requests.Session()
self.session.verify = False
self.session.timeout = 15
def _add_finding(self, title, severity, description, evidence, remediation):
self.report["summary"][severity.lower()] += 1
self.report["findings"].append({
"title": title, "severity": severity,
"description": description,
"evidence": evidence[:500],
"remediation": remediation,
})
def check_security_headers(self):
try:
r = self.session.get(self.base_url)
headers = r.headers
checks = [
("Strict-Transport-Security", "Missing HSTS header — exposes users to downgrade attacks",
"Add `Strict-Transport-Security: max-age=31536000; includeSubDomains`"),
("X-Frame-Options", "Missing clickjacking protection",
"Add `X-Frame-Options: DENY` or `SAMEORIGIN`"),
("X-Content-Type-Options", "Missing MIME-sniffing protection",
"Add `X-Content-Type-Options: nosniff`"),
("Content-Security-Policy", "No CSP header — XSS risk is higher",
"Implement a Content-Security-Policy header restricting script sources"),
("X-XSS-Protection", "Missing XSS filter header",
"Add `X-XSS-Protection: 1; mode=block`"),
]
for header, desc, fix in checks:
if header not in headers:
self._add_finding(f"Missing {header}", "medium", desc, "", fix)
if "Server" in headers:
self._add_finding("Server header disclosure", "low",
f"Server: {headers['Server']} — reveals technology stack",
headers["Server"], "Remove or obfuscate the Server header")
except Exception as e:
self._add_finding("Connection failed", "high",
f"Could not connect to {self.base_url}: {e}", str(e),
"Verify the target is reachable and responds to HTTP requests")
def check_ssl(self):
try:
host, port = self.domain, 443
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
with socket.create_connection((host, port), timeout=10) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as ssock:
ver = ssock.version()
cipher = ssock.cipher()
if ver and ver.startswith("TLSv1"):
sev = "high" if "1.0" in ver or "1.1" in ver else "medium"
self._add_finding(f"Weak TLS version: {ver}", sev,
f"Server supports {ver} which is deprecated",
f"Protocol: {ver}, Cipher: {cipher}", "Disable TLS 1.0/1.1, enforce TLS 1.2+")
except Exception as e:
self._add_finding("SSL check failed", "medium",
f"Could not assess TLS: {e}", str(e), "Verify SSL certificate is valid and port 443 is open")
def check_common_paths(self):
common = [
"/admin", "/.env", "/.git/config", "/wp-admin", "/backup",
"/robots.txt", "/sitemap.xml", "/crossdomain.xml", "/phpinfo.php",
"/.well-known/security.txt",
]
for path in common:
url = f"{self.base_url}{path}"
try:
r = self.session.get(url, allow_redirects=False)
codes = {200: "accessible without auth", 301: "redirects (check destination)",
302: "redirects (check destination)", 403: "exists but blocked (403 — may leak info)",
401: "exists with auth required"}
if r.status_code in codes:
self._add_finding(f"Sensitive path discovered: {path}", "high" if r.status_code == 200 else "medium",
f"{path} returned HTTP {r.status_code} ({codes.get(r.status_code, 'unknown')})",
f"GET {url} -> {r.status_code}",
f"Restrict access to {path} or remove if unintended")
except requests.RequestException:
pass
def check_cors(self):
try:
r = self.session.get(self.base_url, headers={"Origin": "https://evil.com"})
acao = r.headers.get("Access-Control-Allow-Origin", "")
if "evil.com" in acao or acao == "*":
self._add_finding("CORS misconfiguration", "high",
f"Origin 'https://evil.com' is reflected in ACAO header",
f"Origin sent: https://evil.com, ACAO: {acao}",
"Do not reflect untrusted origins. Use an allowlist.")
except Exception:
pass
def check_cookies(self):
try:
r = self.session.get(self.base_url)
for cookie in self.session.cookies:
issues = []
attrs = cookie.__dict__
if not cookie.secure:
issues.append("Missing Secure flag")
if cookie.path and not cookie.has_nonstandard_attr("HttpOnly"):
issues.append("Missing HttpOnly flag")
if not attrs.get("_rest", {}).get("samesite"):
issues.append("Missing SameSite attribute")
if issues:
self._add_finding(f"Insecure cookie: {cookie.name}", "medium",
f"Cookie '{cookie.name}' has issues: {', '.join(issues)}",
f"Cookie: {cookie.name}={cookie.value}", f"Set {', '.join(issues)}")
except Exception:
pass
def check_verbose_errors(self):
test_paths = ["/nonexistent123", "/api/../../etc/passwd"]
for tp in test_paths:
try:
r = self.session.get(f"{self.base_url}{tp}")
if any(sig in r.text.lower() for sig in ["stack trace", "traceback", "file_get_contents",
"syntax error", "unexpected t_", "mysql_fetch"]):
self._add_finding(f"Verbose error on {tp}", "medium",
f"Application leaks internal error details at {tp}",
f"HTTP {r.status_code}: snippet visible",
"Disable debug output in production. Use generic error pages.")
except Exception:
pass
def run(self):
print(f"[*] Scanning {self.base_url} ...")
print("[1/7] Security headers..."); self.check_security_headers()
print("[2/7] SSL/TLS..."); self.check_ssl()
print("[3/7] Common endpoints..."); self.check_common_paths()
print("[4/7] CORS..."); self.check_cors()
print("[5/7] Cookies..."); self.check_cookies()
print("[6/7] Error handling..."); self.check_verbose_errors()
s = self.report["summary"]
print(f"[7/7] Done. Found: {s['critical']} critical, {s['high']} high, {s['medium']} medium, {s['low']} low, {s['info']} info")
self._generate_pdf()
print(f"[+] Report saved: pentest-report-{self.domain}.pdf")
def _generate_pdf(self):
pdf = FPDF()
pdf.add_page()
pdf.set_font("Helvetica", "B", 22)
pdf.cell(0, 14, "Web Application Security Assessment", align="C", new_x="LMARGIN", new_y="NEXT")
pdf.set_font("Helvetica", "", 11)
pdf.cell(0, 8, f"Target: {self.base_url}", align="C", new_x="LMARGIN", new_y="NEXT")
pdf.cell(0, 7, f"Date: {self.report['scanned_at'][:10]}", align="C", new_x="LMARGIN", new_y="NEXT")
pdf.ln(8)
pdf.set_font("Helvetica", "B", 14)
pdf.cell(0, 10, "Executive Summary", new_x="LMARGIN", new_y="NEXT")
pdf.set_font("Helvetica", "", 10)
s = self.report["summary"]
total = sum(s.values())
pdf.multi_cell(0, 6,
f"This report summarizes the automated security scan of {self.base_url}. "
f"A total of {total} findings were identified. "
f"Of these, {s['critical'] + s['high']} are high-severity issues requiring immediate attention, "
f"{s['medium']} are medium-severity, and {s['low']} are low-severity. "
"A full manual penetration test is recommended to identify business logic, "
"authentication, and authorization vulnerabilities that automated scanners miss.")
pdf.ln(4)
pdf.set_font("Helvetica", "B", 12)
pdf.cell(0, 8, "Finding Summary", new_x="LMARGIN", new_y="NEXT")
pdf.set_font("Helvetica", "B", 10)
pdf.cell(30, 7, "Severity", border=1, align="C")
pdf.cell(30, 7, "Count", border=1, align="C", new_x="LMARGIN", new_y="NEXT")
for sev in ["critical", "high", "medium", "low", "info"]:
pdf.set_font("Helvetica", "", 10)
pdf.cell(30, 7, sev.capitalize(), border=1)
pdf.cell(30, 7, str(s[sev]), border=1, align="C", new_x="LMARGIN", new_y="NEXT")
pdf.ln(6)
pdf.set_font("Helvetica", "B", 14)
pdf.cell(0, 10, "Detailed Findings", new_x="LMARGIN", new_y="NEXT")
for i, f in enumerate(self.report["findings"], 1):
pdf.set_font("Helvetica", "B", 11)
pdf.set_text_color(200, 0, 0) if f["severity"] in ("critical", "high") else pdf.set_text_color(0, 0, 0)
pdf.multi_cell(0, 6, f"#{i} [{f['severity'].upper()}] {f['title']}")
pdf.set_text_color(0, 0, 0)
pdf.set_font("Helvetica", "", 10)
pdf.cell(0, 5, f"Description: {f['description']}", new_x="LMARGIN", new_y="NEXT")
if f["evidence"]:
pdf.set_font("Courier", "", 8)
pdf.multi_cell(0, 5, f"Evidence: {f['evidence']}")
pdf.set_font("Helvetica", "", 10)
pdf.cell(0, 5, f"Remediation: {f['remediation']}", new_x="LMARGIN", new_y="NEXT")
pdf.ln(3)
pdf.set_font("Helvetica", "B", 14)
pdf.add_page()
pdf.cell(0, 10, "Recommendations", new_x="LMARGIN", new_y="NEXT")
pdf.set_font("Helvetica", "", 10)
if not self.report["findings"]:
pdf.multi_cell(0, 6, "No significant issues detected by the automated scan. A manual pentest is still recommended.")
else:
pdf.multi_cell(0, 6,
"1. Fix all Critical and High severity findings immediately.\n"
"2. Schedule a follow-up manual penetration test for business logic and authorization flaws.\n"
"3. Implement a Content Security Policy to mitigate XSS risk.\n"
"4. Enforce HTTPS with HSTS across all subdomains.\n"
"5. Restrict access to administrative and debug endpoints.\n"
"6. Conduct regular automated scanning (monthly) and manual pentesting (quarterly or before major releases).")
pdf.output(f"pentest-report-{self.domain}.pdf")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python web_pentest_quick.py https://target.com")
sys.exit(1)
QuickPentest(sys.argv[1]).run()