dc-securex 2.29.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dc_securex-2.29.7.dist-info/METADATA +1028 -0
- dc_securex-2.29.7.dist-info/RECORD +28 -0
- dc_securex-2.29.7.dist-info/WHEEL +5 -0
- dc_securex-2.29.7.dist-info/licenses/LICENSE +21 -0
- dc_securex-2.29.7.dist-info/top_level.txt +2 -0
- securex/__init__.py +18 -0
- securex/backup/__init__.py +5 -0
- securex/backup/manager.py +792 -0
- securex/client.py +329 -0
- securex/handlers/__init__.py +8 -0
- securex/handlers/channel.py +97 -0
- securex/handlers/member.py +110 -0
- securex/handlers/role.py +74 -0
- securex/models.py +156 -0
- securex/utils/__init__.py +5 -0
- securex/utils/punishment.py +124 -0
- securex/utils/whitelist.py +129 -0
- securex/workers/__init__.py +8 -0
- securex/workers/action_worker.py +115 -0
- securex/workers/cleanup_worker.py +94 -0
- securex/workers/guild_worker.py +186 -0
- securex/workers/log_worker.py +88 -0
- tests/__init__.py +9 -0
- tests/test_coverage_client.py +210 -0
- tests/test_coverage_handlers.py +341 -0
- tests/test_coverage_manager.py +504 -0
- tests/test_coverage_utils.py +177 -0
- tests/test_coverage_workers.py +297 -0
securex/client.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SecureX SDK - Main Client
|
|
3
|
+
Backend-only anti-nuke protection system
|
|
4
|
+
"""
|
|
5
|
+
import discord
|
|
6
|
+
import asyncio
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Callable, Optional
|
|
9
|
+
from .backup.manager import BackupManager
|
|
10
|
+
from .handlers.channel import ChannelHandler
|
|
11
|
+
from .handlers.role import RoleHandler
|
|
12
|
+
from .handlers.member import MemberHandler
|
|
13
|
+
from .utils.whitelist import WhitelistManager
|
|
14
|
+
from .utils.punishment import PunishmentExecutor
|
|
15
|
+
from .models import ThreatEvent, BackupInfo
|
|
16
|
+
from .workers import ActionWorker, CleanupWorker, LogWorker, GuildWorker
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SecureX:
|
|
20
|
+
"""
|
|
21
|
+
Main SecureX SDK class for anti-nuke protection.
|
|
22
|
+
Backend-only - no UI, no commands, just pure protection logic.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
bot: discord.Client,
|
|
28
|
+
backup_dir: str = "./data/backups",
|
|
29
|
+
punishments: Optional[Dict[str, str]] = None,
|
|
30
|
+
timeout_duration: int = 600,
|
|
31
|
+
notify_user: bool = True
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize SecureX SDK.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
bot: Your discord.Client or commands.Bot instance
|
|
38
|
+
backup_dir: Directory to store backups (default: ./data/backups)
|
|
39
|
+
punishments: Dict mapping violation types to punishment actions
|
|
40
|
+
timeout_duration: Duration in seconds for timeout punishment (default: 600)
|
|
41
|
+
notify_user: Whether to DM violators about punishment (default: True)
|
|
42
|
+
"""
|
|
43
|
+
self.bot = bot
|
|
44
|
+
self.backup_dir = Path(backup_dir)
|
|
45
|
+
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
self.backup_manager = BackupManager(self)
|
|
49
|
+
self.whitelist = WhitelistManager(self)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
self._callbacks: Dict[str, List[Callable]] = {
|
|
53
|
+
'threat_detected': [],
|
|
54
|
+
'backup_completed': [],
|
|
55
|
+
'restore_completed': [],
|
|
56
|
+
'whitelist_changed': [],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
self.punishments = {
|
|
61
|
+
|
|
62
|
+
"bot_add": "none",
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
"channel_create": "none",
|
|
66
|
+
"channel_delete": "none",
|
|
67
|
+
"channel_update": "none",
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
"role_create": "none",
|
|
71
|
+
"role_delete": "none",
|
|
72
|
+
"role_update": "none",
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
"member_ban": "none",
|
|
76
|
+
"member_kick": "none",
|
|
77
|
+
"member_unban": "none",
|
|
78
|
+
"member_update": "none",
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
"webhook_create": "none",
|
|
82
|
+
"webhook_delete": "none",
|
|
83
|
+
"webhook_update": "none",
|
|
84
|
+
|
|
85
|
+
"guild_update": "none"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
self.punisher = PunishmentExecutor(bot)
|
|
90
|
+
self.timeout_duration = timeout_duration
|
|
91
|
+
self.notify_punished_user = notify_user
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
self.action_queue = asyncio.Queue(maxsize=1000)
|
|
95
|
+
self.log_queue = asyncio.Queue(maxsize=1000)
|
|
96
|
+
self.cleanup_queue = asyncio.Queue(maxsize=1000)
|
|
97
|
+
self.guild_queue = asyncio.Queue(maxsize=1000)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
self.whitelist_cache = {}
|
|
101
|
+
self.punishment_cache = {}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
self._worker_tasks = []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
self.channel_handler = ChannelHandler(self)
|
|
108
|
+
self.role_handler = RoleHandler(self)
|
|
109
|
+
self.member_handler = MemberHandler(self)
|
|
110
|
+
|
|
111
|
+
# Initialize workers
|
|
112
|
+
self.action_worker = ActionWorker(self)
|
|
113
|
+
self.cleanup_worker = CleanupWorker(self)
|
|
114
|
+
self.log_worker = LogWorker(self)
|
|
115
|
+
self.guild_worker = GuildWorker(self)
|
|
116
|
+
|
|
117
|
+
self._register_audit_log_listener()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
self._register_events()
|
|
121
|
+
|
|
122
|
+
def _register_audit_log_listener(self):
|
|
123
|
+
"""Register INSTANT audit log event listener (v2.0 - 5-10ms response)"""
|
|
124
|
+
@self.bot.event
|
|
125
|
+
async def on_audit_log_entry_create(entry: discord.AuditLogEntry):
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
|
|
129
|
+
await self.action_queue.put(entry)
|
|
130
|
+
await self.log_queue.put(entry)
|
|
131
|
+
await self.cleanup_queue.put(entry)
|
|
132
|
+
await self.guild_queue.put(entry)
|
|
133
|
+
except asyncio.QueueFull as e:
|
|
134
|
+
print(f"⚠️ Queue full: {e}")
|
|
135
|
+
|
|
136
|
+
def _register_events(self):
|
|
137
|
+
"""Register Discord event listeners for restorations"""
|
|
138
|
+
|
|
139
|
+
@self.bot.event
|
|
140
|
+
async def on_guild_channel_delete(channel):
|
|
141
|
+
await self.channel_handler.on_channel_delete(channel)
|
|
142
|
+
|
|
143
|
+
@self.bot.event
|
|
144
|
+
async def on_guild_channel_update(before, after):
|
|
145
|
+
await self.channel_handler.on_channel_update(before, after)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@self.bot.event
|
|
149
|
+
async def on_guild_role_delete(role):
|
|
150
|
+
await self.role_handler.on_role_delete(role)
|
|
151
|
+
|
|
152
|
+
@self.bot.event
|
|
153
|
+
async def on_guild_role_update(before, after):
|
|
154
|
+
await self.role_handler.on_role_update(before, after)
|
|
155
|
+
|
|
156
|
+
@self.bot.event
|
|
157
|
+
async def on_member_ban(guild, user):
|
|
158
|
+
await self.member_handler.on_member_ban(guild, user)
|
|
159
|
+
|
|
160
|
+
@self.bot.event
|
|
161
|
+
async def on_member_update(before, after):
|
|
162
|
+
await self.member_handler.on_member_update(before, after)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def on(self, event_name: str):
|
|
166
|
+
"""
|
|
167
|
+
Decorator to register callbacks for SDK events.
|
|
168
|
+
|
|
169
|
+
Usage:
|
|
170
|
+
@sx.on('threat_detected')
|
|
171
|
+
async def handle_threat(event: ThreatEvent):
|
|
172
|
+
print(f"Threat: {event.type}")
|
|
173
|
+
"""
|
|
174
|
+
def decorator(func: Callable):
|
|
175
|
+
if event_name in self._callbacks:
|
|
176
|
+
self._callbacks[event_name].append(func)
|
|
177
|
+
return func
|
|
178
|
+
return decorator
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def on_threat_detected(self):
|
|
182
|
+
"""Convenience decorator for threat_detected events"""
|
|
183
|
+
return self.on('threat_detected')
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def on_backup_completed(self):
|
|
187
|
+
"""Convenience decorator for backup_completed events"""
|
|
188
|
+
return self.on('backup_completed')
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def on_restore_completed(self):
|
|
192
|
+
"""Convenience decorator for restore_completed events"""
|
|
193
|
+
return self.on('restore_completed')
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def on_whitelist_changed(self):
|
|
197
|
+
"""Convenience decorator for whitelist_changed events"""
|
|
198
|
+
return self.on('whitelist_changed')
|
|
199
|
+
|
|
200
|
+
async def _trigger_callbacks(self, event_name: str, *args, **kwargs):
|
|
201
|
+
"""Trigger all registered callbacks for an event"""
|
|
202
|
+
if event_name in self._callbacks:
|
|
203
|
+
for callback in self._callbacks[event_name]:
|
|
204
|
+
try:
|
|
205
|
+
if asyncio.iscoroutinefunction(callback):
|
|
206
|
+
await callback(*args, **kwargs)
|
|
207
|
+
else:
|
|
208
|
+
callback(*args, **kwargs)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
print(f"Error in callback for {event_name}: {e}")
|
|
211
|
+
|
|
212
|
+
async def enable(
|
|
213
|
+
self,
|
|
214
|
+
guild_id: Optional[int] = None,
|
|
215
|
+
whitelist: Optional[List[int]] = None,
|
|
216
|
+
auto_backup: bool = True,
|
|
217
|
+
punishments: Optional[Dict[str, str]] = None,
|
|
218
|
+
timeout_duration: int = 600,
|
|
219
|
+
notify_user: bool = True
|
|
220
|
+
):
|
|
221
|
+
"""
|
|
222
|
+
Enable SecureX protection.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
guild_id: Guild ID to enable protection for (required if using whitelist)
|
|
226
|
+
whitelist: List of user IDs to whitelist (default: [])
|
|
227
|
+
auto_backup: Enable automatic backups every 30 minutes (default: True)
|
|
228
|
+
role_position_monitoring: Enable role position monitoring (default: True)
|
|
229
|
+
punishments: Dict mapping violation types to punishment actions
|
|
230
|
+
Example: {"channel_delete": "ban", "role_create": "kick"}
|
|
231
|
+
Available actions: "none", "warn", "timeout", "kick", "ban"
|
|
232
|
+
timeout_duration: Duration in seconds for timeout punishment (default: 600)
|
|
233
|
+
notify_user: Whether to DM violators about punishment (default: True)
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
await self.whitelist.preload_all()
|
|
237
|
+
await self.backup_manager.preload_all()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if guild_id:
|
|
241
|
+
whitelist_data = await self.whitelist.get_all(guild_id)
|
|
242
|
+
self.whitelist_cache[guild_id] = set(whitelist_data)
|
|
243
|
+
print(f"📋 Loaded {len(whitelist_data)} whitelisted users into cache")
|
|
244
|
+
|
|
245
|
+
if whitelist and guild_id:
|
|
246
|
+
for user_id in whitelist:
|
|
247
|
+
await self.whitelist.add(guild_id, user_id)
|
|
248
|
+
|
|
249
|
+
self.whitelist_cache[guild_id] = self.whitelist_cache.get(guild_id, set()) | set(whitelist)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if punishments:
|
|
253
|
+
self.punishments.update(punishments)
|
|
254
|
+
|
|
255
|
+
if guild_id:
|
|
256
|
+
self.punishment_cache[guild_id] = self.punishments.copy()
|
|
257
|
+
print(f"⚡ Punishment cache populated for guild {guild_id}")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
await self.action_worker.start()
|
|
261
|
+
await self.cleanup_worker.start()
|
|
262
|
+
await self.log_worker.start()
|
|
263
|
+
await self.guild_worker.start()
|
|
264
|
+
print("🚀 Started 4 independent workers (Action, Cleanup, Log, Guild)")
|
|
265
|
+
print("⚡ Broadcast architecture - zero dependencies between workers")
|
|
266
|
+
|
|
267
|
+
if auto_backup:
|
|
268
|
+
|
|
269
|
+
self.backup_manager.start_auto_backup()
|
|
270
|
+
|
|
271
|
+
self.backup_manager.start_auto_refresh()
|
|
272
|
+
|
|
273
|
+
self.timeout_duration = timeout_duration
|
|
274
|
+
self.notify_punished_user = notify_user
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if punishments:
|
|
278
|
+
enabled_punishments = {k: v for k, v in self.punishments.items() if v != "none"}
|
|
279
|
+
if enabled_punishments:
|
|
280
|
+
print(f"⚠️ Punishments configured for {len(enabled_punishments)} violation types")
|
|
281
|
+
|
|
282
|
+
print("✅ SecureX SDK v2.0 - Ultra-fast audit log system ACTIVE")
|
|
283
|
+
print("⚡ Response time: 5-10ms (100x faster than v1.x)")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if guild_id:
|
|
287
|
+
backup = await self.backup_manager.create_backup(guild_id)
|
|
288
|
+
if backup.success:
|
|
289
|
+
print(f"✅ Backup completed for guild {guild_id}: "
|
|
290
|
+
f"{backup.channel_count} channels, {backup.role_count} roles")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
async def create_backup(self, guild_id: int) -> BackupInfo:
|
|
294
|
+
"""
|
|
295
|
+
Create a backup for the specified guild.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
guild_id: The guild ID to backup
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
BackupInfo object with backup results
|
|
302
|
+
"""
|
|
303
|
+
return await self.backup_manager.create_backup(guild_id)
|
|
304
|
+
|
|
305
|
+
async def restore_channel(self, guild: discord.Guild, channel: discord.abc.GuildChannel):
|
|
306
|
+
"""
|
|
307
|
+
Restore a deleted channel from backup.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
guild: The guild object
|
|
311
|
+
channel: The deleted channel object
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
The newly created channel or None if restoration failed
|
|
315
|
+
"""
|
|
316
|
+
return await self.backup_manager.restore_channel(guild, channel)
|
|
317
|
+
|
|
318
|
+
async def restore_role(self, guild: discord.Guild, role_id: int) -> bool:
|
|
319
|
+
"""
|
|
320
|
+
Restore a deleted role from backup.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
guild: The guild object
|
|
324
|
+
role_id: The ID of the deleted role
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
True if successful, False otherwise
|
|
328
|
+
"""
|
|
329
|
+
return await self.backup_manager.restore_role(guild, role_id)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Channel protection handler for SecureX SDK.
|
|
3
|
+
"""
|
|
4
|
+
import discord
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ChannelHandler:
|
|
10
|
+
"""Handles channel protection logic"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, sdk):
|
|
13
|
+
self.sdk = sdk
|
|
14
|
+
self.bot = sdk.bot
|
|
15
|
+
self.backup_manager = sdk.backup_manager
|
|
16
|
+
self.whitelist = sdk.whitelist
|
|
17
|
+
|
|
18
|
+
async def _is_authorized(self, guild: discord.Guild, user_id: int) -> bool:
|
|
19
|
+
"""Check if user is authorized (owner or whitelisted)"""
|
|
20
|
+
if guild.owner_id == user_id:
|
|
21
|
+
return True
|
|
22
|
+
if user_id == self.bot.user.id:
|
|
23
|
+
return True
|
|
24
|
+
return await self.whitelist.is_whitelisted(guild.id, user_id)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def on_channel_delete(self, channel: discord.abc.GuildChannel):
|
|
28
|
+
"""Restore unauthorized channel deletions"""
|
|
29
|
+
try:
|
|
30
|
+
guild = channel.guild
|
|
31
|
+
print(f"🔍 [Debug] Channel deleted: {channel.name} ({channel.id})")
|
|
32
|
+
await asyncio.sleep(1)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
from datetime import timezone
|
|
36
|
+
cutoff_time = datetime.now(timezone.utc) - timedelta(seconds=30)
|
|
37
|
+
|
|
38
|
+
found_entry = False
|
|
39
|
+
async for entry in guild.audit_logs(limit=50, action=discord.AuditLogAction.channel_delete):
|
|
40
|
+
if entry.created_at < cutoff_time:
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
if entry.target.id == channel.id:
|
|
44
|
+
found_entry = True
|
|
45
|
+
authorized = await self._is_authorized(guild, entry.user.id)
|
|
46
|
+
print(f"👤 [Debug] Executor: {entry.user.name} ({entry.user.id}) | Authorized: {authorized}")
|
|
47
|
+
|
|
48
|
+
if not authorized:
|
|
49
|
+
print(f"🛠️ [Debug] Unauthorized deletion detected. Triggering restoration...")
|
|
50
|
+
|
|
51
|
+
new_channel = await self.backup_manager.restore_channel(guild, channel)
|
|
52
|
+
if new_channel:
|
|
53
|
+
print(f"🔄 Restored unauthorized channel deletion: {channel.name}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if new_channel and isinstance(channel, discord.CategoryChannel):
|
|
57
|
+
await self.backup_manager.restore_category_children(
|
|
58
|
+
guild,
|
|
59
|
+
channel.id,
|
|
60
|
+
new_channel
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
print(f"✅ [Debug] Deletion by authorized user. Skipping restoration.")
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
if not found_entry:
|
|
67
|
+
print(f"⚠️ [Debug] Could not find audit log entry for channel deletion of {channel.name}")
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
print(f"❌ [Debug] Error in on_channel_delete: {e}")
|
|
71
|
+
|
|
72
|
+
async def on_channel_update(self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel):
|
|
73
|
+
"""Restore unauthorized channel permission changes"""
|
|
74
|
+
try:
|
|
75
|
+
|
|
76
|
+
if (before.overwrites == after.overwrites and
|
|
77
|
+
before.position == after.position):
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
guild = after.guild
|
|
81
|
+
await asyncio.sleep(0.5)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
from datetime import timezone
|
|
85
|
+
cutoff_time = datetime.now(timezone.utc) - timedelta(seconds=30)
|
|
86
|
+
|
|
87
|
+
async for entry in guild.audit_logs(limit=50, oldest_first=False):
|
|
88
|
+
if entry.created_at < cutoff_time:
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
if entry.action == discord.AuditLogAction.channel_update:
|
|
92
|
+
if entry.target and entry.target.id == after.id:
|
|
93
|
+
if not await self._is_authorized(guild, entry.user.id):
|
|
94
|
+
await self.backup_manager.restore_channel_permissions(guild, after.id)
|
|
95
|
+
break
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Member protection handler (bot/ban/kick protection).
|
|
3
|
+
"""
|
|
4
|
+
import discord
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MemberHandler:
|
|
9
|
+
"""Handles member-related protection"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DANGEROUS_PERMISSIONS = frozenset([
|
|
13
|
+
'administrator',
|
|
14
|
+
'kick_members',
|
|
15
|
+
'ban_members',
|
|
16
|
+
'manage_guild',
|
|
17
|
+
'manage_roles',
|
|
18
|
+
'manage_channels',
|
|
19
|
+
'manage_webhooks',
|
|
20
|
+
'manage_emojis',
|
|
21
|
+
'mention_everyone',
|
|
22
|
+
'manage_expressions'
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
def __init__(self, sdk):
|
|
26
|
+
self.sdk = sdk
|
|
27
|
+
self.bot = sdk.bot
|
|
28
|
+
self.whitelist = sdk.whitelist
|
|
29
|
+
|
|
30
|
+
async def _is_authorized(self, guild: discord.Guild, user_id: int) -> bool:
|
|
31
|
+
"""Check if user is authorized"""
|
|
32
|
+
if guild.owner_id == user_id:
|
|
33
|
+
return True
|
|
34
|
+
if user_id == self.bot.user.id:
|
|
35
|
+
return True
|
|
36
|
+
return await self.whitelist.is_whitelisted(guild.id, user_id)
|
|
37
|
+
|
|
38
|
+
async def on_member_ban(self, guild: discord.Guild, user: discord.User):
|
|
39
|
+
"""Restore banned victims (punishment handled by action worker)"""
|
|
40
|
+
try:
|
|
41
|
+
await asyncio.sleep(0.5)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
from datetime import datetime, timezone, timedelta
|
|
45
|
+
cutoff_time = datetime.now(timezone.utc) - timedelta(seconds=30)
|
|
46
|
+
|
|
47
|
+
async for entry in guild.audit_logs(limit=50, action=discord.AuditLogAction.ban):
|
|
48
|
+
if entry.created_at < cutoff_time:
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
if entry.target.id == user.id:
|
|
52
|
+
if not await self._is_authorized(guild, entry.user.id):
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
await guild.unban(user, reason="SecureX: Restoring banned member")
|
|
56
|
+
print(f"🔄 Unbanned unauthorized member ban: {user.name}")
|
|
57
|
+
except (discord.errors.Forbidden, discord.errors.HTTPException):
|
|
58
|
+
pass
|
|
59
|
+
break
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"Error in on_member_ban: {e}")
|
|
62
|
+
|
|
63
|
+
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
|
64
|
+
"""Check for dangerous permissions and remove unauthorized roles"""
|
|
65
|
+
try:
|
|
66
|
+
|
|
67
|
+
if before.roles == after.roles:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
guild = after.guild
|
|
71
|
+
await asyncio.sleep(0.5)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
from datetime import datetime, timezone, timedelta
|
|
75
|
+
cutoff_time = datetime.now(timezone.utc) - timedelta(seconds=30)
|
|
76
|
+
|
|
77
|
+
async for entry in guild.audit_logs(limit=50, action=discord.AuditLogAction.member_role_update):
|
|
78
|
+
if entry.created_at < cutoff_time:
|
|
79
|
+
break
|
|
80
|
+
if entry.target.id == after.id:
|
|
81
|
+
|
|
82
|
+
is_authorized = await self._is_authorized(guild, entry.user.id)
|
|
83
|
+
|
|
84
|
+
if not is_authorized:
|
|
85
|
+
|
|
86
|
+
dangerous_roles = []
|
|
87
|
+
|
|
88
|
+
for role in after.roles:
|
|
89
|
+
if role.is_default():
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
permissions = role.permissions
|
|
94
|
+
for perm_name in self.DANGEROUS_PERMISSIONS:
|
|
95
|
+
if getattr(permissions, perm_name, False):
|
|
96
|
+
dangerous_roles.append(role)
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if dangerous_roles:
|
|
101
|
+
try:
|
|
102
|
+
await after.remove_roles(*dangerous_roles, reason="SecureX: Removed dangerous roles (unauthorized update)")
|
|
103
|
+
role_names = ", ".join([r.name for r in dangerous_roles])
|
|
104
|
+
print(f"🛡️ Bulk removed {len(dangerous_roles)} dangerous role(s) from {after.name}: {role_names}")
|
|
105
|
+
except (discord.errors.Forbidden, discord.errors.HTTPException) as e:
|
|
106
|
+
print(f"⚠️ Failed to remove roles: {e}")
|
|
107
|
+
|
|
108
|
+
break
|
|
109
|
+
except Exception as e:
|
|
110
|
+
print(f"Error in on_member_update: {e}")
|
securex/handlers/role.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Role protection handler for SecureX SDK.
|
|
3
|
+
"""
|
|
4
|
+
import discord
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RoleHandler:
|
|
10
|
+
"""Handles role protection logic"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, sdk):
|
|
13
|
+
self.sdk = sdk
|
|
14
|
+
self.bot = sdk.bot
|
|
15
|
+
self.backup_manager = sdk.backup_manager
|
|
16
|
+
self.whitelist = sdk.whitelist
|
|
17
|
+
|
|
18
|
+
async def _is_authorized(self, guild: discord.Guild, user_id: int) -> bool:
|
|
19
|
+
"""Check if user is authorized"""
|
|
20
|
+
if guild.owner_id == user_id:
|
|
21
|
+
return True
|
|
22
|
+
if user_id == self.bot.user.id:
|
|
23
|
+
return True
|
|
24
|
+
return await self.whitelist.is_whitelisted(guild.id, user_id)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def on_role_delete(self, role: discord.Role):
|
|
28
|
+
"""Restore unauthorized role deletions"""
|
|
29
|
+
try:
|
|
30
|
+
guild = role.guild
|
|
31
|
+
await asyncio.sleep(1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
from datetime import timezone
|
|
35
|
+
cutoff_time = datetime.now(timezone.utc) - timedelta(seconds=30)
|
|
36
|
+
|
|
37
|
+
async for entry in guild.audit_logs(limit=50, action=discord.AuditLogAction.role_delete):
|
|
38
|
+
if entry.created_at < cutoff_time:
|
|
39
|
+
break
|
|
40
|
+
|
|
41
|
+
if entry.target.id == role.id:
|
|
42
|
+
if not await self._is_authorized(guild, entry.user.id):
|
|
43
|
+
if await self.backup_manager.restore_role(guild, role.id):
|
|
44
|
+
print(f"🔄 Restored unauthorized role deletion: {role.name}")
|
|
45
|
+
break
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
async def on_role_update(self, before: discord.Role, after: discord.Role):
|
|
50
|
+
"""Restore unauthorized role permission changes"""
|
|
51
|
+
try:
|
|
52
|
+
|
|
53
|
+
if before.position == after.position and before.permissions == after.permissions:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
guild = after.guild
|
|
57
|
+
await asyncio.sleep(0.3)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
from datetime import timezone
|
|
61
|
+
cutoff_time = datetime.now(timezone.utc) - timedelta(seconds=30)
|
|
62
|
+
|
|
63
|
+
async for entry in guild.audit_logs(limit=50, oldest_first=False):
|
|
64
|
+
if entry.created_at < cutoff_time:
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
if entry.action == discord.AuditLogAction.role_update:
|
|
68
|
+
if entry.target.id == after.id:
|
|
69
|
+
if not await self._is_authorized(guild, entry.user.id):
|
|
70
|
+
if await self.backup_manager.restore_role_permissions(guild, after.id):
|
|
71
|
+
print(f"🔄 Restored unauthorized role update: {after.name}")
|
|
72
|
+
break
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|