Files
mimic-big/tasks/spec-decisions.md
knacky 887182cfd7 docs: update CHANGELOG + tasks for the backend skeleton sprint 0
- CHANGELOG.md: detail every B0.1..B0.8 deliverable + spec deltas
  D-008 (ttp_version coexists), D-009 (audit hash chain v1),
  D-010 (no type_annotation_map on declarative base).
- tasks/todo.md: tick every B0.x item.
- tasks/spec-decisions.md: log D-008, D-009, D-010 alongside the
  pre-existing D-001..D-007.
2026-05-21 20:39:06 +02:00

8.1 KiB

Spec decisions log

This file tracks implementation arbitrations on top of the frozen spec (Projects/Mimic — Spec.md in the RT-SecondBrain vault).

Format: one entry per decision, newest first.


2026-05-21 — Team kickoff decisions

D-001 — SOC collaboration hypothesis

Context. Devils-advocate flagged the sociological assumption that SOC analysts will cote in the live cockpit. Decision. Hypothesis accepted as-is. No paper PoC. Risk owned by lead RT.

D-002 — Mimic deployment location

Context. Spec §6 NF-network did not pin where Mimic is physically deployed. Decision. Mimic runs on RT infrastructure. SOC client connects through the existing RT reverse proxy (Caddy, out of Mimic scope). Mimic → Mythic / Home C2 through outbound VPN. RT R&D (TTP library, stealthy variants) never sits on client premises.

D-003 — Authentication strategy

Context. Spec mentions OIDC Keycloak but lab onboarding cost is high. Decision. v1 ships local auth (username/password, bcrypt, Flask server-side sessions). v2 adds Keycloak OIDC. The RBAC model is group-based from day one, so OIDC will map claims to existing groups without touching application code. SOC sessions remain a distinct mechanism (soc_session.token_opaque bcrypt hash, clear token out-of-band).

D-004 — C2 credential storage (T2)

Context. Engagement.config_json (encrypted JSON column) vs dedicated table. Decision. Dedicated table c2_credential (id, engagement_id, c2_type, config_json_fernet, version, created_at, retired_at). Active row per engagement = retired_at IS NULL, highest version. Rotation = insert + retire previous. Fernet key in env, never in DB.

D-005 — Cleanup template variable sources (T3)

Context. Jinja {{outputs.X}} source ambiguity. Decision. Two accessors:

  • {{outputs.text}}run_step.output_text (stdout/UTF-8 text).
  • {{outputs.blob("<key>")}} → reads from output_blob_ref, hard cap 10 MB (consistent with F8 evidence limit), UTF-8 decoding with latin-1 fallback, silent refusal + log entry if the blob is non-decodable. regex_extract always operates on the resulting string.

D-006 — SOC session token storage (T4)

Context. soc_session.token_opaque storage form. Decision. bcrypt hash. Clear token generated server-side at session creation, returned once in the API response, delivered out-of-band to the SOC analyst. Never re-displayable.

D-007 — Reverse proxy scope

Context. Mimic exposure to internet for SOC client access. Decision. Reverse proxy (Caddy + TLS + IP allowlist) handled by existing RT infrastructure. Mimic ships an HTTP listener on localhost only; the deployment playbook wires it behind the existing proxy.

D-008 — Group-based RBAC vs spec F11 fixed roles

Context. Spec F11 declares 3 fixed roles (rt_operator, rt_lead, soc_analyst) with an explicit permission matrix. Sprint 0 plan (B0.6, D-003) introduces group / permission / group_permission / user_group tables to prepare OIDC v2 claim-to-group mapping without code change. Decision. Group-based model accepted as an implementation layout, not a scope extension:

  • The 3 spec roles MUST exist as the 3 seeded groups at bootstrap (rt_operator, rt_lead, soc_analyst).
  • The F11 permission matrix is the canonical source: groups receive exactly the permissions of their matching role; no custom permissions UI v1.
  • Custom groups, group editing UI, or per-engagement group overrides = OUT of v1.
  • Any drift between seeded group permissions and the F11 matrix is a spec violation, not a configuration choice.

D-009 — ttp_version table forbidden (H32 reaffirmed)

Context. Sprint 0 plan (B0.2) lists ttp_version among the initial tables. Spec hypothesis H32 explicitly excludes this: "Snapshot de rejouabilité = run.snapshot_json uniquement (pas de table ttp_version séparée — simplification MVP)". Decision. Drop ttp_version from the initial migration. The ttp.version column (informational, §8) is kept. Replayability lives solely on run.snapshot_json. Re-introducing ttp_version requires explicit spec amendment through the team-lead.

D-010 — Ansible for the deployment playbook

Context. Spec §7 names Docker only on the deploy line, but D-007 references a "deployment playbook" wiring Mimic behind the existing reverse proxy. The RT team uses Ansible for infrastructure automation across projects. Decision. Deployment artifacts are Docker images (built in repo) plus an Ansible playbook (lives outside the application repo, in the RT infra repo). Mimic itself ships only the Dockerfile and a sample compose for dev; production roll-out is Ansible-driven. The README stack line is updated accordingly.

D-011 — regex_extract Jinja2 filter semantics (resolves Q-001)

Context. D-005 introduced regex_extract on Jinja templates without fixing its match-mode, no-match behaviour, group selection, or engine flavour. Backend B0.5 (templating sandbox) is starting and needs a frozen signature. Decision.

  • Enginegoogle-re2 (D-005 reaffirmed). Linear-time, no backrefs, OPSEC-safe (no ReDoS).
  • Match mode — first match only.
  • No-match — raise TemplateError("regex_extract: no match for /<pattern>/"). No silent fallback. Drifting cleanup templates must fail loudly at step run time, not on next mission.
  • Group selection — defaults to capture group 1; positional fallback to the full match when the pattern has no groups; named groups via name="<name>".
  • Signatureregex_extract(text, pattern, *, group=1, name=None).
  • Rationale — ATR/Caldera compatibility is not an objective (D-005). Fail- fast > silent string corruption when a cleanup template touches a host with unexpected output shape.

D-012 — output_blob_ref storage layout (resolves Q-002)

Context. §8 declares run_step.output_blob_ref without specifying pool, quota, format, or path. H20 says "local disk v1" only. Sprint 0 needs the layout locked because B0.5 already references {{ outputs.blob(...) }}. Decision.

  • Two separate pools
    • MIMIC_BLOB_ROOT (default /var/lib/mimic/blobs/) — binary outputs from C2Connector polling. Content-addressed layout: <aa>/<bb>/<sha256>.gz where aa/bb are the first two byte-pairs of the sha256 hex digest. gzip systematically; raw stored bytes never on disk.
    • MIMIC_EVIDENCE_ROOT (default /var/lib/mimic/evidence/) — user-uploaded evidence files (F8). Flat layout <engagement_id>/<evidence_id>.<ext>, no compression.
  • Cap per blob — 10 MB (consistent with F8 and D-005).
  • Quota — no in-app global quota v1. OS-level monitoring via Prometheus node_exporter. F12 archival pipeline will own retention/purge post-sprint-0.
  • Filesystem permissions0750, owner the mimic system user.
  • Rationale — CAS deduplicates repeated C2 outputs (same whoami, same Get-Process snapshot) for free. Evidence stays flat because uploads are one-shot and tied to an engagement scope that we want to archive whole. Two pools mean we can wire independent quotas / retention policies in v2 without migration.

Resolved open questions

  • Q-001 → D-011.
  • Q-002 → D-012.

D-013 — Hash-chain in audit_log from v1

Context. Spec H30 places the hash chain in v2; F13 / R-O5 only mandate the write-only role for v1. While implementing B0.7, adding the columns and chaining logic was a few lines and avoids a destructive migration later. Decision. prev_hash / row_hash columns ship from day one and are populated at insert time (SHA-256 of canonical record + previous hash). The chain verifier lands in v2. Cost is negligible (one SELECT + one SHA-256 per audit insert).

D-014 — Type-hinting strategy for the ORM

Context. Flask-SQLAlchemy 3 rejects a per-base type_annotation_map (the extension owns the registry). Decision. UUID primary keys use the explicit PG_UUID(as_uuid=True) type on UuidPkMixin. Foreign-key UUID columns rely on SQLAlchemy 2's built-in Uuid mapping via Mapped[uuid.UUID]. No type_annotation_map on the declarative base.