Suspend immediately when both IP and email signals flag a signup
docker-build-push / build-push (push) Successful in 21s
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:
+52
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user