164 lines
7.2 KiB
Python
164 lines
7.2 KiB
Python
"""Разбор абузов: промо, триалы, ключи, сливы, софт, реселл.
|
||
|
||
Работает поверх уже собранной базы (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} |