"""Simulation CRUD + workflow endpoints.""" from __future__ import annotations from datetime import UTC, datetime from flask import Blueprint, g, jsonify, request from backend.app.auth import login_required, role_required from backend.app.extensions import db from backend.app.models import Engagement from backend.app.models.simulation import Simulation, SimulationStatus from backend.app.serializers import serialize_simulation from backend.app.services import simulation_workflow simulations_bp = Blueprint("simulations", __name__) # --------------------------------------------------------------------------- # Nested under /api/engagements//simulations # --------------------------------------------------------------------------- @simulations_bp.get("/api/engagements//simulations") @login_required def list_simulations(eid: int): engagement = db.session.get(Engagement, eid) if engagement is None: return jsonify({"error": "Engagement not found"}), 404 sims = ( Simulation.query.filter_by(engagement_id=eid) .order_by(Simulation.created_at.desc()) .all() ) return jsonify([serialize_simulation(s) for s in sims]), 200 @simulations_bp.post("/api/engagements//simulations") @role_required("admin", "redteam") def create_simulation(eid: int): engagement = db.session.get(Engagement, eid) if engagement is None: return jsonify({"error": "Engagement not found"}), 404 data = request.get_json(silent=True) or {} name = (data.get("name") or "").strip() if not name: return jsonify({"error": "name is required"}), 400 sim = Simulation( engagement_id=eid, name=name, status=SimulationStatus.PENDING, created_at=datetime.now(UTC), created_by_id=g.current_user.id, ) db.session.add(sim) db.session.commit() return jsonify(serialize_simulation(sim)), 201 # --------------------------------------------------------------------------- # Flat /api/simulations/ # --------------------------------------------------------------------------- @simulations_bp.get("/api/simulations/") @login_required def get_simulation(sid: int): sim = db.session.get(Simulation, sid) if sim is None: return jsonify({"error": "Simulation not found"}), 404 return jsonify(serialize_simulation(sim)), 200 @simulations_bp.patch("/api/simulations/") @login_required def update_simulation(sid: int): sim = db.session.get(Simulation, sid) if sim is None: return jsonify({"error": "Simulation not found"}), 404 user = g.current_user data = request.get_json(silent=True) or {} if not data: return jsonify(serialize_simulation(sim)), 200 err = simulation_workflow.apply_patch(sim, data, user) if err is not None: return err db.session.commit() return jsonify(serialize_simulation(sim)), 200 @simulations_bp.delete("/api/simulations/") @role_required("admin", "redteam") def delete_simulation(sid: int): sim = db.session.get(Simulation, sid) if sim is None: return jsonify({"error": "Simulation not found"}), 404 db.session.delete(sim) db.session.commit() return "", 204 @simulations_bp.post("/api/simulations//transition") @login_required def transition_simulation(sid: int): sim = db.session.get(Simulation, sid) if sim is None: return jsonify({"error": "Simulation not found"}), 404 data = request.get_json(silent=True) or {} to_status = data.get("to", "") err = simulation_workflow.transition(sim, to_status, g.current_user) if err is not None: return err return jsonify(serialize_simulation(sim)), 200 # --------------------------------------------------------------------------- # MITRE autocomplete + matrix # --------------------------------------------------------------------------- @simulations_bp.get("/api/mitre/techniques") @login_required def mitre_techniques(): from backend.app.services import mitre as mitre_svc if not mitre_svc.mitre_loaded: return jsonify({"error": "mitre bundle not loaded"}), 503 q = request.args.get("q", "").strip() results = mitre_svc.search(q) return jsonify(results), 200 @simulations_bp.get("/api/mitre/matrix") @login_required def mitre_matrix(): from backend.app.services import mitre as mitre_svc if not mitre_svc.mitre_loaded: return jsonify({"error": "mitre bundle not loaded"}), 503 return jsonify(mitre_svc.get_matrix()), 200