#!/usr/bin/env python3
# For authorized penetration testing and lab environments only
"""Forced Browsing Authentication Bypass Agent - Tests for unprotected endpoints."""

import json
import logging
import argparse
from datetime import datetime
from urllib.parse import urljoin

import requests

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

DEFAULT_ADMIN_PATHS = [
    "/admin", "/administrator", "/admin-panel", "/wp-admin", "/cpanel",
    "/phpmyadmin", "/adminer", "/manager", "/console", "/debug",
    "/actuator", "/actuator/env", "/actuator/health", "/actuator/beans",
    "/swagger-ui", "/swagger-ui.html", "/api-docs", "/graphql", "/graphiql",
    "/.env", "/server-status", "/server-info", "/.git/HEAD", "/.git/config",
    "/web.config", "/phpinfo.php", "/robots.txt", "/sitemap.xml",
]

SENSITIVE_EXTENSIONS = [
    ".bak", ".old", ".orig", ".save", ".swp", ".tmp", ".config",
    ".sql", ".gz", ".tar", ".zip", ".env",
]


def load_wordlist(wordlist_path):
    """Load directory/file wordlist from file."""
    with open(wordlist_path, "r") as f:
        return [line.strip() for line in f if line.strip() and not line.startswith("#")]


def test_endpoint(base_url, path, session_cookie=None, timeout=10):
    """Test a single endpoint with and without authentication."""
    url = urljoin(base_url, path)
    unauth_resp = requests.get(url, timeout=timeout, allow_redirects=False, verify=False)

    auth_resp = None
    if session_cookie:
        auth_resp = requests.get(
            url, cookies={"session": session_cookie},
            timeout=timeout, allow_redirects=False, verify=False,
        )

    result = {
        "path": path,
        "url": url,
        "unauth_status": unauth_resp.status_code,
        "unauth_size": len(unauth_resp.content),
    }
    if auth_resp:
        result["auth_status"] = auth_resp.status_code
        result["auth_size"] = len(auth_resp.content)
        result["auth_bypass"] = (
            unauth_resp.status_code == 200 and auth_resp.status_code == 200
            and abs(result["unauth_size"] - result["auth_size"]) < 100
        )
    return result


def enumerate_directories(base_url, wordlist, session_cookie=None):
    """Enumerate directories and test authentication enforcement."""
    findings = []
    for word in wordlist:
        path = f"/{word}" if not word.startswith("/") else word
        try:
            result = test_endpoint(base_url, path, session_cookie)
            if result["unauth_status"] in (200, 301, 302, 403):
                findings.append(result)
                logger.info(
                    "Found: %s (status: %d, size: %d)",
                    path, result["unauth_status"], result["unauth_size"],
                )
        except requests.RequestException:
            continue
    return findings


def test_http_method_bypass(base_url, path):
    """Test HTTP method-based authentication bypass."""
    methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
    results = {}
    for method in methods:
        try:
            resp = requests.request(method, urljoin(base_url, path), timeout=10, verify=False)
            results[method] = resp.status_code
        except requests.RequestException:
            results[method] = None
    logger.info("Method bypass test for %s: %s", path, results)
    return results


def test_path_traversal_bypass(base_url, path):
    """Test path normalization bypass techniques."""
    variants = [
        path,
        path.upper(),
        path.replace("/", "/./"),
        f"/public/..{path}",
        path.replace("/", "%2f"),
        f"/;{path}",
        f"/.;{path}",
        f"{path}/",
        f"{path}.json",
    ]
    results = []
    for variant in variants:
        try:
            resp = requests.get(urljoin(base_url, variant), timeout=10, verify=False)
            results.append({"path": variant, "status": resp.status_code, "size": len(resp.content)})
        except requests.RequestException:
            continue
    return results


def check_sensitive_files(base_url):
    """Check for exposed backup and configuration files."""
    sensitive_paths = [
        ".env", ".git/HEAD", ".git/config", "web.config", "wp-config.php.bak",
        "config.php.old", ".htpasswd", "database.yml", "phpinfo.php",
    ]
    exposed = []
    for path in sensitive_paths:
        try:
            resp = requests.get(urljoin(base_url, path), timeout=10, verify=False)
            if resp.status_code == 200 and len(resp.content) > 0:
                exposed.append({"path": path, "status": resp.status_code, "size": len(resp.content)})
                logger.warning("EXPOSED: %s (size: %d bytes)", path, len(resp.content))
        except requests.RequestException:
            continue
    return exposed


def generate_report(findings, method_results, sensitive_files):
    """Generate pentest finding report for forced browsing results."""
    report = {
        "timestamp": datetime.utcnow().isoformat(),
        "total_endpoints_found": len(findings),
        "auth_bypass_candidates": [f for f in findings if f.get("auth_bypass")],
        "accessible_without_auth": [f for f in findings if f["unauth_status"] == 200],
        "http_method_bypass": method_results,
        "sensitive_files_exposed": sensitive_files,
    }
    bypasses = len(report["auth_bypass_candidates"])
    logger.info("Report: %d endpoints, %d auth bypasses, %d sensitive files",
                len(findings), bypasses, len(sensitive_files))
    return report


def main():
    parser = argparse.ArgumentParser(description="Forced Browsing Authentication Bypass Agent")
    parser.add_argument("--target", required=True, help="Target base URL")
    parser.add_argument("--wordlist", help="Path to wordlist file")
    parser.add_argument("--session-cookie", help="Valid session cookie for auth comparison")
    parser.add_argument("--admin-paths", action="store_true", help="Test common admin paths")
    parser.add_argument("--output", default="forced_browsing_report.json")
    args = parser.parse_args()

    wordlist = DEFAULT_ADMIN_PATHS if args.admin_paths else []
    if args.wordlist:
        wordlist = load_wordlist(args.wordlist)

    findings = enumerate_directories(args.target, wordlist, args.session_cookie)
    method_results = {}
    for f in findings:
        if f["unauth_status"] in (401, 403):
            method_results[f["path"]] = test_http_method_bypass(args.target, f["path"])

    sensitive = check_sensitive_files(args.target)
    report = generate_report(findings, method_results, sensitive)

    with open(args.output, "w") as f:
        json.dump(report, f, indent=2)
    logger.info("Report saved to %s", args.output)


if __name__ == "__main__":
    main()
