23 tables + alembic_version covering the v1 data model:
- Auth/RBAC (8): users, groups, permissions, user_groups, group_permissions,
invitations, invitation_groups, refresh_tokens.
- MITRE (4): mitre_tactics, mitre_techniques, mitre_subtechniques + the
technique↔tactic many-to-many.
- Templates (4): test_templates, test_template_mitre_tags (3 nullable FKs +
CHECK exactly_one_mitre_fk), scenario_templates, scenario_template_tests
(UUID PK + UNIQUE(scenario_id, position) so a test can appear at multiple
positions).
- Missions (6): missions, mission_members, mission_scenarios, mission_tests,
mission_test_mitre_tags (deliberately denormalised — copies external_id +
name + url, no FK to mitre_* — so a re-sync of the catalogue can't purge
historical tags), mission_categories.
- Evidence/settings/notifications (5): evidence_files, settings (JSONB
value), detection_levels, notifications.
SQLAlchemy 2.x with Mapped[]/mapped_column(), pk_/fk_/ck_/uq_/ix_ naming
convention. Reusable mixins (UuidPkMixin, TimestampMixin, SoftDeleteMixin —
no auto __table_args__ since classes silently clobber the mixin's).
Soft delete: deleted_at + partial indexes ix_<table>_active WHERE deleted_at
IS NULL on 9 tables (users, groups, test_templates, scenario_templates,
missions, mission_scenarios, mission_tests, mission_categories,
evidence_files). Notifications gets ix_..._unread WHERE read_at IS NULL.
CHECK constraints for status / state / opsec_level / mitre_kind enums.
New API endpoint GET /api/v1/diag/db: returns alembic_revision (short hash)
and the public-schema table_count. 503 with {"reachable": false} on a DB
outage. Database card on the SPA home consumes it.
Test stage in backend/Dockerfile (--target test): runtime + dev extras +
tests/. New make test-api spins an ephemeral pytest container against the
live DB on the compose network. backend/tests/test_schema.py: 8 integration
tests (tables, FK pairs, CHECK constraints, partial indexes, alembic-at-head,
negative INSERT proving the exactly_one_mitre_fk CHECK fires).
e2e/tests/m1-db.spec.ts: 4 Playwright tests covering the diag endpoint
contract + the Database card + footer/roadmap labels.
DoD: make clean && make up && make migrate → 23 tables, 32 FKs, 9 CHECKs,
make test-api → 9 passed, make e2e → 12 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
447 lines
31 KiB
Python
447 lines
31 KiB
Python
"""initial schema
|
|
|
|
Revision ID: 24765a5014b6
|
|
Revises:
|
|
Create Date: 2026-05-10 10:40:31.816149
|
|
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = '24765a5014b6'
|
|
down_revision: str | None = None
|
|
branch_labels: str | Sequence[str] | None = None
|
|
depends_on: str | Sequence[str] | None = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.create_table('detection_levels',
|
|
sa.Column('key', sa.String(length=40), nullable=False),
|
|
sa.Column('label_fr', sa.String(length=80), nullable=False),
|
|
sa.Column('label_en', sa.String(length=80), nullable=False),
|
|
sa.Column('color_token', sa.String(length=16), nullable=False),
|
|
sa.Column('position', sa.Integer(), nullable=False),
|
|
sa.Column('is_default', sa.Boolean(), nullable=False),
|
|
sa.Column('is_system', sa.Boolean(), nullable=False),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_detection_levels')),
|
|
sa.UniqueConstraint('key', name=op.f('uq_detection_levels_key'))
|
|
)
|
|
op.create_table('groups',
|
|
sa.Column('name', sa.String(length=80), nullable=False),
|
|
sa.Column('description', sa.Text(), nullable=True),
|
|
sa.Column('is_system', sa.Boolean(), nullable=False),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_groups'))
|
|
)
|
|
op.create_index('ix_groups_active', 'groups', ['deleted_at'], unique=False, postgresql_where='deleted_at IS NULL')
|
|
op.create_index('uq_groups_name_active', 'groups', ['name'], unique=True, postgresql_where='deleted_at IS NULL')
|
|
op.create_table('missions',
|
|
sa.Column('name', sa.String(length=255), nullable=False),
|
|
sa.Column('client_target', sa.String(length=255), nullable=True),
|
|
sa.Column('date_start', sa.Date(), nullable=True),
|
|
sa.Column('date_end', sa.Date(), nullable=True),
|
|
sa.Column('status', sa.String(length=16), nullable=False),
|
|
sa.Column('description_md', sa.Text(), nullable=True),
|
|
sa.Column('visibility_mode', sa.String(length=16), nullable=False),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.CheckConstraint("status IN ('draft', 'in_progress', 'completed', 'archived')", name=op.f('ck_missions_status_valid')),
|
|
sa.CheckConstraint("visibility_mode IN ('whitebox', 'titles_only', 'executed_only')", name=op.f('ck_missions_visibility_mode_valid')),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_missions'))
|
|
)
|
|
op.create_index('ix_missions_active', 'missions', ['deleted_at'], unique=False, postgresql_where='deleted_at IS NULL')
|
|
op.create_index('ix_missions_status', 'missions', ['status'], unique=False)
|
|
op.create_table('mitre_tactics',
|
|
sa.Column('external_id', sa.String(length=16), nullable=False),
|
|
sa.Column('short_name', sa.String(length=80), nullable=False),
|
|
sa.Column('name', sa.String(length=120), nullable=False),
|
|
sa.Column('description', sa.Text(), nullable=True),
|
|
sa.Column('url', sa.String(length=512), nullable=True),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_mitre_tactics')),
|
|
sa.UniqueConstraint('external_id', name=op.f('uq_mitre_tactics_external_id'))
|
|
)
|
|
op.create_table('mitre_techniques',
|
|
sa.Column('external_id', sa.String(length=16), nullable=False),
|
|
sa.Column('name', sa.String(length=255), nullable=False),
|
|
sa.Column('description', sa.Text(), nullable=True),
|
|
sa.Column('url', sa.String(length=512), nullable=True),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_mitre_techniques')),
|
|
sa.UniqueConstraint('external_id', name=op.f('uq_mitre_techniques_external_id'))
|
|
)
|
|
op.create_index('ix_mitre_techniques_name', 'mitre_techniques', ['name'], unique=False)
|
|
op.create_table('permissions',
|
|
sa.Column('code', sa.String(length=80), nullable=False),
|
|
sa.Column('description', sa.Text(), nullable=True),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_permissions')),
|
|
sa.UniqueConstraint('code', name=op.f('uq_permissions_code'))
|
|
)
|
|
op.create_table('scenario_templates',
|
|
sa.Column('name', sa.String(length=255), nullable=False),
|
|
sa.Column('description', sa.Text(), nullable=True),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_scenario_templates'))
|
|
)
|
|
op.create_index('ix_scenario_templates_active', 'scenario_templates', ['deleted_at'], unique=False, postgresql_where='deleted_at IS NULL')
|
|
op.create_index('ix_scenario_templates_name', 'scenario_templates', ['name'], unique=False)
|
|
op.create_table('settings',
|
|
sa.Column('key', sa.String(length=80), nullable=False),
|
|
sa.Column('value', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
sa.Column('description', sa.Text(), nullable=True),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.PrimaryKeyConstraint('key', name=op.f('pk_settings'))
|
|
)
|
|
op.create_table('test_templates',
|
|
sa.Column('name', sa.String(length=255), nullable=False),
|
|
sa.Column('description', sa.Text(), nullable=True),
|
|
sa.Column('objective', sa.Text(), nullable=True),
|
|
sa.Column('procedure_md', sa.Text(), nullable=True),
|
|
sa.Column('prerequisites_md', sa.Text(), nullable=True),
|
|
sa.Column('expected_result_red_md', sa.Text(), nullable=True),
|
|
sa.Column('expected_detection_blue_md', sa.Text(), nullable=True),
|
|
sa.Column('opsec_level', sa.String(length=8), nullable=False),
|
|
sa.Column('tags', sa.ARRAY(sa.String(length=64)), server_default='{}', nullable=False),
|
|
sa.Column('expected_iocs', sa.ARRAY(sa.String(length=255)), server_default='{}', nullable=False),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.CheckConstraint("opsec_level IN ('low', 'medium', 'high')", name=op.f('ck_test_templates_opsec_level_valid')),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_test_templates'))
|
|
)
|
|
op.create_index('ix_test_templates_active', 'test_templates', ['deleted_at'], unique=False, postgresql_where='deleted_at IS NULL')
|
|
op.create_index('ix_test_templates_name', 'test_templates', ['name'], unique=False)
|
|
op.create_table('users',
|
|
sa.Column('email', sa.String(length=254), nullable=False),
|
|
sa.Column('password_hash', sa.String(length=255), nullable=False),
|
|
sa.Column('display_name', sa.String(length=120), nullable=True),
|
|
sa.Column('locale', sa.String(length=8), nullable=False),
|
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_users'))
|
|
)
|
|
op.create_index('ix_users_active', 'users', ['deleted_at'], unique=False, postgresql_where='deleted_at IS NULL')
|
|
op.create_index('uq_users_email_active', 'users', ['email'], unique=True, postgresql_where='deleted_at IS NULL')
|
|
op.create_table('group_permissions',
|
|
sa.Column('group_id', sa.Uuid(), nullable=False),
|
|
sa.Column('permission_id', sa.Uuid(), nullable=False),
|
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], name=op.f('fk_group_permissions_group_id_groups'), ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], name=op.f('fk_group_permissions_permission_id_permissions'), ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('group_id', 'permission_id', name=op.f('pk_group_permissions'))
|
|
)
|
|
op.create_table('invitations',
|
|
sa.Column('token_hash', sa.String(length=128), nullable=False),
|
|
sa.Column('email_hint', sa.String(length=254), nullable=True),
|
|
sa.Column('created_by_user_id', sa.Uuid(), nullable=False),
|
|
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('consumed_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column('consumed_by_user_id', sa.Uuid(), nullable=True),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.ForeignKeyConstraint(['consumed_by_user_id'], ['users.id'], name=op.f('fk_invitations_consumed_by_user_id_users'), ondelete='SET NULL'),
|
|
sa.ForeignKeyConstraint(['created_by_user_id'], ['users.id'], name=op.f('fk_invitations_created_by_user_id_users'), ondelete='RESTRICT'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_invitations')),
|
|
sa.UniqueConstraint('token_hash', name=op.f('uq_invitations_token_hash'))
|
|
)
|
|
op.create_index('ix_invitations_expires_at', 'invitations', ['expires_at'], unique=False)
|
|
op.create_table('mission_categories',
|
|
sa.Column('mission_id', sa.Uuid(), nullable=False),
|
|
sa.Column('name', sa.String(length=120), nullable=False),
|
|
sa.Column('color_token', sa.String(length=16), nullable=True),
|
|
sa.Column('position', sa.Integer(), nullable=False),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.ForeignKeyConstraint(['mission_id'], ['missions.id'], name=op.f('fk_mission_categories_mission_id_missions'), ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_mission_categories')),
|
|
sa.UniqueConstraint('mission_id', 'name', name='uq_mission_categories_name'),
|
|
sa.UniqueConstraint('mission_id', 'position', name='uq_mission_categories_position')
|
|
)
|
|
op.create_index('ix_mission_categories_active', 'mission_categories', ['deleted_at'], unique=False, postgresql_where='deleted_at IS NULL')
|
|
op.create_table('mission_members',
|
|
sa.Column('mission_id', sa.Uuid(), nullable=False),
|
|
sa.Column('user_id', sa.Uuid(), nullable=False),
|
|
sa.Column('role_hint', sa.String(length=8), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.CheckConstraint("role_hint IN ('red', 'blue')", name=op.f('ck_mission_members_role_hint_valid')),
|
|
sa.ForeignKeyConstraint(['mission_id'], ['missions.id'], name=op.f('fk_mission_members_mission_id_missions'), ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_mission_members_user_id_users'), ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('mission_id', 'user_id', name=op.f('pk_mission_members'))
|
|
)
|
|
op.create_index('ix_mission_members_user', 'mission_members', ['user_id'], unique=False)
|
|
op.create_table('mission_scenarios',
|
|
sa.Column('mission_id', sa.Uuid(), nullable=False),
|
|
sa.Column('source_scenario_template_id', sa.Uuid(), nullable=True),
|
|
sa.Column('snapshot_name', sa.String(length=255), nullable=False),
|
|
sa.Column('snapshot_description', sa.Text(), nullable=True),
|
|
sa.Column('position', sa.Integer(), nullable=False),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.ForeignKeyConstraint(['mission_id'], ['missions.id'], name=op.f('fk_mission_scenarios_mission_id_missions'), ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['source_scenario_template_id'], ['scenario_templates.id'], name=op.f('fk_mission_scenarios_source_scenario_template_id_scenario_templates'), ondelete='SET NULL'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_mission_scenarios')),
|
|
sa.UniqueConstraint('mission_id', 'position', name='uq_mission_scenarios_position')
|
|
)
|
|
op.create_index('ix_mission_scenarios_active', 'mission_scenarios', ['deleted_at'], unique=False, postgresql_where='deleted_at IS NULL')
|
|
op.create_index('ix_mission_scenarios_mission', 'mission_scenarios', ['mission_id'], unique=False)
|
|
op.create_table('mitre_subtechniques',
|
|
sa.Column('external_id', sa.String(length=16), nullable=False),
|
|
sa.Column('name', sa.String(length=255), nullable=False),
|
|
sa.Column('description', sa.Text(), nullable=True),
|
|
sa.Column('url', sa.String(length=512), nullable=True),
|
|
sa.Column('technique_id', sa.Uuid(), nullable=False),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.ForeignKeyConstraint(['technique_id'], ['mitre_techniques.id'], name=op.f('fk_mitre_subtechniques_technique_id_mitre_techniques'), ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_mitre_subtechniques')),
|
|
sa.UniqueConstraint('external_id', name=op.f('uq_mitre_subtechniques_external_id'))
|
|
)
|
|
op.create_index('ix_mitre_subtechniques_technique_id', 'mitre_subtechniques', ['technique_id'], unique=False)
|
|
op.create_table('mitre_technique_tactics',
|
|
sa.Column('technique_id', sa.Uuid(), nullable=False),
|
|
sa.Column('tactic_id', sa.Uuid(), nullable=False),
|
|
sa.ForeignKeyConstraint(['tactic_id'], ['mitre_tactics.id'], name=op.f('fk_mitre_technique_tactics_tactic_id_mitre_tactics'), ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['technique_id'], ['mitre_techniques.id'], name=op.f('fk_mitre_technique_tactics_technique_id_mitre_techniques'), ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('technique_id', 'tactic_id', name=op.f('pk_mitre_technique_tactics'))
|
|
)
|
|
op.create_table('notifications',
|
|
sa.Column('user_id', sa.Uuid(), nullable=False),
|
|
sa.Column('type', sa.String(length=64), nullable=False),
|
|
sa.Column('payload', postgresql.JSONB(astext_type=sa.Text()), server_default='{}', nullable=False),
|
|
sa.Column('read_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_notifications_user_id_users'), ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_notifications'))
|
|
)
|
|
op.create_index('ix_notifications_user_unread', 'notifications', ['user_id', 'created_at'], unique=False, postgresql_where='read_at IS NULL')
|
|
op.create_table('refresh_tokens',
|
|
sa.Column('user_id', sa.Uuid(), nullable=False),
|
|
sa.Column('jti', sa.String(length=64), nullable=False),
|
|
sa.Column('token_hash', sa.String(length=128), nullable=False),
|
|
sa.Column('issued_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column('replaced_by_id', sa.Uuid(), nullable=True),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.ForeignKeyConstraint(['replaced_by_id'], ['refresh_tokens.id'], name=op.f('fk_refresh_tokens_replaced_by_id_refresh_tokens'), ondelete='SET NULL'),
|
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_refresh_tokens_user_id_users'), ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_refresh_tokens')),
|
|
sa.UniqueConstraint('jti', name='uq_refresh_tokens_jti')
|
|
)
|
|
op.create_index('ix_refresh_tokens_user_id_expires_at', 'refresh_tokens', ['user_id', 'expires_at'], unique=False)
|
|
op.create_table('scenario_template_tests',
|
|
sa.Column('scenario_template_id', sa.Uuid(), nullable=False),
|
|
sa.Column('test_template_id', sa.Uuid(), nullable=False),
|
|
sa.Column('position', sa.Integer(), nullable=False),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.ForeignKeyConstraint(['scenario_template_id'], ['scenario_templates.id'], name=op.f('fk_scenario_template_tests_scenario_template_id_scenario_templates'), ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['test_template_id'], ['test_templates.id'], name=op.f('fk_scenario_template_tests_test_template_id_test_templates'), ondelete='RESTRICT'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_scenario_template_tests')),
|
|
sa.UniqueConstraint('scenario_template_id', 'position', name='uq_scenario_template_tests_position')
|
|
)
|
|
op.create_index('ix_scenario_template_tests_scenario', 'scenario_template_tests', ['scenario_template_id'], unique=False)
|
|
op.create_index('ix_scenario_template_tests_test', 'scenario_template_tests', ['test_template_id'], unique=False)
|
|
op.create_table('user_groups',
|
|
sa.Column('user_id', sa.Uuid(), nullable=False),
|
|
sa.Column('group_id', sa.Uuid(), nullable=False),
|
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], name=op.f('fk_user_groups_group_id_groups'), ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_user_groups_user_id_users'), ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('user_id', 'group_id', name=op.f('pk_user_groups'))
|
|
)
|
|
op.create_table('invitation_groups',
|
|
sa.Column('invitation_id', sa.Uuid(), nullable=False),
|
|
sa.Column('group_id', sa.Uuid(), nullable=False),
|
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], name=op.f('fk_invitation_groups_group_id_groups'), ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['invitation_id'], ['invitations.id'], name=op.f('fk_invitation_groups_invitation_id_invitations'), ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('invitation_id', 'group_id', name=op.f('pk_invitation_groups'))
|
|
)
|
|
op.create_table('mission_tests',
|
|
sa.Column('scenario_id', sa.Uuid(), nullable=False),
|
|
sa.Column('source_test_template_id', sa.Uuid(), nullable=True),
|
|
sa.Column('position', sa.Integer(), nullable=False),
|
|
sa.Column('snapshot_name', sa.String(length=255), nullable=False),
|
|
sa.Column('snapshot_description', sa.Text(), nullable=True),
|
|
sa.Column('snapshot_objective', sa.Text(), nullable=True),
|
|
sa.Column('snapshot_procedure_md', sa.Text(), nullable=True),
|
|
sa.Column('snapshot_prerequisites_md', sa.Text(), nullable=True),
|
|
sa.Column('snapshot_expected_red_md', sa.Text(), nullable=True),
|
|
sa.Column('snapshot_expected_blue_md', sa.Text(), nullable=True),
|
|
sa.Column('snapshot_opsec_level', sa.String(length=8), nullable=False),
|
|
sa.Column('snapshot_tags', sa.ARRAY(sa.String(length=64)), server_default='{}', nullable=False),
|
|
sa.Column('snapshot_expected_iocs', sa.ARRAY(sa.String(length=255)), server_default='{}', nullable=False),
|
|
sa.Column('state', sa.String(length=24), nullable=False),
|
|
sa.Column('executed_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column('executed_at_overridden', sa.Boolean(), nullable=False),
|
|
sa.Column('red_command', sa.Text(), nullable=True),
|
|
sa.Column('red_output', sa.Text(), nullable=True),
|
|
sa.Column('red_comment_md', sa.Text(), nullable=True),
|
|
sa.Column('blue_comment_md', sa.Text(), nullable=True),
|
|
sa.Column('detection_level_id', sa.Uuid(), nullable=True),
|
|
sa.Column('category_id', sa.Uuid(), nullable=True),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.CheckConstraint("snapshot_opsec_level IN ('low', 'medium', 'high')", name=op.f('ck_mission_tests_snapshot_opsec_level_valid')),
|
|
sa.CheckConstraint("state IN ('pending', 'executed', 'reviewed_by_blue', 'skipped', 'blocked')", name=op.f('ck_mission_tests_state_valid')),
|
|
sa.ForeignKeyConstraint(['category_id'], ['mission_categories.id'], name=op.f('fk_mission_tests_category_id_mission_categories'), ondelete='SET NULL'),
|
|
sa.ForeignKeyConstraint(['detection_level_id'], ['detection_levels.id'], name=op.f('fk_mission_tests_detection_level_id_detection_levels'), ondelete='SET NULL'),
|
|
sa.ForeignKeyConstraint(['scenario_id'], ['mission_scenarios.id'], name=op.f('fk_mission_tests_scenario_id_mission_scenarios'), ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['source_test_template_id'], ['test_templates.id'], name=op.f('fk_mission_tests_source_test_template_id_test_templates'), ondelete='SET NULL'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_mission_tests')),
|
|
sa.UniqueConstraint('scenario_id', 'position', name='uq_mission_tests_position')
|
|
)
|
|
op.create_index('ix_mission_tests_active', 'mission_tests', ['deleted_at'], unique=False, postgresql_where='deleted_at IS NULL')
|
|
op.create_index('ix_mission_tests_state', 'mission_tests', ['state'], unique=False)
|
|
op.create_table('test_template_mitre_tags',
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('test_template_id', sa.Uuid(), nullable=False),
|
|
sa.Column('mitre_kind', sa.String(length=16), nullable=False),
|
|
sa.Column('tactic_id', sa.Uuid(), nullable=True),
|
|
sa.Column('technique_id', sa.Uuid(), nullable=True),
|
|
sa.Column('subtechnique_id', sa.Uuid(), nullable=True),
|
|
sa.CheckConstraint("mitre_kind IN ('tactic', 'technique', 'subtechnique')", name=op.f('ck_test_template_mitre_tags_mitre_kind_valid')),
|
|
sa.CheckConstraint('(CASE WHEN tactic_id IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN technique_id IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN subtechnique_id IS NOT NULL THEN 1 ELSE 0 END) = 1', name=op.f('ck_test_template_mitre_tags_exactly_one_mitre_fk')),
|
|
sa.ForeignKeyConstraint(['subtechnique_id'], ['mitre_subtechniques.id'], name=op.f('fk_test_template_mitre_tags_subtechnique_id_mitre_subtechniques'), ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['tactic_id'], ['mitre_tactics.id'], name=op.f('fk_test_template_mitre_tags_tactic_id_mitre_tactics'), ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['technique_id'], ['mitre_techniques.id'], name=op.f('fk_test_template_mitre_tags_technique_id_mitre_techniques'), ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['test_template_id'], ['test_templates.id'], name=op.f('fk_test_template_mitre_tags_test_template_id_test_templates'), ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_test_template_mitre_tags')),
|
|
sa.UniqueConstraint('test_template_id', 'tactic_id', 'technique_id', 'subtechnique_id', name='uq_test_template_mitre_tag')
|
|
)
|
|
op.create_index('ix_test_template_mitre_tags_template', 'test_template_mitre_tags', ['test_template_id'], unique=False)
|
|
op.create_table('evidence_files',
|
|
sa.Column('mission_test_id', sa.Uuid(), nullable=False),
|
|
sa.Column('sha256', sa.String(length=64), nullable=False),
|
|
sa.Column('mime', sa.String(length=127), nullable=False),
|
|
sa.Column('size_bytes', sa.BigInteger(), nullable=False),
|
|
sa.Column('storage_path', sa.Text(), nullable=False),
|
|
sa.Column('original_filename', sa.String(length=255), nullable=False),
|
|
sa.Column('uploaded_by_user_id', sa.Uuid(), nullable=True),
|
|
sa.Column('uploaded_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.ForeignKeyConstraint(['mission_test_id'], ['mission_tests.id'], name=op.f('fk_evidence_files_mission_test_id_mission_tests'), ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['uploaded_by_user_id'], ['users.id'], name=op.f('fk_evidence_files_uploaded_by_user_id_users'), ondelete='SET NULL'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_evidence_files'))
|
|
)
|
|
op.create_index('ix_evidence_files_active', 'evidence_files', ['deleted_at'], unique=False, postgresql_where='deleted_at IS NULL')
|
|
op.create_index('ix_evidence_files_mission_test', 'evidence_files', ['mission_test_id'], unique=False)
|
|
op.create_index('ix_evidence_files_sha256', 'evidence_files', ['sha256'], unique=False)
|
|
op.create_table('mission_test_mitre_tags',
|
|
sa.Column('id', sa.Uuid(), nullable=False),
|
|
sa.Column('mission_test_id', sa.Uuid(), nullable=False),
|
|
sa.Column('mitre_kind', sa.String(length=16), nullable=False),
|
|
sa.Column('mitre_external_id', sa.String(length=16), nullable=False),
|
|
sa.Column('mitre_name', sa.String(length=255), nullable=False),
|
|
sa.Column('mitre_url', sa.String(length=512), nullable=True),
|
|
sa.CheckConstraint("mitre_kind IN ('tactic', 'technique', 'subtechnique')", name=op.f('ck_mission_test_mitre_tags_mitre_kind_valid')),
|
|
sa.ForeignKeyConstraint(['mission_test_id'], ['mission_tests.id'], name=op.f('fk_mission_test_mitre_tags_mission_test_id_mission_tests'), ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_mission_test_mitre_tags')),
|
|
sa.UniqueConstraint('mission_test_id', 'mitre_external_id', name='uq_mission_test_mitre_tag')
|
|
)
|
|
op.create_index('ix_mission_test_mitre_tags_test', 'mission_test_mitre_tags', ['mission_test_id'], unique=False)
|
|
# ### end Alembic commands ###
|
|
|
|
|
|
def downgrade() -> None:
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.drop_index('ix_mission_test_mitre_tags_test', table_name='mission_test_mitre_tags')
|
|
op.drop_table('mission_test_mitre_tags')
|
|
op.drop_index('ix_evidence_files_sha256', table_name='evidence_files')
|
|
op.drop_index('ix_evidence_files_mission_test', table_name='evidence_files')
|
|
op.drop_index('ix_evidence_files_active', table_name='evidence_files', postgresql_where='deleted_at IS NULL')
|
|
op.drop_table('evidence_files')
|
|
op.drop_index('ix_test_template_mitre_tags_template', table_name='test_template_mitre_tags')
|
|
op.drop_table('test_template_mitre_tags')
|
|
op.drop_index('ix_mission_tests_state', table_name='mission_tests')
|
|
op.drop_index('ix_mission_tests_active', table_name='mission_tests', postgresql_where='deleted_at IS NULL')
|
|
op.drop_table('mission_tests')
|
|
op.drop_table('invitation_groups')
|
|
op.drop_table('user_groups')
|
|
op.drop_index('ix_scenario_template_tests_test', table_name='scenario_template_tests')
|
|
op.drop_index('ix_scenario_template_tests_scenario', table_name='scenario_template_tests')
|
|
op.drop_table('scenario_template_tests')
|
|
op.drop_index('ix_refresh_tokens_user_id_expires_at', table_name='refresh_tokens')
|
|
op.drop_table('refresh_tokens')
|
|
op.drop_index('ix_notifications_user_unread', table_name='notifications', postgresql_where='read_at IS NULL')
|
|
op.drop_table('notifications')
|
|
op.drop_table('mitre_technique_tactics')
|
|
op.drop_index('ix_mitre_subtechniques_technique_id', table_name='mitre_subtechniques')
|
|
op.drop_table('mitre_subtechniques')
|
|
op.drop_index('ix_mission_scenarios_mission', table_name='mission_scenarios')
|
|
op.drop_index('ix_mission_scenarios_active', table_name='mission_scenarios', postgresql_where='deleted_at IS NULL')
|
|
op.drop_table('mission_scenarios')
|
|
op.drop_index('ix_mission_members_user', table_name='mission_members')
|
|
op.drop_table('mission_members')
|
|
op.drop_index('ix_mission_categories_active', table_name='mission_categories', postgresql_where='deleted_at IS NULL')
|
|
op.drop_table('mission_categories')
|
|
op.drop_index('ix_invitations_expires_at', table_name='invitations')
|
|
op.drop_table('invitations')
|
|
op.drop_table('group_permissions')
|
|
op.drop_index('uq_users_email_active', table_name='users', postgresql_where='deleted_at IS NULL')
|
|
op.drop_index('ix_users_active', table_name='users', postgresql_where='deleted_at IS NULL')
|
|
op.drop_table('users')
|
|
op.drop_index('ix_test_templates_name', table_name='test_templates')
|
|
op.drop_index('ix_test_templates_active', table_name='test_templates', postgresql_where='deleted_at IS NULL')
|
|
op.drop_table('test_templates')
|
|
op.drop_table('settings')
|
|
op.drop_index('ix_scenario_templates_name', table_name='scenario_templates')
|
|
op.drop_index('ix_scenario_templates_active', table_name='scenario_templates', postgresql_where='deleted_at IS NULL')
|
|
op.drop_table('scenario_templates')
|
|
op.drop_table('permissions')
|
|
op.drop_index('ix_mitre_techniques_name', table_name='mitre_techniques')
|
|
op.drop_table('mitre_techniques')
|
|
op.drop_table('mitre_tactics')
|
|
op.drop_index('ix_missions_status', table_name='missions')
|
|
op.drop_index('ix_missions_active', table_name='missions', postgresql_where='deleted_at IS NULL')
|
|
op.drop_table('missions')
|
|
op.drop_index('uq_groups_name_active', table_name='groups', postgresql_where='deleted_at IS NULL')
|
|
op.drop_index('ix_groups_active', table_name='groups', postgresql_where='deleted_at IS NULL')
|
|
op.drop_table('groups')
|
|
op.drop_table('detection_levels')
|
|
# ### end Alembic commands ###
|