246 lines
8.3 KiB
Python
Executable File
246 lines
8.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
wevia-renew-pat.py — GitHub PAT auto-renewer via Selenium + Blade persistent Chrome profile.
|
|
|
|
Referenced in v82_tips_tokens (GitHub PAT auto-renew).
|
|
Usage:
|
|
python3 wevia-renew-pat.py [--token-name NAME] [--expiration-days 30]
|
|
|
|
Pattern (doctrine):
|
|
- Uses Chrome persistent profile C:/Users/Yace/AppData/Local/Google/Chrome/User Data
|
|
- Profile is ALREADY logged into GitHub (via Yacineutt session)
|
|
- Generates new PAT via GitHub UI (github.com/settings/tokens)
|
|
- Writes result to /var/www/html/api/blade-tasks/key_github_token_YYYYMMDD.json
|
|
|
|
Flow:
|
|
1. Launch headless Chrome with persistent profile
|
|
2. Navigate to github.com/settings/tokens/new
|
|
3. Fill form (token name, scopes, expiration)
|
|
4. Submit + extract token from post-creation page
|
|
5. Save to blade-tasks JSON + update secrets.env atomically
|
|
6. Delete/revoke previous token via API
|
|
|
|
Author: WEVIA Master / Opus WIRE (2026-04-18, Session B12)
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import argparse
|
|
import datetime
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
# Default configuration
|
|
DEFAULT_TOKEN_NAME = "weval-auto-renewed"
|
|
DEFAULT_EXPIRATION_DAYS = 30
|
|
DEFAULT_SCOPES = ["repo", "workflow", "read:org"]
|
|
|
|
BLADE_TASKS_DIR = "/var/www/html/api/blade-tasks"
|
|
SECRETS_ENV = "/etc/weval/secrets.env"
|
|
LOG_DIR = "/opt/weval-l99/logs/pat-renew"
|
|
|
|
# Chrome profile path on Blade (documented in v82_tips_cyber)
|
|
# Reminder: this runs via Blade SSH executor, not directly from S204
|
|
BLADE_CHROME_PROFILE = r"C:/Users/Yace/AppData/Local/Google/Chrome/User Data"
|
|
|
|
|
|
def log(msg, level="INFO"):
|
|
"""Simple logger to stdout + log file."""
|
|
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
line = f"[{ts}] [{level}] {msg}"
|
|
print(line)
|
|
os.makedirs(LOG_DIR, exist_ok=True)
|
|
with open(f"{LOG_DIR}/pat-renew-{datetime.date.today().isoformat()}.log", "a") as f:
|
|
f.write(line + "\n")
|
|
|
|
|
|
def check_prereqs():
|
|
"""Verify required directories and environment."""
|
|
issues = []
|
|
if not os.path.isdir(BLADE_TASKS_DIR):
|
|
issues.append(f"Missing {BLADE_TASKS_DIR}")
|
|
if not os.path.isfile(SECRETS_ENV):
|
|
issues.append(f"Missing {SECRETS_ENV}")
|
|
return issues
|
|
|
|
|
|
def create_blade_task(token_name, expiration_days, scopes):
|
|
"""
|
|
Create a blade-task JSON that instructs Blade Chrome to renew the PAT.
|
|
Blade picks up the task, runs Selenium, writes back result.
|
|
"""
|
|
today = datetime.date.today().isoformat().replace("-", "")
|
|
task_id = f"pat_renew_{today}_{os.getpid()}"
|
|
task = {
|
|
"task_id": task_id,
|
|
"type": "github_pat_renew",
|
|
"status": "queued",
|
|
"created_at": datetime.datetime.utcnow().isoformat() + "Z",
|
|
"params": {
|
|
"token_name": token_name,
|
|
"expiration_days": expiration_days,
|
|
"scopes": scopes,
|
|
"target_url": "https://github.com/settings/tokens/new",
|
|
"chrome_profile": BLADE_CHROME_PROFILE,
|
|
},
|
|
"expected_output": {
|
|
"location": f"{BLADE_TASKS_DIR}/key_github_token_{today}.json",
|
|
"schema": {
|
|
"ok": "bool",
|
|
"token": "string (ghp_... or github_pat_...)",
|
|
"created_at": "ISO8601",
|
|
"expires_at": "ISO8601",
|
|
},
|
|
},
|
|
"doctrine_refs": [
|
|
"v82_tips_tokens/GitHub PAT auto-renew",
|
|
"v82_tips_cyber/Chrome persistent profile Yacineutt",
|
|
"doctrine supreme 0/WEVIA Master executor, Opus wires",
|
|
],
|
|
"safety": {
|
|
"zero_manual_powershell": True,
|
|
"zero_fake_data": True,
|
|
"persistent_profile_only": True,
|
|
"revoke_previous_after_success": True,
|
|
},
|
|
}
|
|
|
|
task_path = f"{BLADE_TASKS_DIR}/{task_id}.json"
|
|
with open(task_path, "w") as f:
|
|
json.dump(task, f, indent=2)
|
|
# Ensure www-data ownership (matches other blade-tasks)
|
|
try:
|
|
import pwd
|
|
uid = pwd.getpwnam("www-data").pw_uid
|
|
gid = pwd.getpwnam("www-data").pw_gid
|
|
os.chown(task_path, uid, gid)
|
|
except (KeyError, PermissionError):
|
|
pass
|
|
os.chmod(task_path, 0o644)
|
|
return task_id, task_path
|
|
|
|
|
|
def wait_for_result(task_id, timeout_seconds=300, poll_interval=10):
|
|
"""
|
|
Poll for the result file written by Blade.
|
|
Returns dict with result or None on timeout.
|
|
"""
|
|
today = datetime.date.today().isoformat().replace("-", "")
|
|
result_path = f"{BLADE_TASKS_DIR}/key_github_token_{today}.json"
|
|
import time
|
|
elapsed = 0
|
|
while elapsed < timeout_seconds:
|
|
if os.path.exists(result_path):
|
|
with open(result_path) as f:
|
|
data = json.load(f)
|
|
log(f"Result found after {elapsed}s at {result_path}")
|
|
return data
|
|
time.sleep(poll_interval)
|
|
elapsed += poll_interval
|
|
log(f"Waiting... {elapsed}/{timeout_seconds}s", "DEBUG")
|
|
return None
|
|
|
|
|
|
def update_secrets_env(new_token):
|
|
"""
|
|
Update /etc/weval/secrets.env with new GITHUB_PAT value.
|
|
Atomic write with .bak preservation. Requires root (via docker wrapper when needed).
|
|
"""
|
|
if not os.path.isfile(SECRETS_ENV):
|
|
log(f"secrets.env not writable ({SECRETS_ENV}) — skip update", "WARN")
|
|
return False
|
|
|
|
try:
|
|
with open(SECRETS_ENV) as f:
|
|
lines = f.readlines()
|
|
out = []
|
|
updated = False
|
|
for line in lines:
|
|
if line.strip().startswith("GITHUB_PAT="):
|
|
out.append(f"GITHUB_PAT={new_token}\n")
|
|
updated = True
|
|
else:
|
|
out.append(line)
|
|
if not updated:
|
|
out.append(f"GITHUB_PAT={new_token}\n")
|
|
|
|
# Atomic write
|
|
with tempfile.NamedTemporaryFile(mode="w", delete=False, dir="/tmp") as tmp:
|
|
tmp.writelines(out)
|
|
tmp_path = tmp.name
|
|
# Backup then swap
|
|
bak = f"{SECRETS_ENV}.bak-pat-{datetime.date.today().isoformat()}"
|
|
subprocess.run(["cp", SECRETS_ENV, bak], check=False)
|
|
subprocess.run(["mv", tmp_path, SECRETS_ENV], check=True)
|
|
log(f"secrets.env updated, backup at {bak}")
|
|
return True
|
|
except PermissionError:
|
|
log("Permission denied on secrets.env — needs root/sudo wrapper", "ERROR")
|
|
return False
|
|
except Exception as e:
|
|
log(f"secrets.env update failed: {e}", "ERROR")
|
|
return False
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="GitHub PAT auto-renewer via Blade Selenium")
|
|
parser.add_argument("--token-name", default=DEFAULT_TOKEN_NAME)
|
|
parser.add_argument("--expiration-days", type=int, default=DEFAULT_EXPIRATION_DAYS)
|
|
parser.add_argument("--scopes", nargs="+", default=DEFAULT_SCOPES)
|
|
parser.add_argument("--wait-timeout", type=int, default=300)
|
|
parser.add_argument("--dry-run", action="store_true",
|
|
help="Create task but do not wait / update secrets")
|
|
args = parser.parse_args()
|
|
|
|
log("=" * 60)
|
|
log("WEVIA PAT RENEW START")
|
|
log(f"token_name={args.token_name} expiration_days={args.expiration_days} scopes={args.scopes}")
|
|
|
|
# Prereqs
|
|
issues = check_prereqs()
|
|
if issues:
|
|
for i in issues:
|
|
log(f"PREREQ: {i}", "ERROR")
|
|
sys.exit(1)
|
|
|
|
# Create blade task
|
|
task_id, task_path = create_blade_task(
|
|
args.token_name,
|
|
args.expiration_days,
|
|
args.scopes,
|
|
)
|
|
log(f"Task created: {task_id} at {task_path}")
|
|
|
|
if args.dry_run:
|
|
log("DRY-RUN: task queued, exiting (not waiting for Blade)")
|
|
sys.exit(0)
|
|
|
|
# Wait for Blade result
|
|
result = wait_for_result(task_id, args.wait_timeout)
|
|
if result is None:
|
|
log(f"TIMEOUT after {args.wait_timeout}s - Blade did not produce result", "ERROR")
|
|
sys.exit(2)
|
|
|
|
if not result.get("ok"):
|
|
log(f"Blade reported failure: {result.get('error', 'unknown')}", "ERROR")
|
|
sys.exit(3)
|
|
|
|
new_token = result.get("token")
|
|
if not new_token or not (new_token.startswith("ghp_") or new_token.startswith("github_pat_")):
|
|
log(f"Invalid token format in result: {str(new_token)[:20]}...", "ERROR")
|
|
sys.exit(4)
|
|
|
|
# Update secrets (best effort)
|
|
if update_secrets_env(new_token):
|
|
log("SUCCESS: PAT renewed and secrets.env updated")
|
|
else:
|
|
log("PARTIAL: PAT renewed (see blade-tasks JSON) but secrets.env not updated", "WARN")
|
|
|
|
log("=" * 60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|