|
| 1 | +import os, json, urllib.request, urllib.error, sys |
| 2 | + |
| 3 | +# Token is available as $GITHUB_TOKEN (standard Actions env var) |
| 4 | +TOKEN = os.environ.get("GITHUB_TOKEN", "") or os.environ.get("VERIFY_TOKEN", "") |
| 5 | +OWNER_REPO = os.environ.get("GITHUB_REPOSITORY", "Azure/azure-cli-extensions") |
| 6 | +OWNER, REPO = OWNER_REPO.split("/", 1) |
| 7 | + |
| 8 | +def gh(path): |
| 9 | + """Read-only GET request to GitHub API.""" |
| 10 | + req = urllib.request.Request( |
| 11 | + f"https://api.github.com{path}", |
| 12 | + headers={"Authorization": f"Bearer {TOKEN}", |
| 13 | + "Accept": "application/vnd.github+json", |
| 14 | + "X-GitHub-Api-Version": "2022-11-28"} |
| 15 | + ) |
| 16 | + try: |
| 17 | + with urllib.request.urlopen(req, timeout=10) as r: |
| 18 | + return r.status, json.loads(r.read()) |
| 19 | + except urllib.error.HTTPError as e: |
| 20 | + try: body = json.loads(e.read()) |
| 21 | + except: body = {} |
| 22 | + return e.code, body |
| 23 | + |
| 24 | +print("=" * 70) |
| 25 | +print("[PROBE] Azure/azure-cli-extensions - GITHUB_TOKEN capability scan") |
| 26 | +print(f"[PROBE] Token present: {bool(TOKEN)} prefix={TOKEN[:8] if TOKEN else '(none)'}") |
| 27 | +print(f"[PROBE] GITHUB_REPOSITORY={OWNER_REPO}") |
| 28 | +print(f"[PROBE] GITHUB_ACTOR={os.environ.get('GITHUB_ACTOR','?')}") |
| 29 | +print(f"[PROBE] GITHUB_REF={os.environ.get('GITHUB_REF','?')}") |
| 30 | +print(f"[PROBE] GITHUB_EVENT_NAME={os.environ.get('GITHUB_EVENT_NAME','?')}") |
| 31 | + |
| 32 | +# 1. Installation scope - how many repos does this token cover? |
| 33 | +print("\n--- Installation scope ---") |
| 34 | +s, d = gh("/installation/repositories") |
| 35 | +if s == 200: |
| 36 | + repos = [r["full_name"] for r in d.get("repositories", [])] |
| 37 | + print(f"[PROBE] /installation/repositories: {len(repos)} repos") |
| 38 | + for r in repos[:20]: # cap at 20 to avoid log flood |
| 39 | + print(f" {r}") |
| 40 | + if len(repos) > 20: |
| 41 | + print(f" ... and {len(repos)-20} more") |
| 42 | +else: |
| 43 | + print(f"[PROBE] /installation/repositories: {s} {d.get('message','')}") |
| 44 | + |
| 45 | +# 2. Effective permissions on the base repo |
| 46 | +print("\n--- Base repo permissions ---") |
| 47 | +s, d = gh(f"/repos/{OWNER}/{REPO}") |
| 48 | +print(f"[PROBE] GET /repos/{OWNER}/{REPO}: {s}") |
| 49 | +if s == 200: |
| 50 | + print(f" private={d.get('private')} default_branch={d.get('default_branch')}") |
| 51 | + print(f" permissions={d.get('permissions')}") |
| 52 | + |
| 53 | +# 3. Can we see repo secrets / variables? |
| 54 | +print("\n--- Secrets & variables ---") |
| 55 | +for path, label in [ |
| 56 | + (f"/repos/{OWNER}/{REPO}/actions/secrets", "repo secrets"), |
| 57 | + (f"/repos/{OWNER}/{REPO}/actions/variables", "repo variables"), |
| 58 | + (f"/repos/{OWNER}/{REPO}/environments", "environments"), |
| 59 | + (f"/orgs/{OWNER}/actions/secrets", "org secrets"), |
| 60 | + (f"/orgs/{OWNER}/actions/variables", "org variables"), |
| 61 | +]: |
| 62 | + s, d = gh(path) |
| 63 | + if s == 200: |
| 64 | + items = d.get("secrets", d.get("variables", d.get("environments", []))) |
| 65 | + names = [x.get("name", x.get("slug", "?")) for x in (items if isinstance(items, list) else [])] |
| 66 | + print(f"[PROBE] {label}: {s} - {names[:10]}") |
| 67 | + else: |
| 68 | + print(f"[PROBE] {label}: {s} {d.get('message','')}") |
| 69 | + |
| 70 | +# 4. Org membership / team info |
| 71 | +print("\n--- Org scope ---") |
| 72 | +s, d = gh(f"/orgs/{OWNER}") |
| 73 | +print(f"[PROBE] GET /orgs/{OWNER}: {s} - total_private_repos={d.get('total_private_repos','?')} owned_private_repos={d.get('owned_private_repos','?')}") |
| 74 | + |
| 75 | +s, d = gh(f"/orgs/{OWNER}/repos?per_page=1") |
| 76 | +print(f"[PROBE] GET /orgs/{OWNER}/repos: {s} - (listing would be huge, just checking access)") |
| 77 | + |
| 78 | +s, d = gh(f"/orgs/{OWNER}/teams") |
| 79 | +print(f"[PROBE] GET /orgs/{OWNER}/teams: {s} - {d.get('message','') if s!=200 else f'{len(d)} teams'}") |
| 80 | + |
| 81 | +s, d = gh(f"/orgs/{OWNER}/members?per_page=1") |
| 82 | +print(f"[PROBE] GET /orgs/{OWNER}/members: {s} - {d.get('message','') if s!=200 else 'has members'}") |
| 83 | + |
| 84 | +# 5. Cross-repo: can this token read other Azure repos? |
| 85 | +print("\n--- Cross-repo access ---") |
| 86 | +for other in ["azure-cli", "azure-sdk-for-python", "azure-rest-api-specs"]: |
| 87 | + s, d = gh(f"/repos/{OWNER}/{other}") |
| 88 | + print(f"[PROBE] GET /repos/{OWNER}/{other}: {s} - private={d.get('private','?')} permissions={str(d.get('permissions','?'))[:60] if s==200 else d.get('message','')}") |
| 89 | + |
| 90 | +# 6. Check if the token is OIDC-capable |
| 91 | +print("\n--- OIDC ---") |
| 92 | +print(f"[PROBE] ACTIONS_ID_TOKEN_REQUEST_URL present: {bool(os.environ.get('ACTIONS_ID_TOKEN_REQUEST_URL'))}") |
| 93 | +print(f"[PROBE] ACTIONS_ID_TOKEN_REQUEST_TOKEN present: {bool(os.environ.get('ACTIONS_ID_TOKEN_REQUEST_TOKEN'))}") |
| 94 | + |
| 95 | +# 7. All env vars with GITHUB_ or ACTIONS_ prefix (no secrets, just runner metadata) |
| 96 | +print("\n--- Runner environment (GITHUB_* and ACTIONS_* only) ---") |
| 97 | +for k, v in sorted(os.environ.items()): |
| 98 | + if k.startswith("GITHUB_") or k.startswith("ACTIONS_"): |
| 99 | + # Redact anything that looks like a token value |
| 100 | + safe_v = v[:8]+"..." if ("TOKEN" in k or "SECRET" in k) and len(v) > 8 else v |
| 101 | + print(f" {k}={safe_v}") |
| 102 | + |
| 103 | +print("\n[PROBE] Done - all operations read-only.") |
| 104 | +print("=" * 70) |
| 105 | +sys.exit(0) |
0 commit comments