feat(backend): add Flask app factory, audit writer, flat CRUD + CLI (B0.7)
- Flask app factory wires SQLAlchemy / Migrate / Login / SocketIO and
registers every blueprint. /healthz smoke endpoint included.
- Pydantic 2 DTOs (request/response) for engagement / host / TTP /
scenario aggregates with from_attributes=True conversion.
- Flat CRUD blueprints under /api/v1/:
* engagements (list / create / get / put / delete-as-archive)
* hosts (engagement-scoped CRUD)
* library/ttps (CRUD; promote requires the lead-only TTP_PROMOTE)
* scenarios + steps (F3 invariant enforced: host.c2_type must match
scenario.c2_type at compose time, 400 otherwise).
- @require_perm guards every endpoint per the F11 matrix.
- audit/ writer is hash-chained from v1 (SHA-256 of canonical record
plus previous hash). The SQL-level write-only role enforcement ships
in the deploy playbook (idempotent grants run at migration time).
- mimic-cli (click): user create (seeds RT operator/lead with group
membership), db dump / db restore (manual pg_dump/pg_restore, R-O1).
No orchestrator, no WebSocket, no report generation — those land after
PR1/PR2/PR3.
This commit is contained in:
50
backend/src/mimic/app.py
Normal file
50
backend/src/mimic/app.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Flask application factory."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from flask import Flask, jsonify
|
||||
from flask.typing import ResponseReturnValue
|
||||
|
||||
from mimic.api import register_blueprints
|
||||
from mimic.auth.identity import load_user
|
||||
from mimic.config import Settings, get_settings
|
||||
from mimic.extensions import db, login_manager, migrate, socketio
|
||||
from mimic.logging import configure_logging
|
||||
|
||||
|
||||
def create_app(settings: Settings | None = None) -> Flask:
|
||||
settings = settings or get_settings()
|
||||
configure_logging(settings.log_level, as_json=settings.log_json)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.update(
|
||||
SECRET_KEY=settings.secret_key.get_secret_value(),
|
||||
SQLALCHEMY_DATABASE_URI=settings.database_url,
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS=False,
|
||||
SESSION_COOKIE_SECURE=settings.session_cookie_secure,
|
||||
SESSION_COOKIE_SAMESITE=settings.session_cookie_samesite,
|
||||
SESSION_COOKIE_HTTPONLY=True,
|
||||
PERMANENT_SESSION_LIFETIME=timedelta(minutes=settings.session_lifetime_minutes),
|
||||
MIMIC_SETTINGS=settings,
|
||||
)
|
||||
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db, directory="src/mimic/db/migrations")
|
||||
login_manager.init_app(app)
|
||||
login_manager.user_loader(load_user) # type: ignore[arg-type]
|
||||
|
||||
socketio.init_app(
|
||||
app,
|
||||
cors_allowed_origins=settings.cors_origins or "*",
|
||||
async_mode="gevent",
|
||||
)
|
||||
|
||||
register_blueprints(app)
|
||||
|
||||
@app.get("/healthz")
|
||||
def healthz() -> ResponseReturnValue:
|
||||
return jsonify(status="ok"), 200
|
||||
|
||||
return app
|
||||
Reference in New Issue
Block a user