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.
securex/models.py ADDED
@@ -0,0 +1,156 @@
1
+ """
2
+ Data models for SecureX SDK.
3
+ These objects are returned to developers - no UI included.
4
+ """
5
+ from dataclasses import dataclass, asdict, field
6
+ from datetime import datetime, timezone
7
+ from typing import Optional, List, Dict
8
+ import json
9
+
10
+
11
+ @dataclass
12
+ class ThreatEvent:
13
+ """
14
+ Represents a detected security threat.
15
+
16
+ Attributes:
17
+ type: Type of threat (e.g., "channel_delete", "role_create")
18
+ guild_id: ID of the guild where threat occurred
19
+ actor_id: ID of user who performed the action
20
+ target_id: ID of the affected resource
21
+ target_name: Name of the affected resource
22
+ prevented: Whether the action was prevented
23
+ restored: Whether restoration was successful
24
+ timestamp: When the threat was detected
25
+ details: Additional context about the threat
26
+ """
27
+ type: str
28
+ guild_id: int
29
+ actor_id: int
30
+ target_id: int
31
+ target_name: str
32
+ prevented: bool
33
+ restored: bool
34
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
35
+ details: Dict = field(default_factory=dict)
36
+ punishment_action: Optional[str] = None
37
+
38
+ def to_dict(self) -> dict:
39
+ """Convert to dictionary for serialization"""
40
+ data = asdict(self)
41
+ data['timestamp'] = self.timestamp.isoformat()
42
+ return data
43
+
44
+ def to_json(self) -> str:
45
+ """Convert to JSON string"""
46
+ return json.dumps(self.to_dict())
47
+
48
+
49
+ @dataclass
50
+ class BackupInfo:
51
+ """
52
+ Information about a server backup.
53
+
54
+ Attributes:
55
+ guild_id: ID of the backed up guild
56
+ timestamp: When backup was created
57
+ channel_count: Number of channels backed up
58
+ role_count: Number of roles backed up
59
+ backup_path: Path to backup file
60
+ success: Whether backup completed successfully
61
+ """
62
+ guild_id: int
63
+ timestamp: datetime
64
+ channel_count: int
65
+ role_count: int
66
+ backup_path: str
67
+ success: bool = True
68
+
69
+ def to_dict(self) -> dict:
70
+ """Convert to dictionary"""
71
+ data = asdict(self)
72
+ data['timestamp'] = self.timestamp.isoformat()
73
+ return data
74
+
75
+
76
+ @dataclass
77
+ class RestoreResult:
78
+ """
79
+ Result of a restoration operation.
80
+
81
+ Attributes:
82
+ success: Overall success status
83
+ items_restored: Number of items successfully restored
84
+ items_failed: Number of items that failed to restore
85
+ errors: List of error messages
86
+ duration: Time taken in seconds
87
+ details: Additional restoration details
88
+ """
89
+ success: bool
90
+ items_restored: int
91
+ items_failed: int
92
+ errors: List[str] = field(default_factory=list)
93
+ duration: float = 0.0
94
+ details: Dict = field(default_factory=dict)
95
+
96
+ def to_dict(self) -> dict:
97
+ """Convert to dictionary"""
98
+ return asdict(self)
99
+
100
+
101
+ @dataclass
102
+ class WhitelistChange:
103
+ """
104
+ Represents a whitelist modification.
105
+
106
+ Attributes:
107
+ guild_id: Guild where change occurred
108
+ user_id: User added/removed from whitelist
109
+ action: "added" or "removed"
110
+ timestamp: When the change occurred
111
+ moderator_id: Who made the change
112
+ """
113
+ guild_id: int
114
+ user_id: int
115
+ action: str
116
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
117
+ moderator_id: Optional[int] = None
118
+
119
+ def to_dict(self) -> dict:
120
+ """Convert to dictionary"""
121
+ data = asdict(self)
122
+ data['timestamp'] = self.timestamp.isoformat()
123
+ return data
124
+
125
+
126
+ @dataclass
127
+ class UserToken:
128
+ """
129
+ Represents a user token for guild-level API operations.
130
+
131
+ Attributes:
132
+ guild_id: Guild this token is associated with
133
+ token: The user authentication token
134
+ set_by: User ID who set this token
135
+ timestamp: When the token was added/updated
136
+ last_used: When the token was last used (optional)
137
+ description: Optional description/note about the token
138
+ """
139
+ guild_id: int
140
+ token: str
141
+ set_by: Optional[int] = None
142
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
143
+ last_used: Optional[datetime] = None
144
+ description: Optional[str] = None
145
+
146
+ def to_dict(self) -> dict:
147
+ """Convert to dictionary"""
148
+ data = asdict(self)
149
+ data['timestamp'] = self.timestamp.isoformat()
150
+ if self.last_used:
151
+ data['last_used'] = self.last_used.isoformat()
152
+ return data
153
+
154
+ def mark_used(self):
155
+ """Mark this token as used (updates last_used timestamp)"""
156
+ self.last_used = datetime.now(timezone.utc)
@@ -0,0 +1,5 @@
1
+ """Utils package"""
2
+
3
+ from .whitelist import WhitelistManager
4
+
5
+ __all__ = ['WhitelistManager']
@@ -0,0 +1,124 @@
1
+ """
2
+ Punishment executor for anti-nuke violations.
3
+ Handles kick, ban, timeout, and warn actions.
4
+ """
5
+ import discord
6
+ from datetime import timedelta
7
+ from typing import Optional
8
+
9
+
10
+ class PunishmentExecutor:
11
+ """Executes punishment actions on violators"""
12
+
13
+ def __init__(self, bot):
14
+ """Initialize with bot instance"""
15
+ self.bot = bot
16
+
17
+ async def punish(
18
+ self,
19
+ guild: discord.Guild,
20
+ user: discord.User,
21
+ violation_type: str,
22
+ details: str = None,
23
+ sdk = None
24
+ ) -> str:
25
+ """
26
+ Execute punishment on violator based on violation type.
27
+
28
+ Args:
29
+ guild: The guild where violation occurred
30
+ user: The user who committed the violation
31
+ violation_type: Type of violation (e.g., "channel_delete")
32
+ details: Optional details about the violation
33
+ sdk: SDK instance for accessing punishment config
34
+
35
+ Returns:
36
+ The punishment action that was applied ("none", "warn", "timeout", "kick", "ban")
37
+ """
38
+ if sdk is None:
39
+ return "none"
40
+
41
+
42
+ action = sdk.punishments.get(violation_type, "none")
43
+
44
+ if action == "none":
45
+ return action
46
+
47
+ member = guild.get_member(user.id)
48
+ if not member:
49
+ print(f"Cannot punish {user}: not a member")
50
+ return action
51
+
52
+
53
+ if member.id == guild.owner_id:
54
+ print(f"Cannot punish server owner {user}")
55
+ return action
56
+
57
+
58
+
59
+ try:
60
+ if action == "warn":
61
+
62
+ print(f"โš ๏ธ Warned {user} for {violation_type}")
63
+
64
+ elif action == "timeout":
65
+ duration = timedelta(seconds=sdk.timeout_duration)
66
+ await member.timeout(duration, reason=f"SecureX: {violation_type}")
67
+ print(f"โฑ๏ธ Timed out {user} for {sdk.timeout_duration}s ({violation_type})")
68
+
69
+ elif action == "kick":
70
+ await member.kick(reason=f"SecureX: {violation_type}")
71
+ print(f"๐Ÿ‘ข Kicked {user} for {violation_type}")
72
+
73
+ elif action == "ban":
74
+ await member.ban(reason=f"SecureX: {violation_type}", delete_message_days=0)
75
+ print(f"๐Ÿ”จ Banned {user} for {violation_type}")
76
+
77
+ if sdk.notify_punished_user:
78
+ await self._notify_user(member, violation_type, action, details, sdk)
79
+
80
+
81
+ except discord.Forbidden:
82
+ print(f"โŒ Missing permissions to {action} {user}")
83
+ except Exception as e:
84
+ print(f"Error punishing {user}: {e}")
85
+
86
+ return action
87
+
88
+ async def _notify_user(
89
+ self,
90
+ member: discord.Member,
91
+ violation_type: str,
92
+ action: str,
93
+ details: Optional[str],
94
+ sdk
95
+ ):
96
+ """Send DM notification to punished user"""
97
+ try:
98
+
99
+ readable_type = violation_type.replace("_", " ").title()
100
+
101
+ embed = discord.Embed(
102
+ title="๐Ÿšจ Anti-Nuke Violation Detected",
103
+ description=f"You triggered anti-nuke protection in **{member.guild.name}**",
104
+ color=discord.Color.red()
105
+ )
106
+ embed.add_field(name="Violation Type", value=readable_type, inline=False)
107
+ if details:
108
+ embed.add_field(name="Details", value=details, inline=False)
109
+ embed.add_field(
110
+ name="Action Taken",
111
+ value=f"**{action.title()}**",
112
+ inline=False
113
+ )
114
+
115
+ if action == "timeout":
116
+ mins = sdk.timeout_duration // 60
117
+ embed.add_field(name="Duration", value=f"{mins} minutes", inline=False)
118
+
119
+ await member.send(embed=embed)
120
+ print(f"๐Ÿ“จ Sent punishment notification to {member}")
121
+ except discord.Forbidden:
122
+ print(f"Cannot DM {member} (DMs disabled)")
123
+ except Exception as e:
124
+ print(f"Error notifying {member}: {e}")
@@ -0,0 +1,129 @@
1
+ """
2
+ Whitelist management for SecureX SDK.
3
+ Strictly per-guild - no cross-server whitelisting.
4
+ """
5
+ import json
6
+ import aiofiles
7
+ from pathlib import Path
8
+ from typing import List, Set
9
+ from ..models import WhitelistChange
10
+
11
+
12
+ class WhitelistManager:
13
+ """Manages whitelisted users per guild (guild-specific only)"""
14
+
15
+ def __init__(self, sdk):
16
+ self.sdk = sdk
17
+ self.data_dir = Path("./data/whitelists")
18
+ self.data_dir.mkdir(parents=True, exist_ok=True)
19
+
20
+
21
+ self._whitelists: dict[int, Set[int]] = {}
22
+ self._cache_loaded = False
23
+
24
+ def _get_file_path(self, guild_id: int) -> Path:
25
+ """Get whitelist file path for guild"""
26
+ return self.data_dir / f"{guild_id}.json"
27
+
28
+ async def preload_all(self):
29
+ """
30
+ Preload ALL whitelists into memory on startup.
31
+ Call this in enable() to warm the cache.
32
+ """
33
+ if self._cache_loaded:
34
+ return
35
+
36
+ print("๐Ÿ”„ Preloading whitelists into cache...")
37
+ count = 0
38
+
39
+
40
+ for file_path in self.data_dir.glob("*.json"):
41
+ try:
42
+ guild_id = int(file_path.stem)
43
+ async with aiofiles.open(file_path, 'r') as f:
44
+ content = await f.read()
45
+ data = json.loads(content)
46
+ self._whitelists[guild_id] = set(data.get('users', []))
47
+ count += 1
48
+ except Exception as e:
49
+ print(f"Error loading whitelist {file_path}: {e}")
50
+
51
+ self._cache_loaded = True
52
+ print(f"โœ… Cached {count} whitelists in memory")
53
+
54
+ async def _load_whitelist(self, guild_id: int):
55
+ """Load whitelist from file (fallback if not preloaded)"""
56
+ file_path = self._get_file_path(guild_id)
57
+ if file_path.exists():
58
+ async with aiofiles.open(file_path, 'r') as f:
59
+ content = await f.read()
60
+ data = json.loads(content)
61
+ self._whitelists[guild_id] = set(data.get('users', []))
62
+
63
+ async def _save_whitelist(self, guild_id: int):
64
+ """Save whitelist to file (async)"""
65
+ file_path = self._get_file_path(guild_id)
66
+ async with aiofiles.open(file_path, 'w') as f:
67
+ content = json.dumps({
68
+ 'users': list(self._whitelists.get(guild_id, set()))
69
+ }, indent=2)
70
+ await f.write(content)
71
+
72
+ async def add(self, guild_id: int, user_id: int, moderator_id: int = None):
73
+ """Add user to guild whitelist"""
74
+ if guild_id not in self._whitelists:
75
+ await self._load_whitelist(guild_id)
76
+ if guild_id not in self._whitelists:
77
+ self._whitelists[guild_id] = set()
78
+
79
+ self._whitelists[guild_id].add(user_id)
80
+ await self._save_whitelist(guild_id)
81
+
82
+
83
+ change = WhitelistChange(
84
+ guild_id=guild_id,
85
+ user_id=user_id,
86
+ action="added",
87
+ moderator_id=moderator_id
88
+ )
89
+ await self.sdk._trigger_callbacks('whitelist_changed', change)
90
+
91
+ async def remove(self, guild_id: int, user_id: int, moderator_id: int = None):
92
+ """Remove user from guild whitelist"""
93
+ if guild_id not in self._whitelists:
94
+ await self._load_whitelist(guild_id)
95
+
96
+ if guild_id in self._whitelists:
97
+ self._whitelists[guild_id].discard(user_id)
98
+ await self._save_whitelist(guild_id)
99
+
100
+
101
+ change = WhitelistChange(
102
+ guild_id=guild_id,
103
+ user_id=user_id,
104
+ action="removed",
105
+ moderator_id=moderator_id
106
+ )
107
+ await self.sdk._trigger_callbacks('whitelist_changed', change)
108
+
109
+ async def get_all(self, guild_id: int) -> List[int]:
110
+ """Get all whitelisted users for guild"""
111
+ if guild_id not in self._whitelists:
112
+ await self._load_whitelist(guild_id)
113
+ return list(self._whitelists.get(guild_id, set()))
114
+
115
+ async def is_whitelisted(self, guild_id: int, user_id: int) -> bool:
116
+ """
117
+ Check if user is whitelisted in specific guild.
118
+ Uses cached data - instant response (no file I/O).
119
+ """
120
+ if guild_id not in self._whitelists:
121
+ await self._load_whitelist(guild_id)
122
+
123
+ return user_id in self._whitelists.get(guild_id, set())
124
+
125
+ async def clear(self, guild_id: int):
126
+ """Clear all whitelisted users for a guild"""
127
+ self._whitelists[guild_id] = set()
128
+ await self._save_whitelist(guild_id)
129
+
@@ -0,0 +1,8 @@
1
+ """Workers package - background processing workers"""
2
+
3
+ from .action_worker import ActionWorker
4
+ from .cleanup_worker import CleanupWorker
5
+ from .log_worker import LogWorker
6
+ from .guild_worker import GuildWorker
7
+
8
+ __all__ = ['ActionWorker', 'CleanupWorker', 'LogWorker', 'GuildWorker']
@@ -0,0 +1,115 @@
1
+ """
2
+ Action Worker - Instant punishment for unauthorized actions
3
+ """
4
+ import discord
5
+ import asyncio
6
+
7
+
8
+ class ActionWorker:
9
+ """Worker that bans/punishes violators instantly"""
10
+
11
+ def __init__(self, sdk):
12
+ self.sdk = sdk
13
+ self.bot = sdk.bot
14
+ self.action_queue = asyncio.Queue()
15
+ self._worker_task = None
16
+
17
+ self.action_map = {
18
+ discord.AuditLogAction.bot_add: "bot_add",
19
+ discord.AuditLogAction.channel_create: "channel_create",
20
+ discord.AuditLogAction.channel_delete: "channel_delete",
21
+ discord.AuditLogAction.channel_update: "channel_update",
22
+ discord.AuditLogAction.role_create: "role_create",
23
+ discord.AuditLogAction.role_delete: "role_delete",
24
+ discord.AuditLogAction.role_update: "role_update",
25
+ discord.AuditLogAction.kick: "member_kick",
26
+ discord.AuditLogAction.ban: "member_ban",
27
+ discord.AuditLogAction.unban: "member_unban",
28
+ discord.AuditLogAction.member_update: "member_update",
29
+ discord.AuditLogAction.webhook_create: "webhook_create",
30
+ discord.AuditLogAction.guild_update: "guild_update",
31
+ }
32
+
33
+ async def start(self):
34
+ """Start the action worker"""
35
+ if self._worker_task is None or self._worker_task.done():
36
+ self._worker_task = asyncio.create_task(self._worker_loop())
37
+ print("โšก Action Worker started")
38
+
39
+ async def stop(self):
40
+ """Stop the worker"""
41
+ if self._worker_task:
42
+ self._worker_task.cancel()
43
+ self._worker_task = None
44
+
45
+ async def _worker_loop(self):
46
+ """Main worker loop"""
47
+ while True:
48
+ try:
49
+ entry = await self.action_queue.get()
50
+ await self._process_entry(entry)
51
+ except asyncio.CancelledError:
52
+ break
53
+ except Exception as e:
54
+ print(f"Error in action worker: {e}")
55
+
56
+ async def _process_entry(self, entry):
57
+ """Process audit log entry"""
58
+ print(f"๐Ÿ” [ActionWorker] Processing: {entry.action} by {entry.user.name}")
59
+ try:
60
+ guild = entry.guild
61
+ executor = entry.user
62
+
63
+ if executor.id == self.bot.user.id:
64
+ return
65
+
66
+ if executor.id == guild.owner_id:
67
+ return
68
+
69
+ whitelist_set = self.sdk.whitelist_cache.get(guild.id, set())
70
+ if executor.id in whitelist_set:
71
+ return
72
+
73
+ guild_punishments = self.sdk.punishment_cache.get(guild.id, self.sdk.punishments)
74
+
75
+ violation_type = self.action_map.get(entry.action)
76
+ if not violation_type:
77
+ return
78
+
79
+ punishment_action = guild_punishments.get(violation_type, "none")
80
+ punishment_result = "none"
81
+
82
+ if violation_type == "bot_add":
83
+ try:
84
+ bot_member = guild.get_member(entry.target.id)
85
+ if bot_member:
86
+ await bot_member.ban(reason="SecureX: Unauthorized bot addition")
87
+ punishment_result = "banned_bot"
88
+ print(f"๐Ÿš€ INSTANT: Banned bot {entry.target.name} (added by {executor.name})")
89
+ except Exception as e:
90
+ print(f"Error banning bot: {e}")
91
+
92
+ if violation_type in ["member_kick", "member_ban", "member_unban", "webhook_create", "guild_update", "member_update"]:
93
+ try:
94
+ await guild.ban(executor, reason=f"SecureX: Unauthorized {violation_type}")
95
+ punishment_result = "banned_violator"
96
+ print(f"๐Ÿ”จ INSTANT: Banned {executor.name} for {violation_type}")
97
+ except Exception as e:
98
+ print(f"Error banning violator: {e}")
99
+
100
+
101
+ if punishment_action != "none":
102
+ try:
103
+ punishment_result = await self.sdk.punisher.punish(
104
+ guild=guild,
105
+ user=executor,
106
+ violation_type=violation_type,
107
+ details=f"{violation_type} on {getattr(entry.target, 'name', 'Unknown')}",
108
+ sdk=self.sdk
109
+ )
110
+ print(f"๐Ÿ‘ข {punishment_result.title()} {executor.name} for {violation_type}")
111
+ except Exception as e:
112
+ print(f"Cannot punish {executor.name}: {e}")
113
+
114
+ except Exception as e:
115
+ print(f"Error processing action entry: {e}")
@@ -0,0 +1,94 @@
1
+ """
2
+ Cleanup Worker - Deletes unauthorized creations instantly
3
+ """
4
+ import discord
5
+ import asyncio
6
+
7
+
8
+ class CleanupWorker:
9
+ """Worker that deletes unauthorized creations (channels, roles, webhooks)"""
10
+
11
+ def __init__(self, sdk):
12
+ self.sdk = sdk
13
+ self.bot = sdk.bot
14
+ self.cleanup_queue = asyncio.Queue()
15
+ self._worker_task = None
16
+
17
+ async def start(self):
18
+ """Start the cleanup worker"""
19
+ if self._worker_task is None or self._worker_task.done():
20
+ self._worker_task = asyncio.create_task(self._worker_loop())
21
+ print("๐Ÿงน Cleanup Worker started")
22
+
23
+ async def stop(self):
24
+ """Stop the worker"""
25
+ if self._worker_task:
26
+ self._worker_task.cancel()
27
+ self._worker_task = None
28
+
29
+ async def _worker_loop(self):
30
+ """Main worker loop"""
31
+ while True:
32
+ try:
33
+ entry = await self.cleanup_queue.get()
34
+ await self._process_entry(entry)
35
+ except asyncio.CancelledError:
36
+ break
37
+ except Exception as e:
38
+ print(f"Error in cleanup worker: {e}")
39
+
40
+ async def _process_entry(self, entry):
41
+ """Process deletion of unauthorized creations"""
42
+ try:
43
+ guild = entry.guild
44
+ executor = entry.user
45
+
46
+ if executor.id == self.bot.user.id:
47
+ return
48
+
49
+ if executor.id == guild.owner_id:
50
+ return
51
+
52
+ whitelist_set = self.sdk.whitelist_cache.get(guild.id, set())
53
+ if executor.id in whitelist_set:
54
+ return
55
+
56
+ if entry.action not in [
57
+ discord.AuditLogAction.channel_create,
58
+ discord.AuditLogAction.role_create,
59
+ discord.AuditLogAction.webhook_create
60
+ ]:
61
+ return
62
+
63
+ try:
64
+ target = entry.target
65
+ if not target:
66
+ return
67
+
68
+ if entry.action == discord.AuditLogAction.role_create:
69
+ role = guild.get_role(target.id)
70
+ if role:
71
+ await role.delete(reason="SecureX: Unauthorized role creation")
72
+ print(f"๐Ÿ—‘๏ธ [Worker] Deleted unauthorized role: {role.name}")
73
+
74
+ elif entry.action == discord.AuditLogAction.channel_create:
75
+ channel = guild.get_channel(target.id)
76
+ if channel:
77
+ await channel.delete(reason="SecureX: Unauthorized channel creation")
78
+ print(f"๐Ÿ—‘๏ธ [Worker] Deleted unauthorized channel: {channel.name}")
79
+
80
+ elif entry.action == discord.AuditLogAction.webhook_create:
81
+ webhooks = await guild.webhooks()
82
+ for webhook in webhooks:
83
+ if webhook.id == target.id:
84
+ await webhook.delete(reason="SecureX: Unauthorized webhook creation")
85
+ print(f"๐Ÿ—‘๏ธ [Worker] Deleted unauthorized webhook: {webhook.name}")
86
+ break
87
+
88
+ except (discord.Forbidden, discord.NotFound):
89
+ pass
90
+ except Exception as e:
91
+ print(f"Cleanup error: {e}")
92
+
93
+ except Exception as e:
94
+ print(f"Error processing cleanup entry: {e}")