"""Add terminal_command_logs table for command history Revision ID: 0014 Revises: 0013_add_terminal_sessions_table Create Date: 2024-12-18 This migration creates the terminal_command_logs table to store validated terminal commands for history and audit purposes. """ from alembic import op import sqlalchemy as sa from sqlalchemy import inspect # revision identifiers, used by Alembic. revision = '0014' down_revision = '0013' branch_labels = None depends_on = None def table_exists(table_name: str) -> bool: """Check if a table exists in the database.""" bind = op.get_bind() inspector = inspect(bind) return table_name in inspector.get_table_names() def index_exists(index_name: str, table_name: str) -> bool: """Check if an index exists on a table.""" bind = op.get_bind() inspector = inspect(bind) indexes = inspector.get_indexes(table_name) return any(idx['name'] == index_name for idx in indexes) def upgrade() -> None: # Check if table already exists (idempotent migration) if table_exists('terminal_command_logs'): print("Table 'terminal_command_logs' already exists, skipping creation.") else: op.create_table( 'terminal_command_logs', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column('host_id', sa.String(), nullable=False), sa.Column('user_id', sa.String(), nullable=True), sa.Column('terminal_session_id', sa.String(64), nullable=True), sa.Column('command', sa.Text(), nullable=False), sa.Column('command_hash', sa.String(64), nullable=False), sa.Column('source', sa.String(20), server_default='terminal', nullable=False), sa.Column('is_blocked', sa.Boolean(), server_default='0', nullable=False), sa.Column('blocked_reason', sa.String(255), nullable=True), sa.Column('username', sa.String(100), nullable=True), sa.Column('host_name', sa.String(100), nullable=True), sa.PrimaryKeyConstraint('id'), sa.ForeignKeyConstraint(['host_id'], ['hosts.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'), ) # Create indexes for efficient querying (only if they don't exist) if not index_exists('ix_terminal_command_logs_created_at', 'terminal_command_logs'): op.create_index('ix_terminal_command_logs_created_at', 'terminal_command_logs', ['created_at']) if not index_exists('ix_terminal_command_logs_host_id', 'terminal_command_logs'): op.create_index('ix_terminal_command_logs_host_id', 'terminal_command_logs', ['host_id']) if not index_exists('ix_terminal_command_logs_user_id', 'terminal_command_logs'): op.create_index('ix_terminal_command_logs_user_id', 'terminal_command_logs', ['user_id']) if not index_exists('ix_terminal_command_logs_terminal_session_id', 'terminal_command_logs'): op.create_index('ix_terminal_command_logs_terminal_session_id', 'terminal_command_logs', ['terminal_session_id']) if not index_exists('ix_terminal_command_logs_command_hash', 'terminal_command_logs'): op.create_index('ix_terminal_command_logs_command_hash', 'terminal_command_logs', ['command_hash']) # Composite indexes for common query patterns if not index_exists('ix_terminal_cmd_host_created', 'terminal_command_logs'): op.create_index('ix_terminal_cmd_host_created', 'terminal_command_logs', ['host_id', 'created_at']) if not index_exists('ix_terminal_cmd_user_created', 'terminal_command_logs'): op.create_index('ix_terminal_cmd_user_created', 'terminal_command_logs', ['user_id', 'created_at']) if not index_exists('ix_terminal_cmd_host_hash', 'terminal_command_logs'): op.create_index('ix_terminal_cmd_host_hash', 'terminal_command_logs', ['host_id', 'command_hash']) def downgrade() -> None: # Drop indexes op.drop_index('ix_terminal_cmd_host_hash', table_name='terminal_command_logs') op.drop_index('ix_terminal_cmd_user_created', table_name='terminal_command_logs') op.drop_index('ix_terminal_cmd_host_created', table_name='terminal_command_logs') op.drop_index('ix_terminal_command_logs_command_hash', table_name='terminal_command_logs') op.drop_index('ix_terminal_command_logs_terminal_session_id', table_name='terminal_command_logs') op.drop_index('ix_terminal_command_logs_user_id', table_name='terminal_command_logs') op.drop_index('ix_terminal_command_logs_host_id', table_name='terminal_command_logs') op.drop_index('ix_terminal_command_logs_created_at', table_name='terminal_command_logs') # Drop table op.drop_table('terminal_command_logs')