#!/usr/bin/env python3
# For authorized testing only
"""API inventory and discovery agent for attack surface mapping."""

import json
import sys
import argparse
import re
import subprocess
from datetime import datetime

try:
    import requests
except ImportError:
    print("Install: pip install requests")
    sys.exit(1)


COMMON_API_PATHS = [
    "/api", "/api/v1", "/api/v2", "/api/v3",
    "/graphql", "/graphiql", "/playground",
    "/swagger.json", "/swagger/v1/swagger.json",
    "/openapi.json", "/api-docs", "/docs",
    "/health", "/healthz", "/status", "/metrics",
    "/admin/api", "/internal/api", "/.well-known/openid-configuration",
    "/v1", "/v2", "/rest", "/ws", "/rpc",
]


def discover_api_endpoints(base_url, paths=None, timeout=5):
    """Probe common API paths to discover active endpoints."""
    if paths is None:
        paths = COMMON_API_PATHS
    discovered = []
    for path in paths:
        url = f"{base_url.rstrip('/')}{path}"
        try:
            resp = requests.get(url, timeout=timeout, allow_redirects=False,
                                verify=True, headers={"User-Agent": "API-Inventory-Agent/1.0"})
            if resp.status_code < 500:
                entry = {
                    "url": url,
                    "status": resp.status_code,
                    "content_type": resp.headers.get("Content-Type", ""),
                    "server": resp.headers.get("Server", ""),
                }
                if "json" in entry["content_type"]:
                    entry["type"] = "REST/JSON"
                elif "xml" in entry["content_type"]:
                    entry["type"] = "SOAP/XML"
                elif "html" in entry["content_type"] and "swagger" in path.lower():
                    entry["type"] = "API Documentation"
                else:
                    entry["type"] = "unknown"
                if resp.status_code == 200:
                    entry["finding"] = "Active API endpoint"
                    entry["severity"] = "INFO"
                discovered.append(entry)
        except requests.exceptions.RequestException:
            pass
    return discovered


def parse_swagger_spec(spec_url):
    """Fetch and parse OpenAPI/Swagger spec to inventory endpoints."""
    try:
        resp = requests.get(spec_url, timeout=15)
        resp.raise_for_status()
        spec = resp.json()
    except Exception as e:
        return {"error": str(e)}

    version = spec.get("openapi", spec.get("swagger", "unknown"))
    info = spec.get("info", {})
    paths = spec.get("paths", {})
    endpoints = []
    for path, methods in paths.items():
        for method in methods:
            if method.upper() in ("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"):
                op = methods[method]
                endpoints.append({
                    "method": method.upper(),
                    "path": path,
                    "summary": op.get("summary", ""),
                    "deprecated": op.get("deprecated", False),
                    "auth_required": bool(op.get("security", spec.get("security", []))),
                })
    deprecated = [e for e in endpoints if e["deprecated"]]
    return {
        "spec_version": version,
        "api_title": info.get("title", ""),
        "api_version": info.get("version", ""),
        "total_endpoints": len(endpoints),
        "deprecated_endpoints": len(deprecated),
        "endpoints": endpoints,
    }


def scan_javascript_for_apis(js_url):
    """Fetch JavaScript file and extract API endpoint references."""
    try:
        resp = requests.get(js_url, timeout=15)
        content = resp.text
    except Exception as e:
        return {"error": str(e)}

    api_patterns = [
        re.compile(r'["\'](/api/[^"\']+)["\']'),
        re.compile(r'["\'](/v\d+/[^"\']+)["\']'),
        re.compile(r'fetch\s*\(\s*["\']([^"\']+)["\']'),
        re.compile(r'axios\.\w+\s*\(\s*["\']([^"\']+)["\']'),
        re.compile(r'\.get\s*\(\s*["\']([^"\']+/api[^"\']*)["\']'),
        re.compile(r'\.post\s*\(\s*["\']([^"\']+/api[^"\']*)["\']'),
    ]
    found_apis = set()
    for pattern in api_patterns:
        for match in pattern.findall(content):
            if len(match) > 3 and not match.endswith((".js", ".css", ".png", ".jpg")):
                found_apis.add(match)

    return {"source": js_url, "discovered_apis": sorted(found_apis), "count": len(found_apis)}


def enumerate_subdomains_for_apis(domain):
    """Use DNS enumeration to find API subdomains."""
    api_prefixes = [
        "api", "api-v1", "api-v2", "api-gateway", "api-internal",
        "gateway", "graphql", "rest", "ws", "webhook",
        "staging-api", "dev-api", "sandbox-api", "beta-api",
        "admin-api", "partner-api", "public-api", "mobile-api",
    ]
    found = []
    for prefix in api_prefixes:
        subdomain = f"{prefix}.{domain}"
        try:
            result = subprocess.run(
                ["nslookup", subdomain], capture_output=True, text=True, timeout=5
            )
            if "Non-authoritative answer" in result.stdout or "Address:" in result.stdout:
                found.append({
                    "subdomain": subdomain,
                    "status": "resolved",
                    "severity": "MEDIUM" if "internal" in prefix or "staging" in prefix else "INFO",
                })
        except (subprocess.TimeoutExpired, FileNotFoundError):
            pass
    return found


def classify_api_risk(endpoints):
    """Classify discovered APIs by risk level."""
    findings = []
    for ep in endpoints:
        url = ep.get("url", ep.get("path", ""))
        risk = "LOW"
        reason = "Standard endpoint"
        if any(p in url.lower() for p in ["/admin", "/internal", "/debug", "/metrics"]):
            risk = "HIGH"
            reason = "Administrative/internal endpoint exposed"
        elif any(p in url.lower() for p in ["/graphql", "/graphiql", "/playground"]):
            risk = "HIGH"
            reason = "GraphQL endpoint — check introspection"
        elif "swagger" in url.lower() or "api-docs" in url.lower():
            risk = "MEDIUM"
            reason = "API documentation publicly accessible"
        elif ep.get("deprecated", False):
            risk = "HIGH"
            reason = "Deprecated/zombie API still accessible"
        findings.append({**ep, "risk": risk, "reason": reason})
    return findings


def run_audit(args):
    """Execute API inventory and discovery audit."""
    print(f"\n{'='*60}")
    print(f"  API INVENTORY AND DISCOVERY AUDIT")
    print(f"  Generated: {datetime.utcnow().isoformat()} UTC")
    print(f"{'='*60}\n")

    report = {}

    if args.target_url:
        discovered = discover_api_endpoints(args.target_url)
        classified = classify_api_risk(discovered)
        report["discovered_endpoints"] = classified
        print(f"--- ENDPOINT DISCOVERY ({len(classified)} found) ---")
        for ep in classified:
            print(f"  [{ep['risk']}] {ep['url']} ({ep.get('status','')}): {ep['reason']}")

    if args.swagger_url:
        spec = parse_swagger_spec(args.swagger_url)
        report["swagger_spec"] = spec
        print(f"\n--- SWAGGER SPEC ANALYSIS ---")
        print(f"  API: {spec.get('api_title','')} v{spec.get('api_version','')}")
        print(f"  Endpoints: {spec.get('total_endpoints',0)}")
        print(f"  Deprecated: {spec.get('deprecated_endpoints',0)}")

    if args.js_url:
        js_apis = scan_javascript_for_apis(args.js_url)
        report["js_api_discovery"] = js_apis
        print(f"\n--- JAVASCRIPT API EXTRACTION ({js_apis.get('count',0)}) ---")
        for api in js_apis.get("discovered_apis", [])[:15]:
            print(f"  {api}")

    if args.domain:
        subs = enumerate_subdomains_for_apis(args.domain)
        report["api_subdomains"] = subs
        print(f"\n--- API SUBDOMAIN ENUMERATION ({len(subs)} found) ---")
        for s in subs:
            print(f"  [{s['severity']}] {s['subdomain']}")

    return report


def main():
    parser = argparse.ArgumentParser(description="API Inventory Discovery Agent")
    parser.add_argument("--target-url", help="Base URL to probe for API endpoints")
    parser.add_argument("--swagger-url", help="Swagger/OpenAPI spec URL to parse")
    parser.add_argument("--js-url", help="JavaScript file URL to extract API paths")
    parser.add_argument("--domain", help="Domain for API subdomain enumeration")
    parser.add_argument("--output", help="Save report to JSON file")
    args = parser.parse_args()

    report = run_audit(args)
    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2, default=str)
        print(f"\n[+] Report saved to {args.output}")


if __name__ == "__main__":
    main()
