diff --git a/techsupport_bot/core/auxiliary.py b/techsupport_bot/core/auxiliary.py index e77d550f..4a840b75 100644 --- a/techsupport_bot/core/auxiliary.py +++ b/techsupport_bot/core/auxiliary.py @@ -5,6 +5,7 @@ from __future__ import annotations +import hashlib import json from functools import wraps from typing import TYPE_CHECKING, Any @@ -529,3 +530,16 @@ async def bot_admin_check_interaction(interaction: discord.Interaction) -> bool: if not is_admin: raise app_commands.MissingPermissions(["bot_admin"]) return True + + +async def get_attachment_hash(attachment: discord.Attachment) -> str: + """Computes a sha256 file hash for a given discord attachment + + Args: + attachment (discord.Attachment): The attachment to generate the hash of + + Returns: + str: The hash, as a string + """ + file_bytes = await attachment.read() + return hashlib.sha256(file_bytes).hexdigest() diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 84c24370..be57fa4e 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -11,7 +11,7 @@ import munch from botlogging import LogContext, LogLevel from commands import moderator, modlog -from core import cogs, extensionconfig, moderation +from core import auxiliary, cogs, extensionconfig, moderation from discord.ext import commands if TYPE_CHECKING: @@ -205,7 +205,7 @@ async def response( if ctx.message.author.top_role >= ctx.channel.guild.me.top_role: return - all_punishments = run_all_checks(config, ctx.message) + all_punishments = await run_all_checks(config, ctx.message) if len(all_punishments) == 0: return @@ -455,7 +455,7 @@ def generate_automod_alert_embed( # All checks will return a list of AutoModPunishment, which may be nothing -def run_all_checks( +async def run_all_checks( config: munch.Munch, message: discord.Message ) -> list[AutoModPunishment]: """This runs all 4 checks on a given discord.Message @@ -475,6 +475,7 @@ def run_all_checks( run_only_string_checks(config, message.clean_content) + handle_file_extensions(config, message.attachments) + handle_mentions(config, message) + + await handle_file_hashes(config, message.attachments) ) return all_violations @@ -528,6 +529,35 @@ def handle_file_extensions( return violations +async def handle_file_hashes( + config: munch.Munch, attachments: list[discord.Attachment] +) -> list[AutoModPunishment]: + """This checks a list of attachments for attachments that match the configured list of hashes + + Args: + config (munch.Munch): The guild config to check with + attachments (list[discord.Attachment]): The list of attachments to search + + Returns: + list[AutoModPunishment]: The automod violations that the given message violated + """ + violations = [] + + for attachment in attachments: + file_hash = await auxiliary.get_attachment_hash(attachment) + if file_hash in config.extensions.automod.banned_file_hashes.value: + violations.append( + AutoModPunishment( + f"{attachment.filename} matches a banned file hash", + recommend_delete=True, + recommend_warn=False, + recommend_mute=3600, + ) + ) + + return violations + + def handle_mentions( config: munch.Munch, message: discord.Message ) -> list[AutoModPunishment]: diff --git a/techsupport_bot/functions/logger.py b/techsupport_bot/functions/logger.py index 97779baa..6cb4aefa 100644 --- a/techsupport_bot/functions/logger.py +++ b/techsupport_bot/functions/logger.py @@ -8,7 +8,7 @@ import discord import munch from botlogging import LogContext, LogLevel -from core import cogs, extensionconfig +from core import auxiliary, cogs, extensionconfig from discord.ext import commands if TYPE_CHECKING: @@ -193,13 +193,13 @@ async def send_message( attachments.insert(0, await author.display_avatar.to_file(filename="avatar.png")) # Make and send the embed and files - embed = build_embed( + embed = await build_embed( message, author, src_channel, content_override, special_flags=special_flags ) await dest_channel.send(embed=embed, files=attachments[:11]) -def build_embed( +async def build_embed( message: discord.Message, author: discord.Member, src_channel: discord.abc.GuildChannel | discord.Thread, @@ -266,6 +266,18 @@ def build_embed( value=", ".join(generate_role_list(author)), ) + # Add file hashes, if relevant + file_hash_string = "" + + if message.attachments: + parts = [] + for attachment in message.attachments: + file_hash = await auxiliary.get_attachment_hash(attachment) + parts.append(f"{attachment.filename}: {file_hash}") + + file_hash_string = ", ".join(parts) + embed.add_field(name="File Hashes", value=file_hash_string) + # Flags if special_flags: embed.add_field(name="Flags", value=", ".join(special_flags)) diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index 8f9b4e94..ef8d0744 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -116,7 +116,7 @@ async def response( "\n" ) > self.max_newlines(config.extensions.paste.length_limit.value): if "automod" in config.get("enabled_extensions", []): - automod_actions = automod.run_all_checks(config, ctx.message) + automod_actions = await automod.run_all_checks(config, ctx.message) automod_final = automod.process_automod_violations(automod_actions) if automod_final and automod_final.delete_message: return