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>
261 lines
12 KiB
Python
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())
|