Add anti-raid and anti-nuke protection

Join Gate (account age, avatar, advertising and username filters, bot
checks), raid detection with invite pausing, anti-nuke quarantine of
admins who mass delete, manual and automatic lockdowns, a decaying heat
score against spam, role and channel snapshots with restore, and alert
DMs to the owner and trusted users. Everything that weakens protection
is limited to the owner and the trusted list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
FernandoJVideiraandClaude Opus 5.5 committed 2026-10-20 21:59:52 +01:00
1 parent d294b3463b
commit 3f87e68a2c
26 files changed
+6022

No files matched your search

+159
View File
@@ -228,3 +228,162 @@ CREATE TABLE IF NOT EXISTS ticket_commands (
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
processed_at TIMESTAMPTZ
);
-- Security: anti-raid and anti-nuke (mirror of velox-dashboard-api migration 000017_security)
CREATE TABLE IF NOT EXISTS security_settings (
guild_id BIGINT PRIMARY KEY,
-- Where alerts go, and who gets pinged for raids and nukes.
alert_channel_id BIGINT,
alert_role_id BIGINT,
-- Join Gate: checks run on every join, each with its own action.
joingate_enabled BOOLEAN NOT NULL DEFAULT FALSE,
joingate_no_avatar TEXT NOT NULL DEFAULT 'off' CHECK (joingate_no_avatar IN ('off', 'timeout', 'kick', 'ban')),
joingate_new_account TEXT NOT NULL DEFAULT 'off' CHECK (joingate_new_account IN ('off', 'timeout', 'kick', 'ban')),
joingate_min_account_age_days INT NOT NULL DEFAULT 7 CHECK (joingate_min_account_age_days BETWEEN 1 AND 365),
joingate_advertising_name TEXT NOT NULL DEFAULT 'off' CHECK (joingate_advertising_name IN ('off', 'timeout', 'kick', 'ban')),
joingate_username_filter TEXT NOT NULL DEFAULT 'off' CHECK (joingate_username_filter IN ('off', 'timeout', 'kick', 'ban')),
joingate_username_patterns TEXT[] NOT NULL DEFAULT '{}',
joingate_unverified_bot TEXT NOT NULL DEFAULT 'off' CHECK (joingate_unverified_bot IN ('off', 'kick', 'ban')),
-- A bot added by someone who isn't the owner or trusted.
joingate_unauthorized_bot TEXT NOT NULL DEFAULT 'kick' CHECK (joingate_unauthorized_bot IN ('off', 'kick', 'ban')),
-- Raid mode: raid_join_count joins within raid_join_seconds starts it; it
-- ends after raid_quiet_minutes without a join, or when staff end it.
antiraid_enabled BOOLEAN NOT NULL DEFAULT FALSE,
raid_join_count INT NOT NULL DEFAULT 10 CHECK (raid_join_count BETWEEN 3 AND 100),
raid_join_seconds INT NOT NULL DEFAULT 10 CHECK (raid_join_seconds BETWEEN 5 AND 300),
raid_action TEXT NOT NULL DEFAULT 'kick' CHECK (raid_action IN ('alert', 'timeout', 'kick', 'ban')),
raid_pause_invites BOOLEAN NOT NULL DEFAULT TRUE,
raid_quiet_minutes INT NOT NULL DEFAULT 10 CHECK (raid_quiet_minutes BETWEEN 1 AND 120),
-- Anti-nuke: anyone who isn't the owner or trusted and goes over a limit
-- within antinuke_window_seconds is quarantined. Limits are per action
-- (channel_delete, channel_create, role_delete, role_create, ban, kick,
-- webhook_create); a limit of 0 means the action is never allowed.
antinuke_enabled BOOLEAN NOT NULL DEFAULT FALSE,
antinuke_window_seconds INT NOT NULL DEFAULT 10 CHECK (antinuke_window_seconds BETWEEN 5 AND 300),
antinuke_limits JSONB NOT NULL DEFAULT '{"channel_delete": 2, "channel_create": 5, "role_delete": 2, "role_create": 5, "ban": 3, "kick": 5, "webhook_create": 3}',
-- Also lock every channel when a nuke is stopped.
antinuke_lock_server BOOLEAN NOT NULL DEFAULT FALSE,
quarantine_role_id BIGINT,
-- Restore: snapshots of roles and channels are taken every few hours; a
-- restore recreates what's missing. Automatic after a stopped nuke when
-- antinuke_auto_restore is on, otherwise by the owner or trusted.
antinuke_auto_restore BOOLEAN NOT NULL DEFAULT FALSE,
-- Heat: every message adds heat to its author (more for repeats,
-- mentions, emoji walls, links, blocked words; an invite link maxes it),
-- and heat cools down over time. Reaching the maximum deletes the message
-- and applies heat_action. heat_strictness scales how fast heat builds.
heat_enabled BOOLEAN NOT NULL DEFAULT FALSE,
heat_strictness TEXT NOT NULL DEFAULT 'medium' CHECK (heat_strictness IN ('low', 'medium', 'high')),
heat_action TEXT NOT NULL DEFAULT 'timeout' CHECK (heat_action IN ('timeout', 'kick', 'ban')),
heat_timeout_minutes INT NOT NULL DEFAULT 10 CHECK (heat_timeout_minutes BETWEEN 1 AND 10080),
-- Each new offence within a day doubles the timeout (capped at 28 days).
heat_escalate BOOLEAN NOT NULL DEFAULT TRUE,
heat_blocked_words TEXT[] NOT NULL DEFAULT '{}',
-- Heat panic: this many members maxing out within a minute times out
-- anyone who joined in the last day as soon as they speak, for 10
-- minutes. 0 turns it off.
heat_panic_members INT NOT NULL DEFAULT 3 CHECK (heat_panic_members BETWEEN 0 AND 50),
-- Mention flood: this many mentions by non-exempt members within 30
-- seconds locks every channel. 0 turns it off.
heat_lockdown_mentions INT NOT NULL DEFAULT 0 CHECK (heat_lockdown_mentions BETWEEN 0 AND 1000),
-- Join lockdown (/lockdown joins): every new member is kicked or banned.
join_lock TEXT NOT NULL DEFAULT 'off' CHECK (join_lock IN ('off', 'kick', 'ban'))
);
-- Users and roles exempt from anti-nuke, who may also manage its settings.
-- The server owner is always trusted and never listed here.
CREATE TABLE IF NOT EXISTS security_trusted (
guild_id BIGINT NOT NULL,
target_id BIGINT NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('user', 'role')),
added_by BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (guild_id, target_id)
);
-- Members currently quarantined, with the roles they had, so releasing them
-- gives every role back.
CREATE TABLE IF NOT EXISTS security_quarantines (
guild_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
previous_role_ids BIGINT[] NOT NULL DEFAULT '{}',
reason TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (guild_id, user_id)
);
-- Channels the bot locked, with @everyone's permission overwrite from before,
-- so unlocking puts back exactly what was there.
CREATE TABLE IF NOT EXISTS security_lockdowns (
guild_id BIGINT NOT NULL,
channel_id BIGINT NOT NULL,
had_overwrite BOOLEAN NOT NULL,
previous_allow BIGINT NOT NULL DEFAULT 0,
previous_deny BIGINT NOT NULL DEFAULT 0,
server_wide BOOLEAN NOT NULL DEFAULT FALSE,
locked_by BIGINT NOT NULL,
locked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (guild_id, channel_id)
);
-- A raid in progress, so raid mode survives a bot restart: the quiet timer
-- carries on and the summary still lists everyone caught.
CREATE TABLE IF NOT EXISTS security_raids (
guild_id BIGINT PRIMARY KEY,
started_at TIMESTAMPTZ NOT NULL,
last_join_at TIMESTAMPTZ NOT NULL,
invites_paused BOOLEAN NOT NULL DEFAULT FALSE,
caught_total INT NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS security_raid_caught (
guild_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
action TEXT NOT NULL,
name TEXT NOT NULL,
caught_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (guild_id, user_id)
);
-- Snapshots of a guild's roles and channels for restoring after a nuke. The
-- bot keeps the last three per guild.
CREATE TABLE IF NOT EXISTS security_snapshots (
id BIGSERIAL PRIMARY KEY,
guild_id BIGINT NOT NULL,
data JSONB NOT NULL,
roles INT NOT NULL,
channels INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS security_snapshots_guild_idx ON security_snapshots (guild_id, created_at DESC);
-- What the system did, for the dashboard's history.
CREATE TABLE IF NOT EXISTS security_events (
id BIGSERIAL PRIMARY KEY,
guild_id BIGINT NOT NULL,
kind TEXT NOT NULL,
actor_id BIGINT,
target_id BIGINT,
detail TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS security_events_guild_idx ON security_events (guild_id, created_at DESC);
-- Dashboard -> bot requests, same pattern as music_commands.
CREATE TABLE IF NOT EXISTS security_commands (
id BIGSERIAL PRIMARY KEY,
guild_id BIGINT NOT NULL,
command_type TEXT NOT NULL CHECK (command_type IN ('end_raid', 'release_quarantine', 'unlock_server', 'unlock_joins', 'snapshot', 'restore')),
payload JSONB,
requested_by BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
processed_at TIMESTAMPTZ
);