#!/usr/bin/env bash # ATHELGARD — CTF CLI. Decode locally. Print the flag or an honest fail. # # source <(curl -fsSL https://athelgard.io/athel.sh) && athel help # curl -fsSL https://athelgard.io/athel.sh | bash # curl -fsSL https://athelgard.io/athel.sh | bash -s -- solve 'decode rot13 synt{grfg}' # # Radio (only when local solve finds nothing): # https://athelgard.io/api/athelgard-desk-inbound # Chrome: Load unpacked athelgard-companion · Alt+A # Hermes hangar: https://athelgard.io/hermes # Obsidian vault + plugin: https://athelgard.io/obsidian ATHELGARD_DESK="${ATHELGARD_DESK:-https://athelgard.io}" ATHELGARD_INBOUND="${ATHELGARD_INBOUND:-${ATHELGARD_DESK%/}/api/athelgard-desk-inbound}" ATHELGARD_VAULT="${ATHELGARD_VAULT:-$HOME/Documents/Obsidian/Athelgard}" ATHELGARD_RC="${ATHELGARD_RC:-$HOME/.athel.sh}" _athel_sourced() { [ "${BASH_SOURCE[0]-}" != "${0:-}" ] } _athel_help() { cat <<'EOF' Athelgard — CTF CLI athel help this card athel solve decode / extract flags (rot13, base64, hex) athel same as solve, then radio if no flag athel hunt hunt the paste athel talk interactive solve prompt athel hermes notes & review brief athel obsidian [open] seed vault + install Obsidian plugin athel log print Obsidian vault path athel discord same solver, optional team channel echo athel browser Kinetigor desk browser · athel browser up launches it athel desk same window — companion already loaded Paste ciphertext. I print the flag or an honest fail. https://athelgard.io EOF } _athel_sanitize() { python3 -c ' import re, sys text = sys.stdin.read() ban = re.compile( r"Kimi\+Droid|HOLMES CTF|HTB EVENT LIVE|GAMEDAY LIVE|HTB_TOKEN|" r"Ready to hunt|Chrome companion:|Sidecar:|Crew browser:|" r"Athelgard — \*\*ran it\*\*|Programs: \*\*huntr\*\*|" r"set HTB_TOKEN|auto-submit|you>|owl that hunts", re.I, ) lines = [ln for ln in text.splitlines() if not ban.search(ln)] out = re.sub(r"\n{3,}", "\n\n", "\n".join(lines)).strip() sys.stdout.write(out + ("\n" if out else "")) ' 2>/dev/null || cat } _athel_desk_js() { local cand here here="$(cd "$(dirname "${BASH_SOURCE[0]-}")" 2>/dev/null && pwd)" || here="" for cand in \ "${ATHELGARD_ROOT:+$ATHELGARD_ROOT/lib/athelgard-ctf-desk.cjs}" \ "${here:+$here/lib/athelgard-ctf-desk.cjs}" \ "$HOME/bountywarz/lib/athelgard-ctf-desk.cjs" \ "./lib/athelgard-ctf-desk.cjs"; do if [ -n "$cand" ] && [ -f "$cand" ]; then printf '%s\n' "$cand" return 0 fi done return 1 } _athel_local_solve() { local msg="${1-}" local js if [ -z "$msg" ]; then return 2 fi if command -v node >/dev/null 2>&1 && js="$(_athel_desk_js)"; then ATHELGARD_DESK_JS="$js" node -e ' const path = require("path"); const desk = require(process.env.ATHELGARD_DESK_JS); const out = desk.solve(process.argv[1], { source: "athel.sh" }); if (out.hasFlag) { process.stdout.write((out.reply || out.flagStrings.join("\n")) + "\n"); process.exit(0); } if (out.useful && out.reply) { process.stdout.write(out.reply + "\n"); process.exit(0); } process.exit(2); ' "$msg" && return 0 fi if ! command -v python3 >/dev/null 2>&1; then return 2 fi python3 -c ' import base64, codecs, re, sys FLAG_RE = re.compile(r"(?i)\b(?:HTB|THM|picoCTF|FLAG|CTF)\{[^{}\s]{1,200}\}") text = sys.argv[1] found = [] seen = set() def add(xs): for f in xs: k = f.lower() if k not in seen: seen.add(k) found.append(f) add(FLAG_RE.findall(text)) add(FLAG_RE.findall(codecs.decode(text, "rot_13"))) for m in re.finditer(r"(?i)\b[A-Za-z]{3,12}\{[^}]{1,200}\}", text): add(FLAG_RE.findall(codecs.decode(m.group(0), "rot_13"))) for tok in re.findall(r"[A-Za-z0-9+/]{8,}={0,2}", text): try: raw = base64.b64decode(tok) s = raw.decode("utf-8", "replace") add(FLAG_RE.findall(s)) add(FLAG_RE.findall(codecs.decode(s, "rot_13"))) except Exception: pass hexes = re.findall(r"(?:0x)?[0-9a-fA-F]{8,}", text) for tok in hexes: h = tok[2:] if tok.lower().startswith("0x") else tok if len(h) % 2: continue try: s = bytes.fromhex(h).decode("utf-8", "replace") add(FLAG_RE.findall(s)) except Exception: pass if found: sys.stdout.write("\n".join(found) + "\n") sys.exit(0) sys.exit(2) ' "$msg" } _athel_ask() { local msg="${1-}" local platform="${2:-bash}" if [ -z "$msg" ]; then printf 'usage: athel solve \n' >&2 return 2 fi if ! command -v python3 >/dev/null 2>&1; then printf 'python3 is required for radio. Local solve already ran.\n' >&2 return 1 fi ATHELGARD_INBOUND="$ATHELGARD_INBOUND" python3 -c ' import json, os, re, sys, urllib.error, urllib.request msg, plat = sys.argv[1], sys.argv[2] url = os.environ.get("ATHELGARD_INBOUND") or "https://athelgard.io/api/athelgard-desk-inbound" body = json.dumps({ "message": msg, "authorName": os.environ.get("ATHELGARD_CALLSIGN") or "Captain", "platform": plat, "auto_reply": plat == "discord", }).encode() req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"}) try: with urllib.request.urlopen(req, timeout=90) as r: data = json.load(r) except urllib.error.HTTPError as e: raw = e.read().decode("utf-8", "replace") try: data = json.loads(raw) except Exception: print("Athelgard radio missed HTTP %s: %s" % (e.code, raw[:240]), file=sys.stderr) sys.exit(1) except Exception as e: print("Athelgard radio missed: %s" % e, file=sys.stderr) sys.exit(1) text = data.get("reply") or data.get("reply_short") or data.get("error") or "" if not text: text = json.dumps(data)[:800] ban = re.compile( r"Kimi\+Droid|HOLMES CTF|HTB EVENT LIVE|GAMEDAY LIVE|HTB_TOKEN|" r"Ready to hunt|Chrome companion:|Sidecar:|Crew browser:|" r"Athelgard — \*\*ran it\*\*|Programs: \*\*huntr\*\*|" r"set HTB_TOKEN|auto-submit", re.I, ) lines = [ln for ln in text.splitlines() if not ban.search(ln)] text = re.sub(r"\n{3,}", "\n\n", "\n".join(lines)).strip() if not text: text = "No format-valid flag. Paste more ciphertext." sys.stdout.write(text if text.endswith("\n") else text + "\n") ' "$msg" "$platform" } _athel_logbook() { local order="$1" local reply="$2" mkdir -p "$ATHELGARD_VAULT" 2>/dev/null || return 0 local note="$ATHELGARD_VAULT/Phone-Log.md" if [ ! -f "$note" ]; then printf '# Athelgard log\n\n' > "$note" || return 0 fi { printf '\n## %s\n\n**Order**\n\n```\n%s\n```\n\n**Reply**\n\n%s\n' \ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ "${order:-'(none)'}" \ "${reply:-'(none)'}" } >> "$note" 2>/dev/null || true } _athel_solve_or_ask() { local msg="$1" local platform="${2:-bash}" local out if [ -z "$msg" ]; then printf 'usage: athel solve \n' >&2 return 2 fi if out="$(_athel_local_solve "$msg")"; then printf '%s\n' "$out" _athel_logbook "$msg" "$out" return 0 fi out="$(_athel_ask "$msg" "$platform")" || return $? printf '%s\n' "$out" _athel_logbook "$msg" "$out" } _athel_hermes() { cat <<'EOF' HERMES HANGAR Notes & review. Athelgard hunts; this lane is logging and code review. Talk: source <(curl -fsSL https://athelgard.io/athel.sh) && athel help Chrome: https://athelgard.io/companion · Alt+A Obsidian: athel obsidian Discord: athel discord Page: https://athelgard.io/hermes Vault: https://athelgard.io/obsidian EOF } _athel_obsidian_copy_plugin() { local dest="$ATHELGARD_VAULT/.obsidian/plugins/athelgard-desk" mkdir -p "$dest" local src="${ATHELGARD_PLUGIN_SRC-}" if [ -z "$src" ]; then if [ -f "./extensions/athelgard-obsidian/manifest.json" ]; then src="./extensions/athelgard-obsidian" elif [ -f "$HOME/bountywarz/extensions/athelgard-obsidian/manifest.json" ]; then src="$HOME/bountywarz/extensions/athelgard-obsidian" fi fi if [ -n "$src" ] && [ -f "$src/manifest.json" ]; then cp "$src/manifest.json" "$src/main.js" "$src/styles.css" "$dest/" 2>/dev/null || true [ -f "$src/data.json" ] && cp "$src/data.json" "$dest/" 2>/dev/null || true return 0 fi local zip="/tmp/athelgard-obsidian.zip" if command -v curl >/dev/null 2>&1 && curl -fsSL "https://athelgard.io/downloads/athelgard-obsidian.zip" -o "$zip" 2>/dev/null; then local unpack="/tmp/athelgard-obsidian-unpack" rm -rf "$unpack" mkdir -p "$unpack" if command -v unzip >/dev/null 2>&1 && unzip -o "$zip" -d "$unpack" >/dev/null 2>&1; then local found found="$(find "$unpack" -name manifest.json -print -quit 2>/dev/null || true)" if [ -n "$found" ]; then cp "$(dirname "$found")"/* "$dest/" 2>/dev/null || true return 0 fi fi fi return 0 } _athel_obsidian() { mkdir -p "$ATHELGARD_VAULT/Talk" "$ATHELGARD_VAULT/Hunt" "$ATHELGARD_VAULT/Flags" if [ ! -f "$ATHELGARD_VAULT/Home.md" ]; then cat > "$ATHELGARD_VAULT/Home.md" <<'EOF' # Athelgard vault Talk from the ribbon or Command palette → **Talk to Athelgard**. - Talk/ — radio turns - Hunt/ — CTF notes - Flags/ — staged flags - Phone-Log.md — append-only log (`athel` CLI writes here too) Radio: `source <(curl -fsSL https://athelgard.io/athel.sh) && athel help` Plugin page: https://athelgard.io/obsidian EOF fi if [ ! -f "$ATHELGARD_VAULT/Phone-Log.md" ]; then printf '# Athelgard log\n\n' > "$ATHELGARD_VAULT/Phone-Log.md" fi _athel_obsidian_copy_plugin mkdir -p "$ATHELGARD_VAULT/.obsidian" if command -v python3 >/dev/null 2>&1; then ATHELGARD_VAULT="$ATHELGARD_VAULT" python3 -c ' import json, os p = os.path.join(os.environ["ATHELGARD_VAULT"], ".obsidian", "community-plugins.json") lst = [] if os.path.exists(p): try: raw = json.load(open(p)) if isinstance(raw, list): lst = raw except Exception: lst = [] if "athelgard-desk" not in lst: lst.append("athelgard-desk") open(p, "w").write(json.dumps(lst, indent=2) + "\n") ' || true fi local vault_name vault_name="$(basename "$ATHELGARD_VAULT")" cat </dev/null 2>&1; then xdg-open "obsidian://open?vault=${vault_name}&file=Home" >/dev/null 2>&1 || true elif command -v open >/dev/null 2>&1; then open "obsidian://open?vault=${vault_name}&file=Home" >/dev/null 2>&1 || true fi fi } athel() { local cmd="${1-}" case "$cmd" in -h|--help|help|"") _athel_help ;; hermes|hangar) _athel_hermes ;; obsidian|vault) shift _athel_obsidian "${1-}" ;; log|logbook) mkdir -p "$ATHELGARD_VAULT" 2>/dev/null || true printf '%s\n' "$ATHELGARD_VAULT/Phone-Log.md" ;; products|apps|use) _athel_help ;; browser|crew|desk) cat <<'EOF' Kinetigor desk / crew browser — Chromium with multiple unpacked extensions. Athelgard companion + sidecar already loaded. curl -fsSL https://kinetigor.com/kin.sh | sh https://kinetigor.com/downloads/kinetigor-desk-browser.zip Stock load: companion + sidecar Extra extensions: drop unpacked folders in ~/Documents/Athelgard/extensions Stay in Chrome / Edge / Comet instead: https://athelgard.io/companion Page: https://athelgard.io/browser · https://kinetigor.com EOF if [ "${2-}" = "up" ] || [ "${ATHELGARD_BROWSER_UP-}" = "1" ]; then if [ -f "./hunt-browser/start.cjs" ]; then node ./hunt-browser/start.cjs || true elif [ -f "$HOME/Kinetigor/desk-browser/kinetigor-desk-browser/hunt-browser/start.cjs" ]; then node "$HOME/Kinetigor/desk-browser/kinetigor-desk-browser/hunt-browser/start.cjs" || true elif [ -f "$HOME/bountywarz/hunt-browser/start.cjs" ]; then node "$HOME/bountywarz/hunt-browser/start.cjs" || true elif [ -f "$HOME/bountywarz/tools/kinetigor-browser-up.cjs" ]; then (cd "$HOME/bountywarz" && node tools/kinetigor-browser-up.cjs) || true elif [ -f "./tools/kinetigor-browser-up.cjs" ]; then node ./tools/kinetigor-browser-up.cjs || true else curl -fsSL https://kinetigor.com/kin.sh | sh || true fi fi ;; discord) shift _athel_solve_or_ask "$*" discord ;; hunt) shift if [ -z "${1-}" ]; then printf 'Paste the challenge text, a URL, or ciphertext.\nExample: athel solve decode rot13 synt{grfg}\n' return 0 fi _athel_solve_or_ask "$*" bash ;; solve|decode|flag) shift _athel_solve_or_ask "$*" bash ;; talk) printf 'Athelgard — paste ciphertext. help · quit\n' local line while IFS= read -r -e -p 'athel> ' line; do case "$line" in "" ) continue ;; quit|exit|q) break ;; help|-h|--help) _athel_help; continue ;; hermes|hangar) _athel_hermes; continue ;; obsidian|vault) _athel_obsidian; continue ;; esac _athel_solve_or_ask "$line" bash done ;; *) _athel_solve_or_ask "$*" bash ;; esac } _athel_install_rc() { umask 077 if [ -f "${BASH_SOURCE[0]-}" ]; then cp "${BASH_SOURCE[0]}" "$ATHELGARD_RC" 2>/dev/null || true fi if [ ! -f "$ATHELGARD_RC" ]; then return 0 fi local line='[ -f "$HOME/.athel.sh" ] && . "$HOME/.athel.sh"' for rc in "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.zshrc"; do [ -f "$rc" ] || continue grep -q 'HOME/.athel.sh' "$rc" 2>/dev/null && continue printf '\n%s\n' "$line" >> "$rc" || true done } if ! _athel_sourced; then _athel_install_rc if [ "$#" -eq 0 ]; then athel help else athel "$@" fi fi