- Add Fernet crypto service (MIMIC_ENCRYPTION_KEY env, C2Disabled on absent key) - Add Alembic migration 0006: c2_config + c2_task tables with cascade FKs - Add C2Config and C2Task SQLAlchemy models - Add C2Adapter ABC with dataclasses (C2Health, C2Callback, C2TaskStatus, C2TaskPage) - Add FakeAdapter (deterministic in-memory, MIMIC_C2_ADAPTER=fake) - Add MythicAdapter scaffold: test_connection() live, M2+ raise NotImplementedError - Add decode_response_text() helper for base64/binary Mythic responses - Add GET/PUT/DELETE/POST-test /api/engagements/<id>/c2-config endpoints - RBAC: admin+redteam OK, SOC 403; 503 guard when encryption key absent - Token never returned in API responses; stored Fernet-encrypted only - 42 new tests (300 total, 258 baseline preserved green) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
78 lines
2.7 KiB
Python
78 lines
2.7 KiB
Python
"""Flask application factory."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from flask import Flask, jsonify, send_from_directory
|
|
|
|
from backend.app.api import auth_bp, c2_bp, engagements_bp, simulations_bp, templates_bp, users_bp
|
|
from backend.app.cli import register_cli
|
|
from backend.app.config import Config, TestConfig
|
|
from backend.app.errors import register_error_handlers
|
|
from backend.app.extensions import db, migrate
|
|
|
|
|
|
def create_app(config_object: object | None = None) -> Flask:
|
|
"""Application factory.
|
|
|
|
`config_object` is an *instance* (not a class) of Config or a subclass.
|
|
If None, picks TestConfig when MIMIC_TESTING=1, otherwise Config.
|
|
"""
|
|
static_folder = str(Path(__file__).parent / "static")
|
|
app = Flask(__name__, static_folder=static_folder, static_url_path="/static")
|
|
|
|
if config_object is None:
|
|
config_object = TestConfig() if os.environ.get("MIMIC_TESTING") == "1" else Config()
|
|
app.config.from_object(config_object)
|
|
|
|
db.init_app(app)
|
|
migrations_dir = str(Path(__file__).parent.parent / "migrations")
|
|
migrate.init_app(app, db, directory=migrations_dir)
|
|
|
|
# Ensure models are imported so Alembic/metadata see them.
|
|
from backend.app import models # noqa: F401
|
|
|
|
app.register_blueprint(auth_bp)
|
|
app.register_blueprint(users_bp)
|
|
app.register_blueprint(engagements_bp)
|
|
app.register_blueprint(simulations_bp)
|
|
app.register_blueprint(templates_bp)
|
|
app.register_blueprint(c2_bp)
|
|
|
|
from backend.app.services import mitre as mitre_svc
|
|
mitre_svc.load_bundle()
|
|
|
|
register_error_handlers(app)
|
|
register_cli(app)
|
|
|
|
static_root = Path(static_folder)
|
|
|
|
@app.get("/api/health")
|
|
def health():
|
|
return {"status": "ok"}, 200
|
|
|
|
# Serve the built frontend (Vite output copied into app/static at image build time).
|
|
@app.get("/")
|
|
def index():
|
|
index_path = static_root / "index.html"
|
|
if index_path.exists():
|
|
return send_from_directory(static_folder, "index.html")
|
|
return {"status": "ok", "message": "Mimic API running. Frontend not built."}, 200
|
|
|
|
@app.get("/<path:path>")
|
|
def spa_fallback(path: str):
|
|
# Unknown /api/* paths must stay JSON 404 — never shadowed by index.html.
|
|
if path.startswith("api/"):
|
|
return jsonify({"error": "Not found"}), 404
|
|
# Serve static assets if present; otherwise hand back index.html for client routing.
|
|
candidate = static_root / path
|
|
if candidate.is_file():
|
|
return send_from_directory(static_folder, path)
|
|
index_path = static_root / "index.html"
|
|
if index_path.exists():
|
|
return send_from_directory(static_folder, "index.html")
|
|
return jsonify({"error": "Not found"}), 404
|
|
|
|
return app
|