Files
omni-parse/omni/abuse.py

164 lines
7.2 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Разбор абузов: промо, триалы, ключи, сливы, софт, реселл.
Работает поверх уже собранной базы (items) и поверх новых абуз-источников:
каждая запись получает набор тегов и вес, панель показывает только то,
что реально пахнет абузом, а не весь поток.
"""
from __future__ import annotations
import re
import sqlite3
from .config import DB
# тег -> (человеческое имя, вес, регулярки)
RULES = [
("keys", "ключи", 40, [
r"sk-[A-Za-z0-9_\-]{20,}", r"gh[pousr]_[A-Za-z0-9]{20,}", r"xox[baprs]-[A-Za-z0-9\-]{10,}",
r"(разда|выда|дарю|бесплатн|free|giveaway)[^.]{0,40}(ключ|key|токен|token)",
r"(ключ|key|токен)[^.]{0,25}(бесплатн|free|дарю|раздач|giveaway)",
]),
("promo", "промо", 26, [
r"промокод", r"promo\s?code", r"промо[\s-]?код", r"скидк[аи]\s+\d{1,3}", r"\d{1,3}\s?%\s?(off|скидк)",
r"redeem\s?(code|код)", r"invite\s?code", r"реф(еральн|код)", r"бонус\s+на\s+сч[её]т", r"кэшб[еэ]к|cashback",
]),
("trial", "триалы", 24, [
r"trial", r"триал", r"free\s+credits?", r"бесплатн\w*\s+(кредит|токен|баланс|доступ|период)",
r"free\s+tier", r"\$\d+\s+free", r"试用", r"免费", r"пробн\w+\s+период", r"без\s+депозита", r"no\s+deposit",
]),
("leak", "сливы", 26, [
r"слив(?!\s*(сок|ки|очн|тест|трафик))", r"leaked\s+(database|db|dump|combo)", r"db\s?dump",
r"combo[\s-]?list", r"stealer\s?log", r"утечк\w*\s+баз", r"база\s+(пассов|почт|данных)",
r"пароли\s+от\s+\w+\.(com|ru|net)",
]),
("tool", "софт", 18, [
r"софт", r"кряк", r"crack(ed)?", r"bypass", r"обход\s+(блокиров|бан|защит)",
r"авторег", r"autoreg", r"panel", r"панел[ьи]\s", r"рассылк", r"sender", r"скрипт",
]),
("resale", "продажа", 14, [
r"продаю", r"продам", r"куплю\s+аккаунт", r"for\s+sale", r"цена\s*[:$]", r"\$\d+\s?/\s?(мес|mo|1k|1000)",
r"реселлер", r"resell",
]),
("news", "новости", 6, [
r"анонс", r"выпустил", r"released?", r"launch", r"обновлени", r"release notes",
r"новая модель", r"new model",
]),
]
COMPILED = [(tag, label, w, [re.compile(p, re.I) for p in pats]) for tag, label, w, pats in RULES]
TAG_LABEL = {t: l for t, l, _, _ in RULES}
STRONG = {"keys", "promo", "trial", "resale"}
# на специализированных абуз-площадках хватает одного признака: сам источник тематический
ABUSE_SRC_SC = 14
# категории-источники, которые вообще имеют смысл для абузов
ABUSE_CATS = {"abuse", "forum", "community", "company_cn"}
# «статьи»: слова про ключи и утечки там — текст, а не раздача
# мусор: заголовок из эмодзи/одного слова без текста — не находка
JUNK_TITLE = re.compile(r"^[\W_\d]{0,40}$", re.U)
# «сливать трафик» — арбитраж, не слив базы
TRAFFIC_NOISE = re.compile(r"слива\w*\s+(трафик|убт|ubt)|pinterest|арбитраж|арбит|tiktok", re.I)
NOISE = re.compile(r"(blog|devto|medium|substack|newsletter|docs?|wiki)", re.I)
def _source_meta():
try:
from . import sources as _s
return {x.name: x.category for x in _s.all_sources()}
except Exception:
return {}
SRC_CAT = _source_meta()
def classify(title: str, text: str, tags: str = ""):
blob = (" ".join([title or "", text or "", tags or ""]))[:4000]
hits = []
score = 0
for tag, label, w, pats in COMPILED:
n = 0
for p in pats:
m = p.search(blob)
if m:
n += 1
if n:
hits.append(tag)
score += w * min(n, 3)
return score, hits
def _qualifies(source, title, text, tags):
sc, hits = classify(title, text, tags)
if not hits:
return 0, []
strong = bool(set(hits) & STRONG)
cat = SRC_CAT.get(source)
blob = (title or "") + " " + (text or "")
if len(re.sub(r"\W", "", title or "")) < 8:
return 0, []
if "leak" in hits and TRAFFIC_NOISE.search(blob) and not re.search(r"combo|dump|утечк|база", blob, re.I):
hits = [h for h in hits if h != "leak"]
sc -= 26
if not hits:
return 0, []
raw_key = bool(re.search(r"(sk-[A-Za-z0-9_\-]{20,}|gh[pousr]_[A-Za-z0-9]{20,})", blob))
if NOISE.search(source or "") and not raw_key:
return 0, []
if cat is None:
# старые записи хранят категорию вместо имени источника
if not raw_key and not str(source or "").startswith(("ab_", "tg_", "reddit_", "discourse_", "xenforo")):
return 0, []
elif cat not in ABUSE_CATS and not raw_key:
return 0, []
if cat == "abuse" and sc >= ABUSE_SRC_SC:
return sc, hits
if strong and sc >= 24:
return sc, hits
if len(hits) >= 2 and sc >= 40:
return sc, hits
return 0, []
def _rows(limit=400, tag=None, q=None, since_days=None):
sql = "SELECT id, source, url, title, text, published, tags, score FROM items"
where, args = [], []
if q:
where.append("(title LIKE ? OR text LIKE ? OR source LIKE ?)")
args += ["%%%s%%" % q, "%%%s%%" % q, "%%%s%%" % q]
if since_days:
where.append("published >= date('now', ?)")
args.append("-%d day" % int(since_days))
if where:
sql += " WHERE " + " AND ".join(where)
sql += " ORDER BY id DESC LIMIT ?"
args.append(int(limit) * 6)
con = sqlite3.connect(str(DB), timeout=30)
try:
cur = con.execute(sql, args)
out = []
for r in cur:
sc, hits = _qualifies(r[1], r[3], r[4], r[6] or "")
if not sc:
continue
if tag and tag not in hits:
continue
out.append({"id": r[0], "source": r[1], "url": r[2], "title": r[3],
"text": (r[4] or "")[:600], "published": r[5],
"tags": hits, "abuse_score": sc, "rank": r[7]})
if len(out) >= limit:
break
return out
finally:
con.close()
def scan(limit=120, tag=None, q=None, since_days=None, window=6000):
"""limit — сколько отдать, window — сколько последних записей просканировать."""
items = _rows(limit=window, tag=tag, q=q, since_days=since_days)
items.sort(key=lambda x: (-x["abuse_score"], -(x["id"] or 0)))
counts = {}
for it in items:
for t in it["tags"]:
counts[t] = counts.get(t, 0) + 1
return {"items": items[:limit], "total": len(items), "tags": counts,
"labels": TAG_LABEL}