stage-11: security hardening
- Prompt injection protection: external messages wrapped in [EXTERNAL CONTENT] markers, system prompt instructs Claude to never follow external instructions - Invocation logging: all Claude CLI calls logged with channel, model, duration, token counts to echo-core.invoke logger - Security logging: separate echo-core.security logger for unauthorized access attempts (DMs from non-admins, unauthorized admin/owner commands) - Security log routed to logs/security.log in addition to main log - Extended echo doctor: Claude CLI functional check, config.json secret scan, .gitignore completeness, file permissions, Ollama reachability, bot process - Subprocess env stripping logged at debug level 373 tests pass (10 new security tests). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ from src.claude_session import (
|
||||
from src.router import route_message
|
||||
|
||||
logger = logging.getLogger("echo-core.discord")
|
||||
_security_log = logging.getLogger("echo-core.security")
|
||||
|
||||
# Module-level config reference, set by create_bot()
|
||||
_config: Config | None = None
|
||||
@@ -161,6 +162,7 @@ def create_bot(config: Config) -> discord.Client:
|
||||
interaction: discord.Interaction, alias: str
|
||||
) -> None:
|
||||
if not is_owner(str(interaction.user.id)):
|
||||
_security_log.warning("Unauthorized owner command /channel add by user=%s (%s)", interaction.user.id, interaction.user)
|
||||
await interaction.response.send_message(
|
||||
"Owner only.", ephemeral=True
|
||||
)
|
||||
@@ -186,6 +188,7 @@ def create_bot(config: Config) -> discord.Client:
|
||||
interaction: discord.Interaction, user_id: str
|
||||
) -> None:
|
||||
if not is_owner(str(interaction.user.id)):
|
||||
_security_log.warning("Unauthorized owner command /admin add by user=%s (%s)", interaction.user.id, interaction.user)
|
||||
await interaction.response.send_message(
|
||||
"Owner only.", ephemeral=True
|
||||
)
|
||||
@@ -273,6 +276,7 @@ def create_bot(config: Config) -> discord.Client:
|
||||
model: app_commands.Choice[str] | None = None,
|
||||
) -> None:
|
||||
if not is_admin(str(interaction.user.id)):
|
||||
_security_log.warning("Unauthorized admin command /cron add by user=%s (%s)", interaction.user.id, interaction.user)
|
||||
await interaction.response.send_message(
|
||||
"Admin only.", ephemeral=True
|
||||
)
|
||||
@@ -331,6 +335,7 @@ def create_bot(config: Config) -> discord.Client:
|
||||
@app_commands.describe(name="Job name to remove")
|
||||
async def cron_remove(interaction: discord.Interaction, name: str) -> None:
|
||||
if not is_admin(str(interaction.user.id)):
|
||||
_security_log.warning("Unauthorized admin command /cron remove by user=%s (%s)", interaction.user.id, interaction.user)
|
||||
await interaction.response.send_message(
|
||||
"Admin only.", ephemeral=True
|
||||
)
|
||||
@@ -356,6 +361,7 @@ def create_bot(config: Config) -> discord.Client:
|
||||
interaction: discord.Interaction, name: str
|
||||
) -> None:
|
||||
if not is_admin(str(interaction.user.id)):
|
||||
_security_log.warning("Unauthorized admin command /cron enable by user=%s (%s)", interaction.user.id, interaction.user)
|
||||
await interaction.response.send_message(
|
||||
"Admin only.", ephemeral=True
|
||||
)
|
||||
@@ -381,6 +387,7 @@ def create_bot(config: Config) -> discord.Client:
|
||||
interaction: discord.Interaction, name: str
|
||||
) -> None:
|
||||
if not is_admin(str(interaction.user.id)):
|
||||
_security_log.warning("Unauthorized admin command /cron disable by user=%s (%s)", interaction.user.id, interaction.user)
|
||||
await interaction.response.send_message(
|
||||
"Admin only.", ephemeral=True
|
||||
)
|
||||
@@ -641,6 +648,7 @@ def create_bot(config: Config) -> discord.Client:
|
||||
@tree.command(name="restart", description="Restart the bot process")
|
||||
async def restart(interaction: discord.Interaction) -> None:
|
||||
if not is_owner(str(interaction.user.id)):
|
||||
_security_log.warning("Unauthorized owner command /restart by user=%s (%s)", interaction.user.id, interaction.user)
|
||||
await interaction.response.send_message(
|
||||
"Owner only.", ephemeral=True
|
||||
)
|
||||
@@ -743,6 +751,10 @@ def create_bot(config: Config) -> discord.Client:
|
||||
# DM handling: only process if sender is admin
|
||||
if isinstance(message.channel, discord.DMChannel):
|
||||
if not is_admin(str(message.author.id)):
|
||||
_security_log.warning(
|
||||
"Unauthorized DM from user=%s (%s): %s",
|
||||
message.author.id, message.author, message.content[:100],
|
||||
)
|
||||
return
|
||||
logger.info(
|
||||
"DM from admin %s: %s", message.author, message.content[:100]
|
||||
|
||||
Reference in New Issue
Block a user