Database & Testing
Database
- Production: PostgreSQL (configured via
POSTGRES_*env vars) - Development or small environments: SQLite is supported (
DB_ENGINE=sqlite) - ORM: SQLAlchemy 2.0 with modern
Mappedtype annotations
Schema Generation (No Migrations)
The project currently does not use Alembic or any migration tool.
The database schema is automatically generated on application startup. In app/core/startup.py, the backend inspects all modules that inherit from Base and ensures the database tables exist:
Because of this, destructive changes (like dropping a column or renaming a table) require manual database intervention if applied to an existing production database.
Session Management
Database sessions are managed via FastAPI's dependency injection system, ensuring a session is opened when a request begins and closed when it finishes. Always inject Session = Depends(get_session) into your routers, and explicitly pass it down to your services.
from app.db.session import get_session
@router.get("/")
def my_endpoint(session: Session = Depends(get_session)):
return my_service(session)
Database Schema
Entity-relationship diagram of all SQLAlchemy models in app/models/. The diagram is split into three sections: Core (assessment workflow), Templates (reusable configuration), and Reference Data (MITRE ATT&CK).
All models inherit from Base, which provides audit fields (created_by, created_at, updated_by, updated_at) — these are omitted from the diagram for clarity. Models using SoftDeleteMixin also have deleted, deleted_at, and deleted_by fields.
erDiagram
%% ──────────────────────────────────────────────
%% CORE — Assessment Workflow
%% ──────────────────────────────────────────────
User {
uuid id PK
string email UK
UserRole role
bool mfa_verified
string mfa_secret
string hashed_password
bool disabled
datetime last_login_at
datetime last_logout_at
}
Assessment {
uuid id PK
string name
string description
AssessmentType assessment_type
json default_evaluation_templates
}
Acl {
uuid id PK
uuid user_id FK
uuid assessment_id FK
AclRole assessment_role
}
ActivityGroup {
uuid id PK
uuid assessment_id FK
string name
bool visible
bool is_default
int activity_group_position
}
Activity {
uuid id PK
uuid assessment_id FK
uuid activity_group_id FK
string name
string mitre_tactic
string mitre_technique
string provider
ActivityPriority priority
ActivityState state
bool visible
int activity_position
json linked_knowledge_base_articles
}
ActivityEvaluation {
uuid id PK
uuid activity_id FK
EvaluationResult logged_evaluation
EvaluationResult alerted_evaluation
EvaluationResult prevented_evaluation
EvaluationResult stakeholder_notified_evaluation
int activity_coverage_score
}
ActivityEvaluationDynamicQuestions {
uuid id PK
uuid activity_evaluation_id FK
uuid evaluation_template_id FK
string data
EvaluationResult evaluation_result
int position
}
ActivityHistory {
uuid id PK
uuid activity_id FK
int version
datetime saved_at
uuid saved_by_id FK
json snapshot
}
Tag {
uuid id PK
uuid assessment_id FK
string name
string color
}
Asset {
uuid id PK
uuid assessment_id FK
string name
string icon
json properties
}
File {
uuid id PK
uuid activity_id FK
string filename
FileType content_type
bytes file_content
int size
FileCategory category
}
KnowledgeBase {
uuid id PK
string name UK
string mitre_technique_id
json content
}
%% ──────────────────────────────────────────────
%% TEMPLATES — Reusable Configuration
%% ──────────────────────────────────────────────
EvaluationTemplate {
uuid id PK
string name UK
string evaluation_criteria
string description
}
ReportTemplate {
uuid id PK
string filename
ReportTemplateFormat format
bytes template_content
}
CampaignTemplate {
uuid id PK
string name UK
string description
}
CampaignTemplateItem {
uuid id PK
uuid campaign_template_id FK
int position
string item_type
uuid activity_group_template_id FK
uuid activity_template_id FK
}
ActivityGroupTemplate {
uuid id PK
string name UK
string description
}
ActivityTemplate {
uuid id PK
string name
string mitre_tactic
string mitre_technique
string provider
string priority
json linked_knowledge_base_articles
}
%% ──────────────────────────────────────────────
%% REFERENCE DATA — MITRE ATT&CK
%% ──────────────────────────────────────────────
Tactic {
uuid id PK
string mitre_id UK
string name
string url
}
Technique {
uuid id PK
string mitre_id UK
string name
string url
}
%% ──────────────────────────────────────────────
%% RELATIONSHIPS
%% ──────────────────────────────────────────────
%% Core
User ||--o{ Acl : "has roles"
Assessment ||--o{ Acl : "grants access"
Assessment ||--o{ ActivityGroup : "contains"
Assessment ||--o{ Activity : "contains"
Assessment ||--o{ Tag : "has"
Assessment ||--o{ Asset : "has"
ActivityGroup ||--o{ Activity : "groups"
Activity ||--o| ActivityEvaluation : "has evaluation"
Activity ||--o{ File : "has files"
Activity ||--o{ ActivityHistory : "has history"
ActivityEvaluation ||--o{ ActivityEvaluationDynamicQuestions : "has questions"
ActivityEvaluationDynamicQuestions }o--|| EvaluationTemplate : "uses template"
User ||--o{ ActivityHistory : "saved by"
%% Many-to-many (via association tables)
Activity }o--o{ Tag : "activity_tag"
Activity }o--o{ Asset : "activity_asset (role-based)"
%% Templates
CampaignTemplate ||--o{ CampaignTemplateItem : "contains"
CampaignTemplateItem }o--o| ActivityGroupTemplate : "references"
CampaignTemplateItem }o--o| ActivityTemplate : "references"
ActivityGroupTemplate }o--o{ ActivityTemplate : "activity_template_activity_group"
%% MITRE
Tactic }o--o{ Technique : "technique_tactic"
Association Tables
The following join tables implement many-to-many relationships:
| Table | Links | Extra columns |
|---|---|---|
activity_tag |
Activity ↔ Tag | — |
activity_asset |
Activity ↔ Asset | role (source, target, tool, log_source, prevention_source, alert_source, stakeholder_notification_source) |
technique_tactic |
Technique ↔ Tactic | — |
activity_template_activity_group |
ActivityTemplate ↔ ActivityGroupTemplate | position |
Mixins
| Mixin | Used by | Fields added |
|---|---|---|
SoftDeleteMixin |
Activity, ActivityGroup, Tag, Asset | deleted, deleted_at, deleted_by |
Testing
The backend test suite runs against an isolated, in-memory SQLite database (sqlite://), completely separate from your development or production data.
uv run pytest # Run all tests
uv run pytest tests/test_assessment.py # Run specific test file
uv run pytest -v # Verbose output
AI Slop
Full transparency: All, I mean ALL, tests are AI generated. Without AI, there would probably be no tests, since I couldn't be bothered with writing tests. The effectiveness and coverage of these tests must be proven in the future.
Pytest Fixtures
The test environment provides several crucial fixtures automatically (defined in tests/conftest.py):
session: Provides an active SQLAlchemy session connected to the empty in-memory test database. Use this to seed test data or query results directly.client: ATestClientinstance that has itsget_sessiondependency overridden to use the test database above.- Auth Fixtures: Pre-built users (
test_admin_user,test_regular_user,test_disabled_user) and their corresponding ready-to-use API headers (auth_headers_admin,auth_headers_regular).
Example Test:
def test_create_widget(client: TestClient, auth_headers_admin: dict):
response = client.post(
"/api/v1/widgets/",
json={"name": "Test Widget"},
headers=auth_headers_admin
)
assert response.status_code == 200
Linting and Formatting
We use Ruff for both linting and formatting:
uv run ruff check . # Lint (errors, pyflakes, import sorting)
uv run ruff check --fix . # Lint with auto-fix
uv run ruff format . # Format code
uv run ruff format --check . # Check formatting without changes
Ruff configuration in pyproject.toml:
[tool.ruff.lint]
select = ["E", "F", "I"] # Error, Pyflakes, isort
ignore = ["E501"] # Line length not enforced
All linting and format checks run automatically in CI on pull requests.