reputation: russian rationale alongside english

This commit is contained in:
Andrew Pimenov 2026-08-09 04:10:08 +03:00
parent db407595d6
commit 9e5ea923aa
2 changed files with 63 additions and 2 deletions

View file

@ -169,26 +169,43 @@ def captcha_risk(
ip_type: str,
abuseipdb_score: int | None,
) -> tuple[str, str]:
risk, rationale, _ = _captcha_risk_full(firehol_hits, dnsbl_hits, ip_type, abuseipdb_score)
return risk, rationale
def _captcha_risk_full(
firehol_hits: list[str],
dnsbl_hits: list[DnsblHit],
ip_type: str,
abuseipdb_score: int | None,
) -> tuple[str, str, str]:
reasons: list[str] = []
reasons_ru: list[str] = []
risk = "low"
if firehol_hits:
risk = "high"
reasons.append(f"listed on {', '.join(firehol_hits)}")
reasons_ru.append(f"в блоклисте {', '.join(firehol_hits)}")
if dnsbl_hits:
risk = "high"
reasons.append(f"listed on {', '.join(h.zone for h in dnsbl_hits)}")
reasons_ru.append(f"в блоклисте {', '.join(h.zone for h in dnsbl_hits)}")
if abuseipdb_score is not None and abuseipdb_score >= 50:
risk = "high"
reasons.append(f"AbuseIPDB confidence {abuseipdb_score}")
reasons_ru.append(f"уверенность AbuseIPDB {abuseipdb_score}")
elif abuseipdb_score is not None and abuseipdb_score >= 25 and risk == "low":
risk = "medium"
reasons.append(f"AbuseIPDB confidence {abuseipdb_score}")
reasons_ru.append(f"уверенность AbuseIPDB {abuseipdb_score}")
if risk == "low" and ip_type == "hosting":
risk = "medium"
reasons.append("egress looks like hosting/proxy space, which many sites challenge by default")
reasons_ru.append("исходящий адрес похож на хостинг/прокси-пространство, которое многие сайты по умолчанию проверяют капчей")
if not reasons:
reasons.append("no blocklist match and the address looks like ordinary end-user space")
return risk, "; ".join(reasons)
reasons_ru.append("совпадений с блоклистами нет, адрес выглядит как обычное пользовательское пространство")
return risk, "; ".join(reasons), "; ".join(reasons_ru)
def build_reputation(
@ -203,7 +220,7 @@ def build_reputation(
blocked = False
if dnsbl_outcomes is not None:
hits, blocked = summarize_dnsbl(dnsbl_outcomes)
risk, rationale = captcha_risk(firehol_hits, hits or [], ip_type, abuseipdb_score)
risk, rationale, rationale_ru = _captcha_risk_full(firehol_hits, hits or [], ip_type, abuseipdb_score)
return Reputation(
internetdb=internetdb,
firehol_hits=firehol_hits,
@ -213,6 +230,7 @@ def build_reputation(
abuseipdb_reports=abuseipdb_reports,
captcha_risk=risk,
rationale=rationale,
rationale_ru=rationale_ru,
)

View file

@ -156,3 +156,46 @@ def test_build_reputation_without_dnsbl_leaves_the_field_none(api_fixture):
assert rep.dnsbl_hits is None
assert rep.dnsbl_query_blocked is False
assert rep.captcha_risk == "high"
_CYRILLIC = set("абвгдеёжзийклмнопрстуфхцчшщъыьэюя")
def has_cyrillic(text: str) -> bool:
return any(ch in _CYRILLIC for ch in text.lower())
def test_build_reputation_rationale_ru_is_populated_and_cyrillic_for_every_reason(api_fixture):
cases = [
dict(firehol_hits=["firehol_level1"], dnsbl_outcomes=None, ip_type="residential", abuseipdb_score=None),
dict(
firehol_hits=[],
dnsbl_outcomes=[decode_dnsbl("zen.spamhaus.org", ["127.0.0.2"])],
ip_type="residential",
abuseipdb_score=None,
),
dict(firehol_hits=[], dnsbl_outcomes=None, ip_type="residential", abuseipdb_score=90),
dict(firehol_hits=[], dnsbl_outcomes=None, ip_type="residential", abuseipdb_score=30),
dict(firehol_hits=[], dnsbl_outcomes=None, ip_type="hosting", abuseipdb_score=None),
dict(firehol_hits=[], dnsbl_outcomes=None, ip_type="residential", abuseipdb_score=None),
]
for case in cases:
rep = build_reputation(
internetdb=normalize_internetdb(api_fixture("internetdb.json")),
firehol_hits=case["firehol_hits"],
dnsbl_outcomes=case["dnsbl_outcomes"],
ip_type=case["ip_type"],
abuseipdb_score=case["abuseipdb_score"],
abuseipdb_reports=None,
)
assert rep.rationale
assert rep.rationale_ru
assert has_cyrillic(rep.rationale_ru)
def test_captcha_risk_public_signature_still_returns_a_two_tuple():
result = captcha_risk([], [], "residential", None)
assert len(result) == 2
risk, rationale = result
assert risk == "low"
assert rationale