Add suspicious-signup sweep: suspend flagged signups with no activity

Watches every account flagged by IP-scrutiny or email-domain scrutiny at
signup and, once it goes live, records a baseline post/follow count. A
scheduled sweep (app/suspicious_sweep.py, run via cron on admin.yttrx.com)
suspends any watch past SUSPICIOUS_GRACE_HOURS with zero new posts and
zero new follows since that baseline; any activity clears the watch.

Works whether yttrx is open-registration or requires moderator approval,
since the watch starts at whichever event actually makes the account live
(account.created vs account.approved), same dual handling the welcome flow
already uses. Needs ABUSE_BOT_TOKEN re-minted with admin:read:accounts.
This commit is contained in:
2026-07-05 10:22:39 -07:00
parent 656eec09c0
commit 9924e06b5a
7 changed files with 637 additions and 4 deletions
+65
View File
@@ -124,14 +124,57 @@ Registering email_domain_blocks requires the `ABUSE_BOT_TOKEN` to carry the
`admin:write:email_domain_blocks` scope in addition to its existing scopes
(see `CLAUDE.md`).
## Suspicious-signup sweep
A scheduled companion to the two signup-scrutiny signals above (`app/suspicious_sweep.py`,
run via `docker exec yttrx-welcomebot python -m app.suspicious_sweep`, e.g. hourly cron on
`admin.yttrx.com`):
1. Any account flagged by **either** IP-scrutiny (datacenter/hosting) or
email-domain scrutiny (disposable/high-risk) has a grace-period clock
started the moment it goes live — `account.created` if signups are open
(or auto-approved), `account.approved` if this instance requires
moderator approval. Same dual-event handling the welcome flow already
uses, so it works in either registration mode without extra config. A
baseline snapshot of `statuses_count`/`following_count` is recorded at
that moment (`app.main.maybe_start_suspicious_watch`, table
`suspicious_watch`).
2. The sweep looks for watches past `SUSPICIOUS_GRACE_HOURS` (default 24)
and compares current counts to the baseline:
- **Any new post or new follow** since the baseline → cleared, never
rechecked.
- **Zero of either** → `SUSPICIOUS_ACTION` (default `suspend`) is applied,
the moderator is DMed, and the watch is marked resolved.
3. Already-suspended/silenced accounts, staff (`ABUSE_SKIP_PRIVILEGED`), and
`ABUSE_ALLOWLIST` handles are skipped exactly as elsewhere in this bot —
the sweep reuses those same settings rather than defining its own.
Unlike the report-driven abuse-bot (default `silence`), this sweep defaults
to **`suspend`** — the reasoning is that zero organic activity in a full day
is a stronger bulk/bot-signup signal than a report alone, and Mastodon
suspensions have a server-side undo window. It also ships **live**
(`SUSPICIOUS_DRY_RUN=false`) rather than with the usual dry-run rollout step;
`SUSPICIOUS_DRY_RUN=true` remains available as a kill switch if you want to
pause it without redeploying.
Requires `ABUSE_BOT_TOKEN` to additionally carry the **`admin:read:accounts`**
scope (fetches live counts + role/suspended state via
`GET /api/v1/admin/accounts/:id`) — see `CLAUDE.md`'s moderator-token-gotcha
section for the remint procedure.
Inspect the watch list: `welcomebot-suspicious` / `welcomebot-suspicious --pending`
(see `bin/welcomebot-suspicious`).
## Layout
| Path | Purpose |
|---|---|
| `app/main.py` | The FastAPI app |
| `app/suspicious_sweep.py` | Scheduled sweep: suspends flagged signups with no activity (run via cron, not the webserver) |
| `Dockerfile`, `docker-compose.yml` | Container build + run |
| `.env.example` | Config template (copy to `.env`, never commit) |
| `nginx/hooks.yttrx.com.conf` | nginx site for admin.yttrx.com |
| `bin/welcomebot-logs`, `bin/welcomebot-signups`, `bin/welcomebot-suspicious` | Ops CLIs installed to `/root/bin` on admin.yttrx.com |
| `test_local.py` | Offline smoke test (no network) |
## Configuration
@@ -172,6 +215,10 @@ Copy `.env.example` to `.env` and fill in:
| `CHECK_MAIL_RISK_THRESHOLD` | Flag if `is_disposable` or `risk` ≥ this (default 80) |
| `CHECK_MAIL_HOLD_WELCOME` | `true` — hold the welcome for a flagged signup until `account.approved` |
| `CHECK_MAIL_AUTO_DOMAIN_BLOCK` | Auto-register a flagged domain into Mastodon's `Admin::EmailDomainBlock`; also gates the report-triggered override |
| `SUSPICIOUS_SWEEP_ENABLED` | Master switch for the suspicious-signup sweep (`app/suspicious_sweep.py`) |
| `SUSPICIOUS_GRACE_HOURS` | Hours a flagged signup has to post or follow someone before it's swept (default 24) |
| `SUSPICIOUS_ACTION` | `suspend` (default) or `silence`, applied to a flagged signup with zero activity in the grace window |
| `SUSPICIOUS_DRY_RUN` | `false` (ships live by design) — set `true` to log/DM only, no real action |
## Local test
@@ -272,11 +319,29 @@ Register a throwaway test account on yttrx and confirm the `@welcome` bot DMs
it. Then delete the test account (`tootctl accounts delete` on mammut) and the
test toot.
### 6. Cron for the suspicious-signup sweep (on admin.yttrx.com)
The sweep is not part of the always-running webhook server — it's invoked
on a schedule via `docker exec`. `ABUSE_BOT_TOKEN` must additionally carry
the `admin:read:accounts` scope (remint via the Rails-console snippet in
`CLAUDE.md`, same procedure as the other scope additions) before installing
this cron entry, or every sweep run 403s per-account:
```bash
crontab -l | { cat; echo '17 * * * * docker exec yttrx-welcomebot python -m app.suspicious_sweep >> /var/log/welcomebot-suspicious-sweep.log 2>&1'; } | crontab -
```
Hourly (rather than daily like the dormant-sweep) since the grace window is
only `SUSPICIOUS_GRACE_HOURS` (default 24) — an hourly cadence keeps the gap
between crossing the deadline and being swept small.
## Operations
```bash
welcomebot-logs -f --abuse # convenience CLI on admin (see bin/welcomebot-logs)
welcomebot-signups --flagged # signup-IP classification history (see bin/welcomebot-signups)
welcomebot-suspicious --pending # suspicious-sweep watch list (see bin/welcomebot-suspicious)
docker exec yttrx-welcomebot python -m app.suspicious_sweep # run the sweep manually
docker compose logs -f welcomebot # watch deliveries
docker compose restart welcomebot # after editing .env
docker compose down && docker compose up -d --build # redeploy after code change