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
+91
View File
@@ -404,6 +404,96 @@ def email_scrutiny_tests():
assert domain_blocks == [], domain_blocks
def combined_signal_tests():
"""Drive process_signup's combined IP+email immediate-suspend path
(SUSPICIOUS_COMBINED_*), independent of the OR-based hourly sweep."""
sent = []
dms = []
actions = []
ipblocks = []
domain_blocks = []
main.send_welcome = lambda account_id, acct: sent.append((account_id, acct))
main.dm_moderator = lambda message: dms.append(message)
main.apply_action = lambda target_id, action, text: actions.append((target_id, action))
main.register_ip_block = lambda ip, acct, org: ipblocks.append((ip, acct, org))
main.register_email_domain_block = lambda domain, acct: domain_blocks.append((domain, acct))
main.fetch_account_counts = lambda account_id: (0, 0)
main.IP_SCRUTINY_ENABLED = True
main.IP_SCRUTINY_DRY_RUN = False
main.IP_SCRUTINY_HOLD_WELCOME = True
main.IP_SCRUTINY_AUTO_IPBLOCK = True
main.CHECK_MAIL_ENABLED = True
main.CHECK_MAIL_API_KEY = "test-key"
main.CHECK_MAIL_DRY_RUN = False
main.CHECK_MAIL_HOLD_WELCOME = True
main.CHECK_MAIL_AUTO_DOMAIN_BLOCK = True
main.SUSPICIOUS_COMBINED_ENABLED = True
main.SUSPICIOUS_COMBINED_ACTION = "suspend"
main.SUSPICIOUS_COMBINED_DRY_RUN = False
main.ABUSE_ALLOWLIST = {"trustedstaff"}
ip_classifications = {
"198.51.100.40": ("datacenter", "Example Cloud Hosting Inc", True),
"198.51.100.41": ("datacenter", "Example Cloud Hosting Inc", True),
}
main.classify_signup_ip = lambda ip: ip_classifications[ip]
email_classifications = {
"temp-mail.org": (True, 99),
"gmail.com": (False, 5),
}
main.classify_email_domain = lambda domain: email_classifications[domain]
# A. both signals flagged, live -> suspended immediately, no welcome, no
# suspicious-watch entry (already actioned, nothing left to sweep).
# The individual ip/email auto-blocks still fire independently.
main.process_signup("501", "bothbad", "198.51.100.40", "[email protected]")
assert ("501", "suspend") in actions, actions
assert ("501", "bothbad") not in sent, "must not welcome an immediately-suspended signup"
assert any("bothbad" in d and "BOTH" in d for d in dms), dms
with main._db() as conn:
row = conn.execute(
"SELECT 1 FROM suspicious_watch WHERE account_id = ?", ("501",),
).fetchone()
assert row is None, "an immediately-suspended signup must not also enter the hourly watch"
assert ("198.51.100.40", "bothbad", "Example Cloud Hosting Inc") in ipblocks, ipblocks
assert ("temp-mail.org", "bothbad") in domain_blocks, domain_blocks
# B. only IP flagged -> untouched by the combined path, falls through to
# the normal single-signal hold/welcome behavior.
actions.clear(); sent.clear(); dms.clear()
main.process_signup("502", "iponly", "198.51.100.41", "[email protected]")
assert ("502", "suspend") not in actions, actions
assert ("502", "iponly") not in sent, "single-signal flag should still be held"
# C. both flagged, but allowlisted acct -> combined path skipped; falls
# through to the normal (still-held) path like any other flagged signup.
actions.clear(); sent.clear(); dms.clear()
main.process_signup("503", "trustedstaff", "198.51.100.40", "[email protected]")
assert ("503", "suspend") not in actions, "allowlisted acct must not be auto-suspended"
# D. both flagged, SUSPICIOUS_COMBINED_DRY_RUN -> DM only, no real
# suspend; falls through to the normal held-welcome path.
actions.clear(); sent.clear(); dms.clear()
main.SUSPICIOUS_COMBINED_DRY_RUN = True
main.process_signup("504", "drybothbad", "198.51.100.40", "[email protected]")
assert ("504", "suspend") not in actions, "dry-run must not call apply_action"
assert any("[DRY-RUN]" in d and "drybothbad" in d for d in dms), dms
assert ("504", "drybothbad") not in sent, "still held by the individual signal holds"
# E. both flagged, live, but apply_action fails -> falls back to the
# normal held-welcome path instead of silently dropping the signup.
actions.clear(); sent.clear(); dms.clear()
main.SUSPICIOUS_COMBINED_DRY_RUN = False
def boom(target_id, action, text):
raise main.httpx.HTTPError("boom")
main.apply_action = boom
main.process_signup("505", "failsuspend", "198.51.100.40", "[email protected]")
assert ("505", "failsuspend") not in sent, "still held, falls back to the normal flagged path"
def suspicious_watch_tests():
"""Drive maybe_start_suspicious_watch: baseline capture on flagged
signups only, never on clean ones, and never twice for the same account."""
@@ -553,6 +643,7 @@ if __name__ == "__main__":
policy_tests()
ip_scrutiny_tests()
email_scrutiny_tests()
combined_signal_tests()
suspicious_watch_tests()
suspicious_sweep_tests()
print("ALL TESTS PASSED")