chore(backend): bootstrap Python 3.12+ project skeleton (B0.1)
- pyproject.toml with ruff + mypy strict + pytest + coverage >=70% - Makefile with Docker/Podman auto-detect - Multi-stage Dockerfile (python:3.12-slim-bookworm, non-root user) - docker-compose.yml for Postgres dev DB - alembic.ini wired to src/mimic/db/migrations - scripts/postgres-init/00-roles.sql seeds the audit writer role - .env.example documents every MIMIC_* var (no secrets committed)
This commit is contained in:
23
backend/.env.example
Normal file
23
backend/.env.example
Normal file
@@ -0,0 +1,23 @@
|
||||
# Mimic backend — example env. Copy to .env (gitignored) and adapt.
|
||||
|
||||
MIMIC_ENV=development
|
||||
MIMIC_SECRET_KEY=replace-me-with-secrets.token_urlsafe-32
|
||||
MIMIC_FERNET_KEY=
|
||||
|
||||
# Database
|
||||
POSTGRES_DB=mimic
|
||||
POSTGRES_USER=mimic_app
|
||||
POSTGRES_PASSWORD=mimic_dev_password
|
||||
MIMIC_DATABASE_URL=postgresql+psycopg://mimic_app:mimic_dev_password@localhost:5432/mimic
|
||||
MIMIC_DATABASE_AUDIT_URL=postgresql+psycopg://mimic_audit_writer:CHANGE_ME@localhost:5432/mimic
|
||||
|
||||
# Session / cookies
|
||||
MIMIC_SESSION_COOKIE_SECURE=false
|
||||
MIMIC_SESSION_COOKIE_SAMESITE=Lax
|
||||
|
||||
# CORS (frontend dev)
|
||||
MIMIC_CORS_ORIGINS=http://localhost:5173
|
||||
|
||||
# Logging
|
||||
MIMIC_LOG_LEVEL=DEBUG
|
||||
MIMIC_LOG_JSON=false
|
||||
60
backend/Dockerfile
Normal file
60
backend/Dockerfile
Normal file
@@ -0,0 +1,60 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# --- Stage 1: build --------------------------------------------------------
|
||||
FROM python:3.12-slim-bookworm AS build
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
# WeasyPrint native deps + libpq + build tools.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
libpango-1.0-0 \
|
||||
libpangoft2-1.0-0 \
|
||||
libcairo2 \
|
||||
libgdk-pixbuf-2.0-0 \
|
||||
libffi-dev \
|
||||
shared-mime-info \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /build
|
||||
COPY pyproject.toml README.md ./
|
||||
COPY src ./src
|
||||
|
||||
RUN pip install --upgrade pip wheel build \
|
||||
&& pip wheel --wheel-dir /wheels --no-deps .
|
||||
|
||||
RUN pip install --prefix=/install --no-warn-script-location .
|
||||
|
||||
# --- Stage 2: runtime ------------------------------------------------------
|
||||
FROM python:3.12-slim-bookworm AS runtime
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
FLASK_APP=mimic.app:create_app \
|
||||
MIMIC_ENV=production
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpq5 \
|
||||
libpango-1.0-0 \
|
||||
libpangoft2-1.0-0 \
|
||||
libcairo2 \
|
||||
libgdk-pixbuf-2.0-0 \
|
||||
shared-mime-info \
|
||||
tini \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd --system --gid 1001 mimic \
|
||||
&& useradd --system --uid 1001 --gid 1001 --home-dir /app --shell /usr/sbin/nologin mimic
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /install /usr/local
|
||||
COPY --chown=mimic:mimic src ./src
|
||||
|
||||
USER mimic
|
||||
EXPOSE 5000
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["gunicorn", "--worker-class", "gevent", "--workers", "1", "--bind", "0.0.0.0:5000", "mimic.app:create_app()"]
|
||||
80
backend/Makefile
Normal file
80
backend/Makefile
Normal file
@@ -0,0 +1,80 @@
|
||||
# --- Mimic backend Makefile ------------------------------------------------
|
||||
# Detects Docker / Podman automatically (NF-platform).
|
||||
|
||||
PY ?= python3.12
|
||||
VENV ?= .venv
|
||||
PIP := $(VENV)/bin/pip
|
||||
PYTHON := $(VENV)/bin/python
|
||||
PYTEST := $(VENV)/bin/pytest
|
||||
RUFF := $(VENV)/bin/ruff
|
||||
MYPY := $(VENV)/bin/mypy
|
||||
ALEMBIC := $(VENV)/bin/alembic
|
||||
FLASK := $(VENV)/bin/flask
|
||||
|
||||
# Container runtime auto-detect: Docker (preferred) or Podman rootless.
|
||||
CONTAINER ?= $(shell command -v docker 2>/dev/null || command -v podman 2>/dev/null)
|
||||
COMPOSE ?= $(shell command -v docker-compose 2>/dev/null || echo "$(CONTAINER) compose")
|
||||
|
||||
export FLASK_APP=mimic.app:create_app
|
||||
|
||||
.PHONY: help install lint fmt typecheck test test-int test-cov run \
|
||||
db-up db-down db-migrate db-revision db-seed db-dump db-restore \
|
||||
build clean
|
||||
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
|
||||
awk 'BEGIN {FS=":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
install: ## Create venv and install dev dependencies
|
||||
$(PY) -m venv $(VENV)
|
||||
$(PIP) install --upgrade pip wheel
|
||||
$(PIP) install -e ".[dev]"
|
||||
|
||||
lint: ## Ruff lint
|
||||
$(RUFF) check src tests
|
||||
|
||||
fmt: ## Ruff format
|
||||
$(RUFF) format src tests
|
||||
$(RUFF) check --fix src tests
|
||||
|
||||
typecheck: ## Mypy strict
|
||||
$(MYPY) src
|
||||
|
||||
test: ## Unit tests (SQLite)
|
||||
$(PYTEST) tests/unit -v
|
||||
|
||||
test-int: ## Integration tests (testcontainers Postgres)
|
||||
$(PYTEST) tests/integration -v -m integration
|
||||
|
||||
test-cov: ## Unit + integration with coverage report
|
||||
$(PYTEST) --cov=mimic --cov-report=term-missing --cov-report=html
|
||||
|
||||
run: ## Run Flask dev server
|
||||
$(FLASK) run --host 0.0.0.0 --port 5000 --debug
|
||||
|
||||
db-up: ## Start Postgres dev container
|
||||
$(COMPOSE) up -d postgres
|
||||
|
||||
db-down: ## Stop Postgres dev container
|
||||
$(COMPOSE) down
|
||||
|
||||
db-migrate: ## Apply migrations
|
||||
$(ALEMBIC) upgrade head
|
||||
|
||||
db-revision: ## Generate migration (msg=...)
|
||||
$(ALEMBIC) revision --autogenerate -m "$(msg)"
|
||||
|
||||
db-seed: ## Seed local dev data (TBD sprint 1)
|
||||
@echo "TBD sprint 1"
|
||||
|
||||
db-dump: ## Manual DB dump (NF-state, R-O1)
|
||||
$(VENV)/bin/mimic-cli db dump --out backups/mimic-$$(date +%Y%m%d-%H%M%S).sql
|
||||
|
||||
db-restore: ## Restore from dump (file=...)
|
||||
$(VENV)/bin/mimic-cli db restore --file $(file)
|
||||
|
||||
build: ## Build wheel
|
||||
$(PYTHON) -m build
|
||||
|
||||
clean:
|
||||
rm -rf $(VENV) .pytest_cache .mypy_cache .ruff_cache htmlcov dist build *.egg-info
|
||||
56
backend/README.md
Normal file
56
backend/README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Mimic — backend
|
||||
|
||||
Sprint 0 skeleton. Python 3.12+ / Flask / SQLAlchemy 2 / Alembic / Pydantic 2.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
backend/
|
||||
├── src/mimic/
|
||||
│ ├── app.py # Flask app factory + SocketIO init
|
||||
│ ├── config.py # Pydantic Settings
|
||||
│ ├── extensions.py # db, migrate, socketio, login_manager
|
||||
│ ├── db/
|
||||
│ │ ├── models/ # SQLAlchemy 2 typed models
|
||||
│ │ ├── repositories/ # data access per aggregate
|
||||
│ │ └── migrations/ # Alembic
|
||||
│ ├── schemas/ # Pydantic 2 DTOs
|
||||
│ ├── api/ # Flask blueprints (REST)
|
||||
│ ├── ws/ # Flask-SocketIO namespaces
|
||||
│ ├── connectors/ # C2Connector ABC + payload mapping
|
||||
│ ├── orchestrator/ # run state machine (stub in sprint 0)
|
||||
│ ├── templating/ # Jinja2 sandbox + regex_extract
|
||||
│ ├── audit/ # append-only writer + rotation
|
||||
│ ├── reporting/ # WeasyPrint builder (stub in sprint 0)
|
||||
│ ├── rbac/ # group-based permission matrix (F11)
|
||||
│ ├── importers/ # ATR + C2 journal (stub in sprint 0)
|
||||
│ └── cli/ # mimic-cli (click)
|
||||
└── tests/
|
||||
├── unit/ # SQLite, pure logic
|
||||
└── integration/ # testcontainers Postgres
|
||||
```
|
||||
|
||||
## Local dev
|
||||
|
||||
```bash
|
||||
make install # uv venv + pip install -e .[dev]
|
||||
make db-up # docker compose up -d postgres
|
||||
make db-migrate # alembic upgrade head
|
||||
make run # flask run (debug)
|
||||
make test # pytest unit
|
||||
make test-int # pytest integration (testcontainers)
|
||||
make lint # ruff + mypy strict
|
||||
```
|
||||
|
||||
## What sprint 0 ships
|
||||
|
||||
- Full §8 data model + Alembic initial migration (Postgres-specific constraints: audit_log write-only role, soc_session hash, c2_credential Fernet column).
|
||||
- `C2Connector` ABC + dataclasses + `payload_type` enum + factory. **No real Mythic/Home implementation** (blocked on PR1/PR2).
|
||||
- Jinja2 SandboxedEnvironment + `regex_extract` filter (re2).
|
||||
- Local auth (bcrypt + Flask session) + group-based RBAC matching the F11 permission matrix.
|
||||
- Flat CRUD on engagements / hosts / TTPs / scenarios.
|
||||
- pytest baseline + testcontainers Postgres scaffolding.
|
||||
|
||||
## Out of sprint 0
|
||||
|
||||
Orchestrator, WebSocket cockpit, real connectors, report generation, audit rotation.
|
||||
39
backend/alembic.ini
Normal file
39
backend/alembic.ini
Normal file
@@ -0,0 +1,39 @@
|
||||
[alembic]
|
||||
script_location = src/mimic/db/migrations
|
||||
prepend_sys_path = src
|
||||
version_path_separator = os
|
||||
sqlalchemy.url =
|
||||
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
23
backend/docker-compose.yml
Normal file
23
backend/docker-compose.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: mimic-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-mimic}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-mimic_app}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mimic_dev_password}
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
volumes:
|
||||
- mimic_pgdata:/var/lib/postgresql/data
|
||||
- ./scripts/postgres-init:/docker-entrypoint-initdb.d:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mimic_app} -d ${POSTGRES_DB:-mimic}"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
mimic_pgdata:
|
||||
name: mimic_pgdata
|
||||
147
backend/pyproject.toml
Normal file
147
backend/pyproject.toml
Normal file
@@ -0,0 +1,147 @@
|
||||
[build-system]
|
||||
requires = ["hatchling>=1.24"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mimic"
|
||||
version = "0.1.0a0"
|
||||
description = "Mimic — internal BAS platform (sprint 0 skeleton)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
license = { text = "Proprietary" }
|
||||
authors = [{ name = "RT" }]
|
||||
|
||||
dependencies = [
|
||||
"flask>=3.0,<4.0",
|
||||
"flask-socketio>=5.3,<6.0",
|
||||
"flask-login>=0.6.3,<1.0",
|
||||
"flask-migrate>=4.0,<5.0",
|
||||
"sqlalchemy>=2.0,<3.0",
|
||||
"alembic>=1.13,<2.0",
|
||||
"psycopg[binary]>=3.1,<4.0",
|
||||
"pydantic>=2.6,<3.0",
|
||||
"pydantic-settings>=2.2,<3.0",
|
||||
"python-json-logger>=2.0,<3.0",
|
||||
"structlog>=24.1,<25.0",
|
||||
"bcrypt>=4.1,<5.0",
|
||||
"cryptography>=42.0,<43.0",
|
||||
"jinja2>=3.1,<4.0",
|
||||
"google-re2>=1.1,<2.0",
|
||||
"click>=8.1,<9.0",
|
||||
"gevent>=24.2,<25.0",
|
||||
"gevent-websocket>=0.10,<1.0",
|
||||
"httpx>=0.27,<1.0",
|
||||
"weasyprint>=61.0,<62.0",
|
||||
"authlib>=1.3,<2.0",
|
||||
"pyyaml>=6.0,<7.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.0,<9.0",
|
||||
"pytest-cov>=5.0,<6.0",
|
||||
"pytest-flask>=1.3,<2.0",
|
||||
"pytest-mock>=3.12,<4.0",
|
||||
"testcontainers[postgres]>=4.4,<5.0",
|
||||
"ruff>=0.4,<1.0",
|
||||
"mypy>=1.10,<2.0",
|
||||
"types-pyyaml>=6.0,<7.0",
|
||||
"freezegun>=1.5,<2.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
mimic-cli = "mimic.cli:cli"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/mimic"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["src/mimic", "README.md", "pyproject.toml"]
|
||||
|
||||
# -- Ruff -------------------------------------------------------------------
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py312"
|
||||
src = ["src", "tests"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", "F", "W", # pycodestyle / pyflakes
|
||||
"I", # isort
|
||||
"B", # bugbear
|
||||
"UP", # pyupgrade
|
||||
"N", # pep8-naming
|
||||
"S", # flake8-bandit (security)
|
||||
"C4", # comprehensions
|
||||
"DTZ", # datetime tz
|
||||
"PIE",
|
||||
"PT", # pytest
|
||||
"RET",
|
||||
"SIM",
|
||||
"TID",
|
||||
"PL",
|
||||
"RUF",
|
||||
]
|
||||
ignore = [
|
||||
"PLR0913", # too many args (Flask handlers + DI)
|
||||
"S101", # assert in tests
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/**" = ["S101", "S105", "S106", "PLR2004"]
|
||||
"src/mimic/db/migrations/**" = ["E501", "N999"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["mimic"]
|
||||
|
||||
# -- Mypy -------------------------------------------------------------------
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.12"
|
||||
strict = true
|
||||
warn_unreachable = true
|
||||
warn_unused_ignores = true
|
||||
show_error_codes = true
|
||||
plugins = ["pydantic.mypy"]
|
||||
exclude = ["src/mimic/db/migrations/versions/"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"weasyprint.*",
|
||||
"google.re2.*",
|
||||
"re2",
|
||||
"flask_socketio.*",
|
||||
"flask_migrate.*",
|
||||
"gevent.*",
|
||||
"testcontainers.*",
|
||||
"authlib.*",
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
# -- Pytest -----------------------------------------------------------------
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-ra --strict-markers --strict-config"
|
||||
markers = [
|
||||
"integration: requires testcontainers Postgres",
|
||||
"slow: long-running tests",
|
||||
]
|
||||
filterwarnings = ["error"]
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
source = ["src/mimic"]
|
||||
omit = ["src/mimic/db/migrations/*"]
|
||||
|
||||
[tool.coverage.report]
|
||||
fail_under = 70
|
||||
show_missing = true
|
||||
skip_covered = false
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"raise NotImplementedError",
|
||||
"if TYPE_CHECKING:",
|
||||
"\\.\\.\\.",
|
||||
]
|
||||
20
backend/scripts/postgres-init/00-roles.sql
Normal file
20
backend/scripts/postgres-init/00-roles.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- Roles used by the application.
|
||||
-- NF-AUDIT: audit_log must be append-only at the SQL level. The application
|
||||
-- writes via mimic_audit_writer (INSERT only). The standard mimic_app role
|
||||
-- has SELECT on audit_log but no UPDATE/DELETE.
|
||||
--
|
||||
-- This file runs once at container init. Production deployment uses Ansible
|
||||
-- to apply the same grants idempotently.
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'mimic_audit_writer') THEN
|
||||
CREATE ROLE mimic_audit_writer LOGIN PASSWORD 'CHANGE_ME';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- The mimic_app user is created by the official image entrypoint
|
||||
-- via $POSTGRES_USER. We only need to make sure the audit writer exists.
|
||||
-- Per-table grants are applied by the application's bootstrap step after
|
||||
-- migrations land (so the audit_log table actually exists).
|
||||
Reference in New Issue
Block a user