Suspend immediately when both IP and email signals flag a signup
docker-build-push / build-push (push) Successful in 21s

The hourly OR-based sweep (SUSPICIOUS_GRACE_HOURS) still handles a
single-signal flag as before. This adds an additional check in
process_signup(): a signup flagged by BOTH IP-scrutiny and
email-domain scrutiny at once is a stronger signal, so it's suspended
right away instead of waiting out the grace period, skipping the
hold/welcome path and the suspicious_watch entry entirely.

Gated behind new SUSPICIOUS_COMBINED_* env vars, dry-run first per the
usual rollout convention (unlike SUSPICIOUS_DRY_RUN, which shipped
live by design). Falls back to the normal held-welcome path if the
suspend API call fails, and respects ABUSE_ALLOWLIST.
This commit is contained in:
pmb
2026-07-15 10:07:19 -07:00
parent a7208a84d1
commit fc965e3f17
3 changed files with 162 additions and 0 deletions
+52
View File
@@ -183,6 +183,18 @@ SUSPICIOUS_ACTION = os.environ.get("SUSPICIOUS_ACTION", "suspend").lower()
# this feature. Flip to "true" to pause without redeploying.
SUSPICIOUS_DRY_RUN = os.environ.get("SUSPICIOUS_DRY_RUN", "false").lower() in ("1", "true", "yes")
# --- Combined-signal immediate suspend (both IP AND email flagged) ---
# The sweep above acts on EITHER signal after SUSPICIOUS_GRACE_HOURS of no
# activity. A signup flagged by BOTH signals at once is a stronger indicator
# than either alone, so it's acted on immediately in process_signup() instead
# of waiting for the hourly sweep — this is additive, it doesn't change the
# OR-based sweep's behavior for single-signal flags.
SUSPICIOUS_COMBINED_ENABLED = os.environ.get("SUSPICIOUS_COMBINED_ENABLED", "true").lower() in ("1", "true", "yes")
SUSPICIOUS_COMBINED_ACTION = os.environ.get("SUSPICIOUS_COMBINED_ACTION", "suspend").lower()
# Rollout safety switch — ships dry-run-first (unlike SUSPICIOUS_DRY_RUN's
# by-design "false" default above) since this is a brand-new action path.
SUSPICIOUS_COMBINED_DRY_RUN = os.environ.get("SUSPICIOUS_COMBINED_DRY_RUN", "true").lower() in ("1", "true", "yes")
if not WEBHOOK_SECRET:
log.warning("WEBHOOK_SECRET is empty — signature verification will reject all requests.")
if not BOT_ACCESS_TOKEN:
@@ -601,9 +613,18 @@ def process_signup(account_id: str, acct: str, ip: str, email: str = "",
Admin::Account.approved state at signup time: if it's already true (open
registration, never queued), account.approved will never be delivered for
it, so the hold is lifted immediately instead of waiting forever.
A signup flagged by BOTH signals at once (see SUSPICIOUS_COMBINED_ENABLED)
is auto-actioned immediately here rather than falling through to the
hold/welcome path — no welcome, no suspicious-watch entry, since the
account is already suspended. This is additive to the existing hourly
sweep (app/suspicious_sweep.py), which still separately handles
single-signal flags after SUSPICIOUS_GRACE_HOURS of inactivity.
"""
reasons: list[str] = []
hold = False
ip_flagged = False
email_flagged = False
if IP_SCRUTINY_ENABLED and ip:
classification, org, ip_flagged = classify_signup_ip(ip)
@@ -635,6 +656,37 @@ def process_signup(account_id: str, acct: str, ip: str, email: str = "",
reasons.append(f"{prefix}email domain {domain} (disposable={is_disposable}, risk={risk})")
hold = hold or (CHECK_MAIL_HOLD_WELCOME and not CHECK_MAIL_DRY_RUN)
if ip_flagged and email_flagged and SUSPICIOUS_COMBINED_ENABLED:
allowlisted = (acct.split("@")[0].lower() in ABUSE_ALLOWLIST
or acct.lower() in ABUSE_ALLOWLIST)
if not allowlisted:
note = (
f"Auto-{SUSPICIOUS_COMBINED_ACTION} at signup: flagged by BOTH "
f"IP-scrutiny and email-domain scrutiny ({'; '.join(reasons)})."
)
if SUSPICIOUS_COMBINED_DRY_RUN:
log.warning("[DRY-RUN] would %s acct=%s immediately (IP+email combined signal)",
SUSPICIOUS_COMBINED_ACTION, acct)
dm_moderator(
f"[DRY-RUN] would {SUSPICIOUS_COMBINED_ACTION} @{acct} immediately — "
f"flagged by BOTH IP and email signals ({'; '.join(reasons)})."
)
else:
try:
apply_action(account_id, SUSPICIOUS_COMBINED_ACTION, note)
except httpx.HTTPError as exc:
log.error("acct=%s: failed to immediately %s on combined signal, "
"falling back to the normal flagged-signup path: %s",
acct, SUSPICIOUS_COMBINED_ACTION, exc)
else:
log.warning("auto-%sd acct=%s immediately — combined IP+email signup signal",
SUSPICIOUS_COMBINED_ACTION, acct)
dm_moderator(
f"🚨 Auto-{SUSPICIOUS_COMBINED_ACTION}d @{acct} immediately — "
f"flagged by BOTH IP and email signals ({'; '.join(reasons)})."
)
return
if not reasons:
send_welcome(account_id, acct)
return