From 82cf364159341896b513631555ae3466bda9feb1 Mon Sep 17 00:00:00 2001 From: June P Date: Sun, 23 Jul 2023 13:52:08 +0900 Subject: [PATCH] feat: More robust antiraid --- bridget/cogs/antiraid.py | 73 +++++++++++++++++++++++++++++++--------- bridget/utils/pfpcalc.py | 25 ++++++++++++++ bridget/utils/reports.py | 6 ++-- pdm.lock | 47 +++++++++++++++++++++++++- pyproject.toml | 1 + 5 files changed, 132 insertions(+), 20 deletions(-) create mode 100644 bridget/utils/pfpcalc.py diff --git a/bridget/cogs/antiraid.py b/bridget/cogs/antiraid.py index c6d0700..14e052a 100644 --- a/bridget/cogs/antiraid.py +++ b/bridget/cogs/antiraid.py @@ -1,16 +1,21 @@ import re +import discord +import os + from asyncio import Lock from datetime import datetime, timedelta, timezone +from discord.ext import commands +from discord import app_commands +from expiringdict import ExpiringDict +from datasketch import MinHash, MinHashLSH -import discord from bridget.utils.enums import PermissionLevel -from model import Case +from bridget.utils.pfpcalc import calculate_hash, hamming_distance +from model import Infraction from utils.services import guild_service, user_service -from discord.ext import commands -from expiringdict import ExpiringDict from utils.config import cfg from utils.mod import prepare_ban_log -from utils.views import report_raid, report_raid_phrase, report_spam +from utils.reports import report_raid, report_raid_phrase, report_spam class RaidType: PingSpam = 1 @@ -38,6 +43,8 @@ def __init__(self, bot): # cooldown to monitor if users are spamming a message (8 within 6 seconds) self.message_spam_detection_threshold = commands.CooldownMapping.from_cooldown( rate=7, per=6.0, type=commands.BucketType.member) + self.message_spam_aggresive_detection_threshold = commands.CooldownMapping.from_cooldown( + rate=3, per=5.5, type=commands.BucketType.member) # cooldown to monitor if too many accounts created on the same date are joining within a short period of time # (5 accounts created on the same date joining within 45 minutes of each other) self.join_overtime_raid_detection_threshold = commands.CooldownMapping.from_cooldown( @@ -67,6 +74,10 @@ def __init__(self, bot): self.join_overtime_lock = Lock() self.banning_lock = Lock() + # caches all joined users for profile picture analysis + self.last30pfps = [] + self.last30messagecontents = {} + @commands.Cog.listener() async def on_member_join(self, member: discord.Member) -> None: """Antiraid filter for when members join. @@ -91,6 +102,24 @@ async def on_member_join(self, member: discord.Member) -> None: member) self.join_user_mapping[member.id] = member + if member.avatar != None: + this_hash = calculate_hash((await member.avatar.to_file()).fp) + + for pfphash in self.last30pfps: + distance = hamming_distance(pfphash, this_hash) + similarity = (distance / 64) + if similarity <= 10: + # 90% chance of similar image! + await report_raid(member) + member.ban(reason="Similar profile picture spam raid detected.") + self.last30pfps.append(this_hash) + return + + + self.last30pfps.append(this_hash) + if len(self.last30pfps) > 30: + del self.last30pfps[0] + # if ratelimit is triggered, we should ban all the users that joined in the past 8 seconds if join_spam_detection_bucket.update_rate_limit(current): users = list(self.join_user_mapping.keys()) @@ -176,11 +205,11 @@ async def on_message(self, message: discord.Message): return if message.guild.id != cfg.guild_id: return - message.author = message.guild.get_member(message.author.id) - if PermissionLevel.MOD == message.guild: + if PermissionLevel.MOD == message.author: return + if await self.message_spam(message): await self.handle_raid_detection(message, RaidType.MessageSpam) elif await self.ping_spam(message): @@ -264,10 +293,19 @@ async def ping_spam(self, message: discord.Message): A report is generated which a mod must review (either unmute or ban the user using a react) """ + umentioncount, rmentioncount = 4, 2 + if PermissionLevel.MEMPLUS == message.author: - return False + umentioncount, rmentioncount = 6, 2 + elif PermissionLevel.MEMPRO == message.author: + umentioncount, rmentioncount = 8, 3 + + + if (abs(datetime.now().timestamp() - message.author.joined_at.timestamp()) <= 43200 or datetime.now().timestamp() - (((message.author.id << 22) + 1420070400000) / 1000) <= 432000) and not PermissionLevel.MEMPLUS == message.author: + # Aggresive raid detection target (joined guild in the last 12 hours or created account within the last 5 days and is not a member plus) + umentioncount, rmentioncount = 2, 1 - if len(set(message.mentions)) > 4 or len(set(message.role_mentions)) > 2: + if len(set(message.mentions)) > umentioncount or len(set(message.role_mentions)) > rmentioncount: bucket = self.spam_report_cooldown.get_bucket(message) current = message.created_at.replace( tzinfo=timezone.utc).timestamp() @@ -289,7 +327,11 @@ async def message_spam(self, message: discord.Message): if PermissionLevel.MEMPLUS == message.author: return False - bucket = self.message_spam_detection_threshold.get_bucket(message) + if (abs(datetime.now().timestamp() - message.author.joined_at.timestamp()) <= 43200 or datetime.now().timestamp() - (((message.author.id << 22) + 1420070400000) / 1000) <= 432000) and not PermissionLevel.MEMPLUS == message.author: + # Aggresive raid detection target (joined guild in the last 12 hours or created account within the last 5 days and is not a member plus) + bucket = self.message_spam_aggresive_detection_threshold.get_bucket(message) + else: + bucket = self.message_spam_detection_threshold.get_bucket(message) current = message.created_at.replace(tzinfo=timezone.utc).timestamp() if bucket.update_rate_limit(current): @@ -298,7 +340,6 @@ async def message_spam(self, message: discord.Message): tzinfo=timezone.utc).timestamp() if not bucket.update_rate_limit(current): user = message.author - ctx = await self.bot.get_context(message) # await mute(ctx, user, mod=ctx.guild.me, reason="Message spam") twoweek = datetime.now() + timedelta(days=14) @@ -344,8 +385,8 @@ async def raid_ban(self, user: discord.Member, reason="Raid phrase detected", dm db_guild = guild_service.get_guild() - case = Case( - _id=db_guild.case_id, + infraction = Infraction( + _id=db_guild.infraction_id, _type="BAN", date=datetime.now(), mod_id=self.bot.user.id, @@ -354,10 +395,10 @@ async def raid_ban(self, user: discord.Member, reason="Raid phrase detected", dm reason=reason ) - guild_service.inc_caseid() - user_service.add_case(user.id, case) + guild_service.inc_infractionid() + user_service.add_infraction(user.id, infraction) - log = prepare_ban_log(self.bot.user, user, case) + log = prepare_ban_log(self.bot.user, user, infraction) if dm_user: try: diff --git a/bridget/utils/pfpcalc.py b/bridget/utils/pfpcalc.py new file mode 100644 index 0000000..deb91b0 --- /dev/null +++ b/bridget/utils/pfpcalc.py @@ -0,0 +1,25 @@ +from PIL import Image +from typing import Union +from io import BytesIO + +def calculate_hash(image_path: Union[str, bytes, BytesIO]) -> str: + # Open image using PIL + image = Image.open(image_path) + + # Resize image to a fixed size (e.g., 8x8 pixels) + image = image.resize((8, 8), Image.ANTIALIAS) + + # Convert image to grayscale + image = image.convert('L') + + # Calculate average pixel value + average_pixel = sum(image.getdata()) / 64 + + # Generate binary hash + hash_value = ''.join(['1' if pixel >= average_pixel else '0' for pixel in image.getdata()]) + + return hash_value + +def hamming_distance(hash1: str, hash2: str) -> float: + # Calculate the Hamming distance between two hashes + return sum(c1 != c2 for c1, c2 in zip(hash1, hash2)) diff --git a/bridget/utils/reports.py b/bridget/utils/reports.py index da15daa..eb8857c 100644 --- a/bridget/utils/reports.py +++ b/bridget/utils/reports.py @@ -268,11 +268,11 @@ def prepare_embed(target: Union[discord.Message, discord.Member], word: str = No name="Roles", value=roles if roles else "None", inline=False) if len(rd) > 0: - embed.add_field(name=f"{len(rd)} most recent cases", + embed.add_field(name=f"{len(rd)} most recent infractions", value=rd_text, inline=True) else: - embed.add_field(name=f"Recent cases", - value="This user has no cases.", inline=True) + embed.add_field(name=f"Recent infractions", + value="This user has no infractions.", inline=True) return embed diff --git a/pdm.lock b/pdm.lock index a8bb528..8b86275 100644 --- a/pdm.lock +++ b/pdm.lock @@ -96,6 +96,15 @@ dependencies = [ "tiktoken<1.0.0,>=0.3.2", ] +[[package]] +name = "datasketch" +version = "1.5.9" +summary = "Probabilistic data structures for processing and searching very large datasets" +dependencies = [ + "numpy>=1.11", + "scipy>=1.0.0", +] + [[package]] name = "discord-py" version = "2.3.0a4848+gd34a8841" @@ -307,6 +316,15 @@ dependencies = [ "pygments<3.0.0,>=2.13.0", ] +[[package]] +name = "scipy" +version = "1.9.3" +requires_python = ">=3.8" +summary = "Fundamental algorithms for scientific computing in Python" +dependencies = [ + "numpy<1.26.0,>=1.18.5", +] + [[package]] name = "setuptools" version = "67.7.1" @@ -380,7 +398,7 @@ dependencies = [ lock_version = "4.2" cross_platform = true groups = ["default"] -content_hash = "sha256:cce4d749f02c701a7cfc8202eee4b617c855e172faa65a07308b82990b927caa" +content_hash = "sha256:ec6e48fd4525ea915ce2459dc30d57ab91d42b287e8239a6ebfbc1f638e26f9b" [metadata.files] "aiocache 0.12.1" = [ @@ -581,6 +599,10 @@ content_hash = "sha256:cce4d749f02c701a7cfc8202eee4b617c855e172faa65a07308b82990 {url = "https://files.pythonhosted.org/packages/fa/8e/2e5c742c3082bce3eea2ddd5b331d08050cda458bc362d71c48e07a44719/charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, {url = "https://files.pythonhosted.org/packages/ff/d7/8d757f8bd45be079d76309248845a04f09619a7b17d6dfc8c9ff6433cac2/charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, ] +"datasketch 1.5.9" = [ + {url = "https://files.pythonhosted.org/packages/34/42/22ca877495066c15f05ed0fef1769545ff81efc97de0bfca49e703e06a49/datasketch-1.5.9.tar.gz", hash = "sha256:c725ff61489c23a8e97d1d83c3faad67ec7425ee6ae73113c95ee4499dc14d44"}, + {url = "https://files.pythonhosted.org/packages/80/32/ca56a284db10c9eae321162044b931489ac4736a376c2728b45a180e3772/datasketch-1.5.9-py3-none-any.whl", hash = "sha256:7b8b9a267a92924a80f02fed33a33e5e9813684e5deb0fe1cd31814de7a59d61"}, +] "dnspython 2.3.0" = [ {url = "https://files.pythonhosted.org/packages/12/86/d305e87555430ff4630d729420d97dece3b16efcbf2b7d7e974d11b0d86c/dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"}, {url = "https://files.pythonhosted.org/packages/91/8b/522301c50ca1f78b09c2ca116ffb0fd797eadf6a76085d376c01f9dd3429/dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"}, @@ -1133,6 +1155,29 @@ content_hash = "sha256:cce4d749f02c701a7cfc8202eee4b617c855e172faa65a07308b82990 {url = "https://files.pythonhosted.org/packages/31/3b/2360352760b436f822258396e66ffb6d42585518a9cde2f93f142e64c5eb/rich-13.3.4.tar.gz", hash = "sha256:b5d573e13605423ec80bdd0cd5f8541f7844a0e71a13f74cf454ccb2f490708b"}, {url = "https://files.pythonhosted.org/packages/9d/1a/28117ae737aec7c004ed5067034a8949adab43730420b50312821f466c3f/rich-13.3.4-py3-none-any.whl", hash = "sha256:22b74cae0278fd5086ff44144d3813be1cedc9115bdfabbfefd86400cb88b20a"}, ] +"scipy 1.9.3" = [ + {url = "https://files.pythonhosted.org/packages/0a/2e/44795c6398e24e45fa0bb61c3e98de1cfea567b1b51efd3751e2f7ff9720/scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"}, + {url = "https://files.pythonhosted.org/packages/40/0e/3ff193b6ba6a0a6f13f8d367e8976370232e769bd609c8c11d86e0353adf/scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"}, + {url = "https://files.pythonhosted.org/packages/42/14/d2500818b7bb7b862d70c1ae97e646a4795b068583c67720553764095024/scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"}, + {url = "https://files.pythonhosted.org/packages/42/81/0a64d2204c3b261380ac96c6d61f018528108b62c0e21e6153a58cebf4f6/scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"}, + {url = "https://files.pythonhosted.org/packages/44/8a/bae77e624391b27aeea2d33a02f2ce4a8019f1378ce92faf5780f1521f2e/scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"}, + {url = "https://files.pythonhosted.org/packages/56/af/6a2b90fe280e89466d84747054667f74b84a8304f75931a173090919991f/scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"}, + {url = "https://files.pythonhosted.org/packages/59/0b/8a9acfc5c36bbf6e18d02f3a08db5b83bebba510be2df3230f53852c74a4/scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"}, + {url = "https://files.pythonhosted.org/packages/59/ef/d54d17c36b46a9b8f6e1d4bf039b7f7ad236504cfb13cf1872caec9cbeaa/scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"}, + {url = "https://files.pythonhosted.org/packages/84/86/4f38fa30c112c3590954420f85d95b8cd23811ecc5cfc4bfd4d988d4db44/scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"}, + {url = "https://files.pythonhosted.org/packages/92/f9/7ae2c1ae200212bc84b5a8369a10d644aa8b588140fe292d59db3b4a2545/scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"}, + {url = "https://files.pythonhosted.org/packages/b5/67/c5451465ec94e654e6315cd5136961d267ae94a0f799b85d26eb9efe4c9f/scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"}, + {url = "https://files.pythonhosted.org/packages/bb/b7/380c9e4cd71263f03d16f8a92c0e44c9bdef38777e1a7dde1f47ba996bac/scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"}, + {url = "https://files.pythonhosted.org/packages/c3/3e/e40c52775a5d19abd43b1c245fbc5dee283a29acc45c830bc73bfad9468b/scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"}, + {url = "https://files.pythonhosted.org/packages/c8/0f/d9f8c50be8670b7ba6f002679e84cd18f46a23faf62c1590f4d1bbec0c8c/scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"}, + {url = "https://files.pythonhosted.org/packages/ce/28/635391e72e24bd3f4a91e374f4a186a5e4ecc95f23d8a55c9b0d25777cf7/scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"}, + {url = "https://files.pythonhosted.org/packages/cf/0e/3f1685c1fcb5dfe35ec027a5fc7a29e8818c61b2cc7fa207b4fc7b959f52/scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"}, + {url = "https://files.pythonhosted.org/packages/d0/96/4f6eac3fea18f836a0e403539556b1684e6f3361fa39aa5d5797dedecd75/scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"}, + {url = "https://files.pythonhosted.org/packages/df/75/c0254dc58d1f1b00f9d3dbda029743b71b815dd512461ed20d9b7f459e37/scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"}, + {url = "https://files.pythonhosted.org/packages/f4/9d/882134b1e774a9227ab855c71a39612194e1106185595417ce92f0f1e78c/scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"}, + {url = "https://files.pythonhosted.org/packages/f9/37/5cd44af74d7178a44452b17ea162bc93996d5555b4a978877d2efd56fe84/scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"}, + {url = "https://files.pythonhosted.org/packages/fb/ba/1733dbbc19f2aa07d100cfa220bcc83a3977bc5c9f0a5ad262dae1f3ab90/scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"}, +] "setuptools 67.7.1" = [ {url = "https://files.pythonhosted.org/packages/01/18/73f7ca619f72b22300493451350214d3a035e7fa8f17ce31db2da24fe0c9/setuptools-67.7.1.tar.gz", hash = "sha256:bb16732e8eb928922eabaa022f881ae2b7cdcfaf9993ef1f5e841a96d32b8e0c"}, {url = "https://files.pythonhosted.org/packages/47/6a/dfaf9838b6dd1f04537c0a66cf880dd24acd829f8a39d468c11fc0b7d8c6/setuptools-67.7.1-py3-none-any.whl", hash = "sha256:6f0839fbdb7e3cfef1fc38d7954f5c1c26bf4eebb155a55c9bf8faf997b9fb67"}, diff --git a/pyproject.toml b/pyproject.toml index b99b04b..b52cadd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "opencv-python>=4.8.0.74", "expiringdict>=1.2.2", "pytimeparse>=1.1.8", + "datasketch>=1.5.9", ] requires-python = ">=3.10,<4.0" license = {text = "BSD-3-Clause"}