Skip to main content

Database Migrations

AuroraSOC uses Alembic for PostgreSQL schema migrations, integrated with SQLAlchemy 2.0 async models.

Setup

Alembic Configuration (alembic.ini)

[alembic]
script_location = alembic
sqlalchemy.url = postgresql+asyncpg://aurora:aurora@localhost:5432/aurorasoc

Migration Environment (alembic/env.py)

from aurorasoc.core.models import Base

target_metadata = Base.metadata

def run_migrations_online():
connectable = create_async_engine(config.get_main_option("sqlalchemy.url"))

async def do_run():
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)

asyncio.run(do_run())

Initial Schema (001_initial_schema.py)

The initial migration creates all 11 tables:

PostgreSQL Extensions

CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- UUID generation
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Trigram similarity for IOC search
note

Migration 004 adds vector (pgvector) and pgcrypto extensions. See below.

Key Indexes

TableIndexTypePurpose
alertsidx_alerts_severityB-treeFilter by severity
alertsidx_alerts_statusB-treeFilter by status
alertsidx_alerts_created_atB-treeTime-range queries
alertsidx_alerts_dedup_hashB-treeDeduplication lookup
casesidx_cases_statusB-treeFilter by status
casesidx_cases_severityB-treeFilter by severity
cps_devicesidx_cps_device_idB-treeDevice lookup
cps_devicesidx_cps_attestation_statusB-treeFilter by attestation
iocsidx_iocs_type_valueB-tree (unique)Dedup + fast lookup
human_approvalsidx_approvals_statusB-treePending approval filter

Auto-Update Trigger

CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';

-- Applied to all tables with updated_at columns
CREATE TRIGGER update_alerts_updated_at
BEFORE UPDATE ON alerts
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

Schema Hardening (004_harden_schema_enums_fks_pgvector.py)

Migration 004 is a comprehensive hardening pass that introduces PostgreSQL enums, foreign keys, CHECK constraints, pgvector, and auto-update triggers across all tables.

New PostgreSQL Extensions

CREATE EXTENSION IF NOT EXISTS "vector"; -- pgvector for embedding storage & ANN search
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid(), cryptographic functions

PostgreSQL Enums

Seven CREATE TYPE ... AS ENUM statements replace unconstrained VARCHAR columns with type-safe enums:

Enum TypeValuesApplied To
severity_enumLOW, MEDIUM, HIGH, CRITICALalerts.severity, cases.severity
alert_status_enumnew, investigating, resolved, escalated, false_positivealerts.status
case_status_enumopen, investigating, resolved, escalated, closedcases.status
approval_status_enumpending, approved, denied, expiredhuman_approvals.status
attestation_status_enumunknown, trusted, untrusted, revokedcps_devices.attestation_status, attestation_results.status
user_role_enumadmin, analyst, viewer, serviceusers.role
execution_status_enumpending, running, completed, failed, cancelledplaybook_executions.status

The migration uses ALTER TABLE ... ALTER COLUMN ... TYPE enum_name USING column::enum_name to convert existing VARCHAR data in-place.

Foreign Key Constraints

ALTER TABLE alerts
ADD CONSTRAINT fk_alerts_case_id FOREIGN KEY (case_id) REFERENCES cases(id);
ALTER TABLE case_timeline
ADD CONSTRAINT fk_timeline_case_id FOREIGN KEY (case_id) REFERENCES cases(id);
ALTER TABLE human_approvals
ADD CONSTRAINT fk_approvals_case_id FOREIGN KEY (case_id) REFERENCES cases(id);
ALTER TABLE playbook_executions
ADD CONSTRAINT fk_executions_case_id FOREIGN KEY (case_id) REFERENCES cases(id);
ALTER TABLE reports
ADD CONSTRAINT fk_reports_case_id FOREIGN KEY (case_id) REFERENCES cases(id);
ALTER TABLE attestation_results
ADD CONSTRAINT fk_attestation_device_id FOREIGN KEY (device_id) REFERENCES cps_devices(id);

CHECK Constraints

ALTER TABLE iocs ADD CONSTRAINT chk_iocs_confidence
CHECK (confidence >= 0.0 AND confidence <= 1.0);
ALTER TABLE cps_devices ADD CONSTRAINT chk_cps_risk_score
CHECK (risk_score >= 0.0 AND risk_score <= 100.0);
ALTER TABLE iocs ADD CONSTRAINT chk_iocs_risk_score
CHECK (risk_score >= 0.0 AND risk_score <= 100.0);

Vector Embeddings Table (pgvector)

CREATE TABLE IF NOT EXISTS vector_embeddings (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
collection VARCHAR(100) NOT NULL,
source_id VARCHAR(200) NOT NULL,
embedding vector(384) NOT NULL,
payload JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_vec_collection_source UNIQUE (collection, source_id)
);

CREATE INDEX IF NOT EXISTS idx_vec_emb_hnsw
ON vector_embeddings
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
  • vector(384): Matches the 384-dimensional output of all-MiniLM-L6-v2
  • HNSW index: m=16 (graph connectivity), ef_construction=200 (build-time search depth)
  • vector_cosine_ops: Enables the <=> cosine distance operator
  • UNIQUE (collection, source_id): Enables upsert via ON CONFLICT

See Database Models — vector_embeddings for usage details.

Auto-Update Triggers

Migration 004 applies updated_at triggers to all tables that have an updated_at column, using the same update_updated_at_column() function from migration 001.

Migration Commands

# Generate auto-migration from model changes
alembic revision --autogenerate -m "Description of change"

# Apply all pending migrations
alembic upgrade head

# Rollback one migration
alembic downgrade -1

# Rollback to specific revision
alembic downgrade 001

# Show current migration state
alembic current

# Show migration history
alembic history --verbose

Creating New Migrations

When you modify SQLAlchemy models in aurorasoc/core/models.py:

  1. Make the model change:
class AlertModel(Base):
# ... existing columns ...
priority: Mapped[int] = mapped_column(Integer, default=3) # NEW
  1. Generate migration:
alembic revision --autogenerate -m "Add priority to alerts"
  1. Review the generated migration (Alembic generates in alembic/versions/):
def upgrade():
op.add_column('alerts', sa.Column('priority', sa.Integer(), nullable=True, default=3))

def downgrade():
op.drop_column('alerts', 'priority')
  1. Apply:
alembic upgrade head

Docker Integration

In Docker Compose, migrations run automatically on API startup:

# aurorasoc/core/database.py
async def init_db():
engine = create_async_engine(settings.postgres.url)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)

For production deployments, use explicit Alembic commands:

# docker-compose.yml
api:
command: >
sh -c "alembic upgrade head && uvicorn aurorasoc.api.main:app --host 0.0.0.0"

JSONB Migration Patterns

JSONB columns don't require migrations for schema changes within the JSON structure. Adding a new key to an IOC's context field just works:

# Before: {"source": "virustotal"}
# After: {"source": "virustotal", "confidence": 0.9}
# No migration needed — JSONB is schema-less

This is why AuroraSOC uses JSONB for flexible fields like iocs, mitre_techniques, metadata, and context.