new-site/scripts/recon_gckey.py
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

261 lines
12 KiB
Python

#!/usr/bin/env python3
"""GCKey signup flow reconnaissance — read-only observation.
Navigates the GCKey signup process via Playwright, captures screenshots
and form field details at each step. Does NOT submit any forms.
"""
import asyncio
import json
import os
import io
import sys
from playwright.async_api import async_playwright
async def dump_page(page, label):
"""Capture current page state and upload screenshot."""
print(f"\n{'='*60}")
print(f" STEP: {label}")
print(f" URL: {page.url}")
print(f" Title: {await page.title()}")
# Get form inputs
els = await page.evaluate("""() => {
const r = [];
document.querySelectorAll('input, select, textarea').forEach(el => {
r.push({
tag: el.tagName,
name: el.name || '',
id: el.id || '',
type: el.type || '',
ph: el.placeholder || '',
req: el.required,
ml: el.maxLength > 0 ? el.maxLength : 0,
vis: el.offsetParent !== null,
});
});
return r;
}""")
visible_inputs = [e for e in els if e.get('vis') or e.get('type') == 'hidden']
if visible_inputs:
print(f" Inputs ({len(visible_inputs)}):")
for e in visible_inputs:
hid = " [HIDDEN]" if e['type'] == 'hidden' else ""
print(f" <{e['tag']}> name={e['name']} id={e['id']} type={e['type']}"
f" placeholder='{e['ph']}' required={e['req']} maxlen={e['ml']}{hid}")
# Get buttons
btns = await page.evaluate("""() => {
const r = [];
document.querySelectorAll('button, input[type=submit], input[type=button]').forEach(el => {
r.push({
tag: el.tagName,
val: el.value || '',
txt: (el.textContent || '').trim().substring(0, 60),
id: el.id || '',
name: el.name || '',
});
});
return r;
}""")
if btns:
print(f" Buttons ({len(btns)}):")
for b in btns:
print(f" <{b['tag']}> name={b['name']} id={b['id']}"
f" value='{b['val'][:40]}' text='{b['txt'][:40]}'")
# Get links with auth keywords
links = await page.evaluate("""() => {
const keywords = ['sign', 'register', 'create', 'gckey', 'enroll', 'new user', 'account'];
return Array.from(document.querySelectorAll('a')).map(a => ({
t: (a.textContent || '').trim().substring(0, 80),
h: a.href || '',
})).filter(a => {
const lower = (a.t + ' ' + a.h).toLowerCase();
return keywords.some(k => lower.includes(k));
});
}""")
if links:
print(f" Auth-related links ({len(links)}):")
for l in links:
print(f" '{l['t'][:60]}' -> {l['h'][:100]}")
# Check for CAPTCHA
captcha = await page.evaluate("""() => {
return {
recaptcha: !!document.querySelector('iframe[src*=recaptcha], .g-recaptcha, #recaptcha'),
hcaptcha: !!document.querySelector('iframe[src*=hcaptcha], .h-captcha'),
imgcaptcha: !!document.querySelector('img[alt*=captcha i], img[src*=captcha i], img[id*=captcha i]'),
iframes: Array.from(document.querySelectorAll('iframe')).map(f => f.src).filter(s => s),
};
}""")
print(f" CAPTCHA: recaptcha={captcha['recaptcha']} hcaptcha={captcha['hcaptcha']} img={captcha['imgcaptcha']}")
if captcha['iframes']:
print(f" Iframes: {captcha['iframes']}")
# Screenshot — save locally, skip MinIO for recon
ss = await page.screenshot(type="png")
fname = f"/tmp/gckey_{label.replace(' ', '_').lower()}.png"
with open(fname, "wb") as f:
f.write(ss)
print(f" Screenshot saved: {fname} ({len(ss)} bytes)")
return ss
async def recon():
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
args=["--ignore-certificate-errors", "--no-sandbox", "--disable-dev-shm-usage"],
)
ctx = await browser.new_context(
viewport={"width": 1280, "height": 900},
locale="en-CA",
ignore_https_errors=True,
)
page = await ctx.new_page()
try:
# ── Step 1: CRTC SAML → GCKey login page ──────────────────
print("Step 1: CRTC SmartForms → GACS → GCKey login")
await page.goto(
"https://services.crtc.gc.ca/Pro/SmartForms/?_gc_lang=eng",
wait_until="domcontentloaded",
timeout=30000,
)
await asyncio.sleep(2)
# Click "GCKey Log In" to get through GACS to GCKey
gckey_login = await page.query_selector("a:has-text('GCKey Log In')")
if gckey_login:
await gckey_login.click()
try:
await page.wait_for_load_state("domcontentloaded", timeout=30000)
except Exception:
pass
await asyncio.sleep(5)
await dump_page(page, "01_gckey_login")
# ── Step 2: Navigate directly to the Sign Up page ──────────
# Extract ReqID from current URL
import re
req_match = re.search(r'ReqID=([A-Z0-9]+)', page.url)
if req_match:
req_id = req_match.group(1)
signup_url = f"https://clegc-gckey.gc.ca/j/eng/rg?ReqID={req_id}"
print(f"\nStep 2: Navigating to signup: {signup_url}")
await page.goto(signup_url, wait_until="domcontentloaded", timeout=20000)
await asyncio.sleep(3)
await dump_page(page, "02_gckey_signup_terms")
# ── Step 3: Accept terms ───────────────────────────────
accept = await page.query_selector(
"input[type=submit][value*='Accept'], "
"input[type=submit][value*='accept'], "
"button:has-text('Accept'), "
"button:has-text('I Accept'), "
"input[type=submit][value*='I Accept']"
)
if accept:
val = await accept.get_attribute("value") or ""
print(f"\nStep 3: Accepting terms ('{val}')...")
await accept.click()
try:
await page.wait_for_load_state("domcontentloaded", timeout=20000)
except Exception:
pass
await asyncio.sleep(3)
await dump_page(page, "03_gckey_create_username")
# ── Step 4: Capture username creation page ─────────
# Fill a dummy username to see what the next page looks like
# (we won't submit — just fill and capture)
username_field = await page.query_selector("input[name*='user'], input[name*='token'], input[id*='user']")
if username_field:
print("\nStep 4: Found username field — filling dummy value...")
await username_field.fill("pw-recon-test-12345")
# Find the submit/continue button
cont = await page.query_selector(
"input[type=submit]:not([value*='Cancel']):not([value*='Exit']), "
"button[type=submit]:not(:has-text('Cancel'))"
)
if cont:
val = await cont.get_attribute("value") or await cont.inner_text()
print(f" Continue button: '{val}' — clicking to see password page...")
await cont.click()
try:
await page.wait_for_load_state("domcontentloaded", timeout=20000)
except Exception:
pass
await asyncio.sleep(3)
await dump_page(page, "04_gckey_create_password")
# ── Step 5: Capture password page ─────────
pwd_field = await page.query_selector("input[type=password]")
if pwd_field:
print("\nStep 5: Found password field — filling dummy...")
await pwd_field.fill("Pw$Recon2026!x9")
# Check for confirm password
pwd_confirm = await page.evaluate("""() => {
const pwds = document.querySelectorAll('input[type=password]');
return pwds.length;
}""")
print(f" Password fields count: {pwd_confirm}")
if pwd_confirm >= 2:
fields = await page.query_selector_all("input[type=password]")
await fields[1].fill("Pw$Recon2026!x9")
cont2 = await page.query_selector(
"input[type=submit]:not([value*='Cancel']):not([value*='Exit']), "
"button[type=submit]:not(:has-text('Cancel'))"
)
if cont2:
val = await cont2.get_attribute("value") or await cont2.inner_text()
print(f" Continue button: '{val}' — clicking to see security questions...")
await cont2.click()
try:
await page.wait_for_load_state("domcontentloaded", timeout=20000)
except Exception:
pass
await asyncio.sleep(3)
await dump_page(page, "05_gckey_security_questions")
# ── Step 6: Capture security questions ─
# Check for select elements (question dropdowns)
selects = await page.query_selector_all("select")
if selects:
print(f"\nStep 6: Found {len(selects)} select dropdowns")
for i, sel in enumerate(selects):
options = await sel.evaluate("""el =>
Array.from(el.options).map(o => ({v: o.value, t: o.text}))
""")
print(f" Select {i}: {len(options)} options")
for opt in options[:10]:
print(f" '{opt['t'][:60]}' (value={opt['v']})")
if len(options) > 10:
print(f" ... and {len(options)-10} more")
else:
print("\n No accept/terms button found — page might be different")
else:
print(" Could not extract ReqID from URL")
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()
await dump_page(page, "error_state")
finally:
await browser.close()
print(f"\n{'='*60}")
print("RECON COMPLETE")
print("Screenshots uploaded to minio://performancewest/recon/gckey/")
print("="*60)
if __name__ == "__main__":
asyncio.run(recon())