393 lines
23 KiB
Python
393 lines
23 KiB
Python
"""Initial schema for SneakyScanner
|
|
|
|
Revision ID: 001
|
|
Revises: None
|
|
Create Date: 2025-12-24
|
|
|
|
This is the complete initial schema for SneakyScanner. All tables are created
|
|
in the correct order to satisfy foreign key constraints.
|
|
"""
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
# revision identifiers, used by Alembic
|
|
revision = '001'
|
|
down_revision = None
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade():
|
|
"""Create all tables for SneakyScanner."""
|
|
|
|
# =========================================================================
|
|
# Settings Table (no dependencies)
|
|
# =========================================================================
|
|
op.create_table(
|
|
'settings',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('key', sa.String(length=255), nullable=False, comment='Setting key'),
|
|
sa.Column('value', sa.Text(), nullable=True, comment='Setting value (JSON for complex values)'),
|
|
sa.Column('updated_at', sa.DateTime(), nullable=False, comment='Last modification time'),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('key')
|
|
)
|
|
op.create_index('ix_settings_key', 'settings', ['key'], unique=True)
|
|
|
|
# =========================================================================
|
|
# Reusable Site Definition Tables
|
|
# =========================================================================
|
|
op.create_table(
|
|
'sites',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('name', sa.String(length=255), nullable=False, comment='Unique site name'),
|
|
sa.Column('description', sa.Text(), nullable=True, comment='Site description'),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, comment='Site creation time'),
|
|
sa.Column('updated_at', sa.DateTime(), nullable=False, comment='Last modification time'),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('name')
|
|
)
|
|
op.create_index('ix_sites_name', 'sites', ['name'], unique=True)
|
|
|
|
op.create_table(
|
|
'site_ips',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('site_id', sa.Integer(), nullable=False, comment='FK to sites'),
|
|
sa.Column('ip_address', sa.String(length=45), nullable=False, comment='IPv4 or IPv6 address'),
|
|
sa.Column('expected_ping', sa.Boolean(), nullable=True, comment='Expected ping response'),
|
|
sa.Column('expected_tcp_ports', sa.Text(), nullable=True, comment='JSON array of expected TCP ports'),
|
|
sa.Column('expected_udp_ports', sa.Text(), nullable=True, comment='JSON array of expected UDP ports'),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, comment='IP creation time'),
|
|
sa.ForeignKeyConstraint(['site_id'], ['sites.id']),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('site_id', 'ip_address', name='uix_site_ip_address')
|
|
)
|
|
op.create_index('ix_site_ips_site_id', 'site_ips', ['site_id'])
|
|
|
|
# =========================================================================
|
|
# Scan Configuration Tables
|
|
# =========================================================================
|
|
op.create_table(
|
|
'scan_configs',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('title', sa.String(length=255), nullable=False, comment='Configuration title'),
|
|
sa.Column('description', sa.Text(), nullable=True, comment='Configuration description'),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, comment='Config creation time'),
|
|
sa.Column('updated_at', sa.DateTime(), nullable=False, comment='Last modification time'),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
|
|
op.create_table(
|
|
'scan_config_sites',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('config_id', sa.Integer(), nullable=False),
|
|
sa.Column('site_id', sa.Integer(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, comment='Association creation time'),
|
|
sa.ForeignKeyConstraint(['config_id'], ['scan_configs.id']),
|
|
sa.ForeignKeyConstraint(['site_id'], ['sites.id']),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('config_id', 'site_id', name='uix_config_site')
|
|
)
|
|
op.create_index('ix_scan_config_sites_config_id', 'scan_config_sites', ['config_id'])
|
|
op.create_index('ix_scan_config_sites_site_id', 'scan_config_sites', ['site_id'])
|
|
|
|
# =========================================================================
|
|
# Scheduling Tables
|
|
# =========================================================================
|
|
op.create_table(
|
|
'schedules',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('name', sa.String(length=255), nullable=False, comment='Schedule name'),
|
|
sa.Column('config_id', sa.Integer(), nullable=True, comment='FK to scan_configs table'),
|
|
sa.Column('cron_expression', sa.String(length=100), nullable=False, comment='Cron-like schedule'),
|
|
sa.Column('enabled', sa.Boolean(), nullable=False, default=True, comment='Is schedule active?'),
|
|
sa.Column('last_run', sa.DateTime(), nullable=True, comment='Last execution time'),
|
|
sa.Column('next_run', sa.DateTime(), nullable=True, comment='Next scheduled execution'),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, comment='Schedule creation time'),
|
|
sa.Column('updated_at', sa.DateTime(), nullable=False, comment='Last modification time'),
|
|
sa.ForeignKeyConstraint(['config_id'], ['scan_configs.id']),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_schedules_config_id', 'schedules', ['config_id'])
|
|
|
|
# =========================================================================
|
|
# Core Scan Tables
|
|
# =========================================================================
|
|
op.create_table(
|
|
'scans',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('timestamp', sa.DateTime(), nullable=False, comment='Scan start time (UTC)'),
|
|
sa.Column('duration', sa.Float(), nullable=True, comment='Total scan duration in seconds'),
|
|
sa.Column('status', sa.String(length=20), nullable=False, default='running', comment='running, finalizing, completed, failed, cancelled'),
|
|
sa.Column('config_id', sa.Integer(), nullable=True, comment='FK to scan_configs table'),
|
|
sa.Column('title', sa.Text(), nullable=True, comment='Scan title from config'),
|
|
sa.Column('json_path', sa.Text(), nullable=True, comment='Path to JSON report'),
|
|
sa.Column('html_path', sa.Text(), nullable=True, comment='Path to HTML report'),
|
|
sa.Column('zip_path', sa.Text(), nullable=True, comment='Path to ZIP archive'),
|
|
sa.Column('screenshot_dir', sa.Text(), nullable=True, comment='Path to screenshot directory'),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, comment='Record creation time'),
|
|
sa.Column('triggered_by', sa.String(length=50), nullable=False, default='manual', comment='manual, scheduled, api'),
|
|
sa.Column('schedule_id', sa.Integer(), nullable=True, comment='FK to schedules if triggered by schedule'),
|
|
sa.Column('started_at', sa.DateTime(), nullable=True, comment='Scan execution start time'),
|
|
sa.Column('completed_at', sa.DateTime(), nullable=True, comment='Scan execution completion time'),
|
|
sa.Column('error_message', sa.Text(), nullable=True, comment='Error message if scan failed'),
|
|
sa.Column('current_phase', sa.String(length=50), nullable=True, comment='Current scan phase'),
|
|
sa.Column('total_ips', sa.Integer(), nullable=True, comment='Total number of IPs to scan'),
|
|
sa.Column('completed_ips', sa.Integer(), nullable=True, default=0, comment='Number of IPs completed'),
|
|
sa.ForeignKeyConstraint(['config_id'], ['scan_configs.id']),
|
|
sa.ForeignKeyConstraint(['schedule_id'], ['schedules.id']),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_scans_timestamp', 'scans', ['timestamp'])
|
|
op.create_index('ix_scans_config_id', 'scans', ['config_id'])
|
|
|
|
op.create_table(
|
|
'scan_sites',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('scan_id', sa.Integer(), nullable=False),
|
|
sa.Column('site_name', sa.String(length=255), nullable=False, comment='Site name from config'),
|
|
sa.ForeignKeyConstraint(['scan_id'], ['scans.id']),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_scan_sites_scan_id', 'scan_sites', ['scan_id'])
|
|
|
|
op.create_table(
|
|
'scan_ips',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('scan_id', sa.Integer(), nullable=False),
|
|
sa.Column('site_id', sa.Integer(), nullable=False),
|
|
sa.Column('ip_address', sa.String(length=45), nullable=False, comment='IPv4 or IPv6 address'),
|
|
sa.Column('ping_expected', sa.Boolean(), nullable=True, comment='Expected ping response'),
|
|
sa.Column('ping_actual', sa.Boolean(), nullable=True, comment='Actual ping response'),
|
|
sa.ForeignKeyConstraint(['scan_id'], ['scans.id']),
|
|
sa.ForeignKeyConstraint(['site_id'], ['scan_sites.id']),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('scan_id', 'ip_address', name='uix_scan_ip')
|
|
)
|
|
op.create_index('ix_scan_ips_scan_id', 'scan_ips', ['scan_id'])
|
|
op.create_index('ix_scan_ips_site_id', 'scan_ips', ['site_id'])
|
|
|
|
op.create_table(
|
|
'scan_ports',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('scan_id', sa.Integer(), nullable=False),
|
|
sa.Column('ip_id', sa.Integer(), nullable=False),
|
|
sa.Column('port', sa.Integer(), nullable=False, comment='Port number (1-65535)'),
|
|
sa.Column('protocol', sa.String(length=10), nullable=False, comment='tcp or udp'),
|
|
sa.Column('expected', sa.Boolean(), nullable=True, comment='Was this port expected?'),
|
|
sa.Column('state', sa.String(length=20), nullable=False, default='open', comment='open, closed, filtered'),
|
|
sa.ForeignKeyConstraint(['scan_id'], ['scans.id']),
|
|
sa.ForeignKeyConstraint(['ip_id'], ['scan_ips.id']),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('scan_id', 'ip_id', 'port', 'protocol', name='uix_scan_ip_port')
|
|
)
|
|
op.create_index('ix_scan_ports_scan_id', 'scan_ports', ['scan_id'])
|
|
op.create_index('ix_scan_ports_ip_id', 'scan_ports', ['ip_id'])
|
|
|
|
op.create_table(
|
|
'scan_services',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('scan_id', sa.Integer(), nullable=False),
|
|
sa.Column('port_id', sa.Integer(), nullable=False),
|
|
sa.Column('service_name', sa.String(length=100), nullable=True, comment='Service name'),
|
|
sa.Column('product', sa.String(length=255), nullable=True, comment='Product name'),
|
|
sa.Column('version', sa.String(length=100), nullable=True, comment='Version string'),
|
|
sa.Column('extrainfo', sa.Text(), nullable=True, comment='Additional nmap info'),
|
|
sa.Column('ostype', sa.String(length=100), nullable=True, comment='OS type if detected'),
|
|
sa.Column('http_protocol', sa.String(length=10), nullable=True, comment='http or https'),
|
|
sa.Column('screenshot_path', sa.Text(), nullable=True, comment='Relative path to screenshot'),
|
|
sa.ForeignKeyConstraint(['scan_id'], ['scans.id']),
|
|
sa.ForeignKeyConstraint(['port_id'], ['scan_ports.id']),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_scan_services_scan_id', 'scan_services', ['scan_id'])
|
|
op.create_index('ix_scan_services_port_id', 'scan_services', ['port_id'])
|
|
|
|
op.create_table(
|
|
'scan_certificates',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('scan_id', sa.Integer(), nullable=False),
|
|
sa.Column('service_id', sa.Integer(), nullable=False),
|
|
sa.Column('subject', sa.Text(), nullable=True, comment='Certificate subject (CN)'),
|
|
sa.Column('issuer', sa.Text(), nullable=True, comment='Certificate issuer'),
|
|
sa.Column('serial_number', sa.Text(), nullable=True, comment='Serial number'),
|
|
sa.Column('not_valid_before', sa.DateTime(), nullable=True, comment='Validity start date'),
|
|
sa.Column('not_valid_after', sa.DateTime(), nullable=True, comment='Validity end date'),
|
|
sa.Column('days_until_expiry', sa.Integer(), nullable=True, comment='Days until expiration'),
|
|
sa.Column('sans', sa.Text(), nullable=True, comment='JSON array of SANs'),
|
|
sa.Column('is_self_signed', sa.Boolean(), nullable=True, default=False, comment='Self-signed flag'),
|
|
sa.ForeignKeyConstraint(['scan_id'], ['scans.id']),
|
|
sa.ForeignKeyConstraint(['service_id'], ['scan_services.id']),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_scan_certificates_scan_id', 'scan_certificates', ['scan_id'])
|
|
op.create_index('ix_scan_certificates_service_id', 'scan_certificates', ['service_id'])
|
|
|
|
op.create_table(
|
|
'scan_tls_versions',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('scan_id', sa.Integer(), nullable=False),
|
|
sa.Column('certificate_id', sa.Integer(), nullable=False),
|
|
sa.Column('tls_version', sa.String(length=20), nullable=False, comment='TLS 1.0, 1.1, 1.2, 1.3'),
|
|
sa.Column('supported', sa.Boolean(), nullable=False, comment='Is this version supported?'),
|
|
sa.Column('cipher_suites', sa.Text(), nullable=True, comment='JSON array of cipher suites'),
|
|
sa.ForeignKeyConstraint(['scan_id'], ['scans.id']),
|
|
sa.ForeignKeyConstraint(['certificate_id'], ['scan_certificates.id']),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_scan_tls_versions_scan_id', 'scan_tls_versions', ['scan_id'])
|
|
op.create_index('ix_scan_tls_versions_certificate_id', 'scan_tls_versions', ['certificate_id'])
|
|
|
|
op.create_table(
|
|
'scan_progress',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('scan_id', sa.Integer(), nullable=False),
|
|
sa.Column('ip_address', sa.String(length=45), nullable=False, comment='IP address being scanned'),
|
|
sa.Column('site_name', sa.String(length=255), nullable=True, comment='Site name'),
|
|
sa.Column('phase', sa.String(length=50), nullable=False, comment='Phase: ping, tcp_scan, etc.'),
|
|
sa.Column('status', sa.String(length=20), nullable=False, default='pending', comment='pending, in_progress, completed, failed'),
|
|
sa.Column('ping_result', sa.Boolean(), nullable=True, comment='Ping response result'),
|
|
sa.Column('tcp_ports', sa.Text(), nullable=True, comment='JSON array of TCP ports'),
|
|
sa.Column('udp_ports', sa.Text(), nullable=True, comment='JSON array of UDP ports'),
|
|
sa.Column('services', sa.Text(), nullable=True, comment='JSON array of services'),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, comment='Entry creation time'),
|
|
sa.Column('updated_at', sa.DateTime(), nullable=False, comment='Last update time'),
|
|
sa.ForeignKeyConstraint(['scan_id'], ['scans.id']),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('scan_id', 'ip_address', name='uix_scan_progress_ip')
|
|
)
|
|
op.create_index('ix_scan_progress_scan_id', 'scan_progress', ['scan_id'])
|
|
|
|
op.create_table(
|
|
'scan_site_associations',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('scan_id', sa.Integer(), nullable=False),
|
|
sa.Column('site_id', sa.Integer(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, comment='Association creation time'),
|
|
sa.ForeignKeyConstraint(['scan_id'], ['scans.id']),
|
|
sa.ForeignKeyConstraint(['site_id'], ['sites.id']),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('scan_id', 'site_id', name='uix_scan_site')
|
|
)
|
|
op.create_index('ix_scan_site_associations_scan_id', 'scan_site_associations', ['scan_id'])
|
|
op.create_index('ix_scan_site_associations_site_id', 'scan_site_associations', ['site_id'])
|
|
|
|
# =========================================================================
|
|
# Alert Tables
|
|
# =========================================================================
|
|
op.create_table(
|
|
'alert_rules',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('name', sa.String(length=255), nullable=True, comment='User-friendly rule name'),
|
|
sa.Column('rule_type', sa.String(length=50), nullable=False, comment='unexpected_port, cert_expiry, etc.'),
|
|
sa.Column('enabled', sa.Boolean(), nullable=False, default=True, comment='Is rule active?'),
|
|
sa.Column('threshold', sa.Integer(), nullable=True, comment='Threshold value'),
|
|
sa.Column('email_enabled', sa.Boolean(), nullable=False, default=False, comment='Send email?'),
|
|
sa.Column('webhook_enabled', sa.Boolean(), nullable=False, default=False, comment='Send webhook?'),
|
|
sa.Column('severity', sa.String(length=20), nullable=True, comment='critical, warning, info'),
|
|
sa.Column('filter_conditions', sa.Text(), nullable=True, comment='JSON filter conditions'),
|
|
sa.Column('config_id', sa.Integer(), nullable=True, comment='Optional: specific config this rule applies to'),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, comment='Rule creation time'),
|
|
sa.Column('updated_at', sa.DateTime(), nullable=True, comment='Last update time'),
|
|
sa.ForeignKeyConstraint(['config_id'], ['scan_configs.id']),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_alert_rules_config_id', 'alert_rules', ['config_id'])
|
|
|
|
op.create_table(
|
|
'alerts',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('scan_id', sa.Integer(), nullable=False),
|
|
sa.Column('rule_id', sa.Integer(), nullable=True, comment='Associated alert rule'),
|
|
sa.Column('alert_type', sa.String(length=50), nullable=False, comment='Alert type'),
|
|
sa.Column('severity', sa.String(length=20), nullable=False, comment='info, warning, critical'),
|
|
sa.Column('message', sa.Text(), nullable=False, comment='Human-readable message'),
|
|
sa.Column('ip_address', sa.String(length=45), nullable=True, comment='Related IP'),
|
|
sa.Column('port', sa.Integer(), nullable=True, comment='Related port'),
|
|
sa.Column('email_sent', sa.Boolean(), nullable=False, default=False, comment='Was email sent?'),
|
|
sa.Column('email_sent_at', sa.DateTime(), nullable=True, comment='Email send timestamp'),
|
|
sa.Column('webhook_sent', sa.Boolean(), nullable=False, default=False, comment='Was webhook sent?'),
|
|
sa.Column('webhook_sent_at', sa.DateTime(), nullable=True, comment='Webhook send timestamp'),
|
|
sa.Column('acknowledged', sa.Boolean(), nullable=False, default=False, comment='Was alert acknowledged?'),
|
|
sa.Column('acknowledged_at', sa.DateTime(), nullable=True, comment='Acknowledgment timestamp'),
|
|
sa.Column('acknowledged_by', sa.String(length=255), nullable=True, comment='User who acknowledged'),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, comment='Alert creation time'),
|
|
sa.ForeignKeyConstraint(['scan_id'], ['scans.id']),
|
|
sa.ForeignKeyConstraint(['rule_id'], ['alert_rules.id']),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_alerts_scan_id', 'alerts', ['scan_id'])
|
|
op.create_index('ix_alerts_rule_id', 'alerts', ['rule_id'])
|
|
op.create_index('ix_alerts_acknowledged', 'alerts', ['acknowledged'])
|
|
|
|
# =========================================================================
|
|
# Webhook Tables
|
|
# =========================================================================
|
|
op.create_table(
|
|
'webhooks',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('name', sa.String(length=255), nullable=False, comment='Webhook name'),
|
|
sa.Column('url', sa.Text(), nullable=False, comment='Webhook URL'),
|
|
sa.Column('enabled', sa.Boolean(), nullable=False, default=True, comment='Is webhook enabled?'),
|
|
sa.Column('auth_type', sa.String(length=20), nullable=True, comment='none, bearer, basic, custom'),
|
|
sa.Column('auth_token', sa.Text(), nullable=True, comment='Encrypted auth token'),
|
|
sa.Column('custom_headers', sa.Text(), nullable=True, comment='JSON custom headers'),
|
|
sa.Column('alert_types', sa.Text(), nullable=True, comment='JSON array of alert types'),
|
|
sa.Column('severity_filter', sa.Text(), nullable=True, comment='JSON array of severities'),
|
|
sa.Column('timeout', sa.Integer(), nullable=True, default=10, comment='Request timeout'),
|
|
sa.Column('retry_count', sa.Integer(), nullable=True, default=3, comment='Retry attempts'),
|
|
sa.Column('template', sa.Text(), nullable=True, comment='Jinja2 template for payload'),
|
|
sa.Column('template_format', sa.String(length=20), nullable=True, default='json', comment='json, text'),
|
|
sa.Column('content_type_override', sa.String(length=100), nullable=True, comment='Custom Content-Type'),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, comment='Creation time'),
|
|
sa.Column('updated_at', sa.DateTime(), nullable=False, comment='Last update time'),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
|
|
op.create_table(
|
|
'webhook_delivery_log',
|
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
sa.Column('webhook_id', sa.Integer(), nullable=False, comment='Associated webhook'),
|
|
sa.Column('alert_id', sa.Integer(), nullable=False, comment='Associated alert'),
|
|
sa.Column('status', sa.String(length=20), nullable=True, comment='success, failed, retrying'),
|
|
sa.Column('response_code', sa.Integer(), nullable=True, comment='HTTP response code'),
|
|
sa.Column('response_body', sa.Text(), nullable=True, comment='Response body'),
|
|
sa.Column('error_message', sa.Text(), nullable=True, comment='Error message if failed'),
|
|
sa.Column('attempt_number', sa.Integer(), nullable=True, comment='Which attempt'),
|
|
sa.Column('delivered_at', sa.DateTime(), nullable=False, comment='Delivery timestamp'),
|
|
sa.ForeignKeyConstraint(['webhook_id'], ['webhooks.id']),
|
|
sa.ForeignKeyConstraint(['alert_id'], ['alerts.id']),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_webhook_delivery_log_webhook_id', 'webhook_delivery_log', ['webhook_id'])
|
|
op.create_index('ix_webhook_delivery_log_alert_id', 'webhook_delivery_log', ['alert_id'])
|
|
op.create_index('ix_webhook_delivery_log_status', 'webhook_delivery_log', ['status'])
|
|
|
|
print("\n✓ Initial schema created successfully")
|
|
|
|
|
|
def downgrade():
|
|
"""Drop all tables in reverse order."""
|
|
op.drop_table('webhook_delivery_log')
|
|
op.drop_table('webhooks')
|
|
op.drop_table('alerts')
|
|
op.drop_table('alert_rules')
|
|
op.drop_table('scan_site_associations')
|
|
op.drop_table('scan_progress')
|
|
op.drop_table('scan_tls_versions')
|
|
op.drop_table('scan_certificates')
|
|
op.drop_table('scan_services')
|
|
op.drop_table('scan_ports')
|
|
op.drop_table('scan_ips')
|
|
op.drop_table('scan_sites')
|
|
op.drop_table('scans')
|
|
op.drop_table('schedules')
|
|
op.drop_table('scan_config_sites')
|
|
op.drop_table('scan_configs')
|
|
op.drop_table('site_ips')
|
|
op.drop_table('sites')
|
|
op.drop_table('settings')
|
|
|
|
print("\n✓ All tables dropped")
|