Suspend immediately on strong IP-reputation signals (not vpn alone)
docker-build-push / build-push (push) Failing after 15s
docker-build-push / build-push (push) Failing after 15s
datacenter/proxy/tor/abuser is a much cleaner bulk/bot-signup indicator than vpn, which also flags plenty of privacy-conscious real users. A strong-flagged signup now suspends immediately regardless of the email signal; vpn-only still gets the normal held-welcome/ipblock/lowered- threshold treatment, falling through to the existing combined-signal suspend only if also paired with a flagged email domain.
This commit is contained in:
+83
-26
@@ -11,7 +11,9 @@ and dispatches by event:
|
||||
tor/abuser via ipapi.is) and email domain (disposable/high-risk via
|
||||
check-mail.org); a flagged
|
||||
signup gets more scrutiny (held welcome, auto ip_block/email_domain_block,
|
||||
moderator DM) before falling through to a normal welcome.
|
||||
moderator DM) before falling through to a normal welcome. A signup flagged
|
||||
with a *strong* IP signal (datacenter/proxy/tor/abuser, not vpn alone) is
|
||||
suspended immediately regardless of the email signal.
|
||||
* ``report.created`` — evaluates the reported account and, when enough
|
||||
*distinct* reporters have open reports against a young or dormant account,
|
||||
auto-**silences** it (reversible) and DMs a moderator for review. Uses a
|
||||
@@ -147,6 +149,23 @@ IP_SCRUTINY_AUTO_IPBLOCK = os.environ.get("IP_SCRUTINY_AUTO_IPBLOCK", "true").lo
|
||||
# severity: sign_up_requires_approval | sign_up_block | no_access
|
||||
IP_SCRUTINY_IPBLOCK_SEVERITY = os.environ.get("IP_SCRUTINY_IPBLOCK_SEVERITY", "sign_up_requires_approval")
|
||||
|
||||
# Signals stronger than vpn alone — vpn also flags plenty of privacy-conscious
|
||||
# real users, but datacenter/proxy/tor/independently-scored-abuser is a much
|
||||
# cleaner bulk/bot-signup indicator. A signup flagged with any of these
|
||||
# suspends immediately at signup, regardless of the email-domain signal,
|
||||
# instead of just the held-welcome/auto-ipblock/lowered-threshold treatment
|
||||
# every other flagged signup gets.
|
||||
IP_SCRUTINY_STRONG_SIGNALS = frozenset({"datacenter", "proxy", "tor", "abuser"})
|
||||
IP_SCRUTINY_STRONG_SUSPEND_ENABLED = os.environ.get(
|
||||
"IP_SCRUTINY_STRONG_SUSPEND_ENABLED", "true"
|
||||
).lower() in ("1", "true", "yes")
|
||||
IP_SCRUTINY_STRONG_SUSPEND_ACTION = os.environ.get("IP_SCRUTINY_STRONG_SUSPEND_ACTION", "suspend").lower()
|
||||
# Rollout safety switch — ships dry-run-first, same reasoning as
|
||||
# SUSPICIOUS_COMBINED_DRY_RUN, since this is a brand-new action path.
|
||||
IP_SCRUTINY_STRONG_SUSPEND_DRY_RUN = os.environ.get(
|
||||
"IP_SCRUTINY_STRONG_SUSPEND_DRY_RUN", "true"
|
||||
).lower() in ("1", "true", "yes")
|
||||
|
||||
# --- Disposable/high-risk email signup scrutiny (check-mail.org) -----------
|
||||
# Every account.created delivery already carries the signup email for free
|
||||
# (Admin::Account.email). On each new local signup, the bot classifies the
|
||||
@@ -621,6 +640,13 @@ def process_signup(account_id: str, acct: str, ip: str, email: str = "",
|
||||
registration, never queued), account.approved will never be delivered for
|
||||
it, so the hold is lifted immediately instead of waiting forever.
|
||||
|
||||
A signup whose IP is flagged with a *strong* signal — datacenter, proxy,
|
||||
tor, or abuser, but NOT vpn alone (see IP_SCRUTINY_STRONG_SIGNALS) — is
|
||||
auto-actioned immediately here regardless of the email-domain signal, same
|
||||
early-return shape as the combined-signal path below. A vpn-only flag
|
||||
falls through to the normal hold/welcome path unless also combined with a
|
||||
flagged email domain.
|
||||
|
||||
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
|
||||
@@ -631,6 +657,7 @@ def process_signup(account_id: str, acct: str, ip: str, email: str = "",
|
||||
reasons: list[str] = []
|
||||
hold = False
|
||||
ip_flagged = False
|
||||
ip_strong_flagged = False
|
||||
email_flagged = False
|
||||
|
||||
if IP_SCRUTINY_ENABLED and ip:
|
||||
@@ -638,6 +665,7 @@ def process_signup(account_id: str, acct: str, ip: str, email: str = "",
|
||||
record_signup_ip(account_id, acct, ip, classification, org, ip_flagged)
|
||||
|
||||
if ip_flagged:
|
||||
ip_strong_flagged = bool(set(classification.split("+")) & IP_SCRUTINY_STRONG_SIGNALS)
|
||||
log.warning("flagged signup acct=%s ip=%s classification=%s org=%s",
|
||||
acct, ip, classification, org)
|
||||
if IP_SCRUTINY_AUTO_IPBLOCK and not IP_SCRUTINY_DRY_RUN:
|
||||
@@ -663,36 +691,65 @@ 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)})."
|
||||
allowlisted = (acct.split("@")[0].lower() in ABUSE_ALLOWLIST
|
||||
or acct.lower() in ABUSE_ALLOWLIST)
|
||||
|
||||
if ip_strong_flagged and IP_SCRUTINY_STRONG_SUSPEND_ENABLED and not allowlisted:
|
||||
action = IP_SCRUTINY_STRONG_SUSPEND_ACTION
|
||||
note = (
|
||||
f"Auto-{action} at signup: signup IP flagged as {classification} "
|
||||
f"({'; '.join(reasons)})."
|
||||
)
|
||||
if IP_SCRUTINY_STRONG_SUSPEND_DRY_RUN:
|
||||
log.warning("[DRY-RUN] would %s acct=%s immediately (strong IP signal: %s)",
|
||||
action, acct, classification)
|
||||
dm_moderator(
|
||||
f"[DRY-RUN] would {action} @{acct} immediately — strong IP signal "
|
||||
f"({'; '.join(reasons)})."
|
||||
)
|
||||
if SUSPICIOUS_COMBINED_DRY_RUN:
|
||||
log.warning("[DRY-RUN] would %s acct=%s immediately (IP+email combined signal)",
|
||||
else:
|
||||
try:
|
||||
apply_action(account_id, action, note)
|
||||
except httpx.HTTPError as exc:
|
||||
log.error("acct=%s: failed to immediately %s on strong IP signal, "
|
||||
"falling back to the normal flagged-signup path: %s",
|
||||
acct, action, exc)
|
||||
else:
|
||||
log.warning("auto-%sd acct=%s immediately — strong IP signup signal (%s)",
|
||||
action, acct, classification)
|
||||
dm_moderator(
|
||||
f"🚨 Auto-{action}d @{acct} immediately — strong IP signal "
|
||||
f"({'; '.join(reasons)})."
|
||||
)
|
||||
return
|
||||
|
||||
if ip_flagged and email_flagged and SUSPICIOUS_COMBINED_ENABLED and 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"[DRY-RUN] would {SUSPICIOUS_COMBINED_ACTION} @{acct} immediately — "
|
||||
f"🚨 Auto-{SUSPICIOUS_COMBINED_ACTION}d @{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
|
||||
return
|
||||
|
||||
if not reasons:
|
||||
send_welcome(account_id, acct)
|
||||
|
||||
Reference in New Issue
Block a user