#!/usr/bin/env bash # welcomebot-reputation — inspect the pre-signup reputation query API's activity. # # Installed at /usr/local/bin/welcomebot-reputation on admin.yttrx.com. # Source of truth: ~/yttrx-welcomebot/bin/welcomebot-reputation (workstation). # # Reads the persistent `reputation_query` (every answer the API gave) and # `reputation_block` (every block it installed) sqlite tables, so this survives # log rotation — see app/main.py's reputation_check / README's "Reputation # query API". For the raw log lines instead, use `welcomebot-logs --reputation`. set -euo pipefail CONTAINER="${WELCOMEBOT_CONTAINER:-yttrx-welcomebot}" limit=20 mode="queries" filter="all" kind_filter="" since="" usage() { cat <&2; usage >&2; exit 2 ;; esac shift done if ! command -v docker >/dev/null 2>&1; then echo "welcomebot-reputation: docker not found in PATH" >&2; exit 1 fi if ! docker inspect "$CONTAINER" >/dev/null 2>&1; then echo "welcomebot-reputation: container '$CONTAINER' not found (is it deployed/running?)" >&2; exit 1 fi docker exec -i \ -e REP_LIMIT="$limit" \ -e REP_MODE="$mode" \ -e REP_FILTER="$filter" \ -e REP_KIND="$kind_filter" \ -e REP_SINCE="$since" \ "$CONTAINER" python3 <<'PYEOF' import os import sqlite3 conn = sqlite3.connect("/data/welcomed.db") mode = os.environ.get("REP_MODE", "queries") kind = os.environ.get("REP_KIND", "") since = os.environ.get("REP_SINCE", "") limit = os.environ.get("REP_LIMIT", "20") def limited(sql): return sql if limit == "all" else f"{sql} LIMIT {int(limit)}" def since_expr(column): """SQL for the --since bound. A leading +/- means "relative to now" — sqlite needs that spelled datetime('now', '-24 hours'), NOT datetime('-24 hours'), which returns NULL and silently matches no rows. """ if since.startswith(("+", "-")): return f"{column} >= datetime('now', ?)" return f"{column} >= datetime(?)" def tables_exist(): names = {r[0] for r in conn.execute( "SELECT name FROM sqlite_master WHERE type = 'table'")} return {"reputation_query", "reputation_block"} <= names if not tables_exist(): print("(no reputation tables yet — the query API has never been called on " "this deployment)") raise SystemExit(0) if mode == "stats": where, params = [], [] if since: where.append(since_expr("queried_at")) params.append(since) clause = (" WHERE " + " AND ".join(where)) if where else "" row = conn.execute( "SELECT COUNT(*)," " SUM(flagged), SUM(blocked)," " SUM(CASE WHEN classification = 'unknown' THEN 1 ELSE 0 END)," " MIN(queried_at), MAX(queried_at)" f" FROM reputation_query{clause}", params).fetchone() total, flagged, blocked, unknown, first, last = row total = total or 0 if not total: print("(no queries in range)") raise SystemExit(0) def pct(n): return f"{(n or 0) / total * 100:5.1f}%" print(f"queries {total:>7} {first} .. {last}") print(f" flagged {flagged or 0:>7} {pct(flagged)}") print(f" led to a block {blocked or 0:>7} {pct(blocked)}") print(f" unknown {unknown or 0:>7} {pct(unknown)} " "(lookup failed, or refused by the hourly budget)") print() print("by source:") for source, n, nb in conn.execute( "SELECT COALESCE(source, '-'), COUNT(*), SUM(blocked) " f"FROM reputation_query{clause} GROUP BY 1 ORDER BY 2 DESC", params): print(f" {source:24s} {n:>7} queries, {nb or 0:>5} blocked") print() print("blocks installed (still in force):") for kind_, n in conn.execute( "SELECT kind, COUNT(*) FROM reputation_block " "WHERE block_active = 1 AND (expires_at IS NULL OR expires_at > " " strftime('%Y-%m-%dT%H:%M:%S+00:00', 'now')) GROUP BY 1"): print(f" {kind_:24s} {n:>7}") raise SystemExit(0) if mode == "blocks": where, params = [], [] if kind: where.append("kind = ?") params.append(kind) if since: where.append(since_expr("first_blocked_at")) params.append(since) clause = (" WHERE " + " AND ".join(where)) if where else "" rows = conn.execute(limited( "SELECT kind, target, classification, org, queries, first_blocked_at," " last_seen_at, block_active, expires_at" f" FROM reputation_block{clause} ORDER BY last_seen_at DESC"), params).fetchall() if not rows: print("(no blocks installed by the query API)") for k, target, cls, org, queries, first, last, active, expires in rows: state = "active" if active else "GONE" exp = f"expires {expires}" if expires else "no expiry" print(f"{last} {k:6s} {target:22s} {state:7s} {exp:34s} " f"hits={queries:<5} {cls or ''} {org or ''}".rstrip()) raise SystemExit(0) # mode == "queries" where, params = [], [] filt = os.environ.get("REP_FILTER", "all") if filt == "flagged": where.append("flagged = 1") elif filt == "blocked": where.append("blocked = 1") elif filt == "deferred": where.append("classification = 'unknown'") if kind: where.append("kind = ?") params.append(kind) if since: where.append(since_expr("queried_at")) params.append(since) clause = (" WHERE " + " AND ".join(where)) if where else "" rows = conn.execute(limited( "SELECT queried_at, kind, value, classification, org, risk, flagged," " target, blocked, source" f" FROM reputation_query{clause} ORDER BY id DESC"), params).fetchall() if not rows: print("(no matching reputation queries)") for ts, k, value, cls, org, risk, flagged, target, blocked, source in rows: flag = "FLAGGED" if flagged else "-" blk = "BLOCKED" if blocked else "-" extra = org or "" if k == "email" and risk: extra = f"risk={risk}" print(f"{ts} {k:6s} {value or '':24s} {cls or '':22s} {flag:8s} {blk:8s} " f"{target or '':22s} {source or '-':16s} {extra}".rstrip()) PYEOF