b52cb0e5e41fcaaa0888cc3cdf327d141c0d7993
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b52cb0e5e4 |
refactor(m4): full-bleed matrix + word-only line breaks
Two follow-up tweaks per user feedback ("wrap sur les mots, agrandit le
cadre"):
- Full-bleed wrapper: the matrix breaks out of the page's max-w-page (1400px)
constraint via `margin: 0 calc(50% - 50vw)` + `width: 100vw`, mirroring the
60px page padding internally. On wide viewports the picker now uses the
ENTIRE viewport width, so column widths grow proportionally — names that
used to wrap on 3 lines now fit on 1-2.
- Word-only wrapping: replaced `break-words` (overflow-wrap: break-word,
which falls back to mid-word breaks) with `break-normal hyphens-none`
(overflow-wrap: normal + word-break: normal). Cells break only at word
boundaries; if a single word is longer than the cell it overflows
visually rather than splitting `Aut\nhentication`-style. The grid is
configured `minmax(7rem, 1fr)` so the minimum column is wide enough for
every single word in MITRE v19 names, and stretches with available space.
- Spec §F2 rewritten as a bullet contract locking in: full-bleed, 15 cols
minmax(7rem, 1fr), word-only wrap, font sans 12px / count 10px, headers/
cells show name-only with external_id on hover + chips. Future spec-reviewer
passes can grade against this.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8742fb2b6e |
refactor(m4): match attack.mitre.org sizing — equal-width cols, name-only cells
Visual parity pass against attack.mitre.org/# per user feedback ("trop dense,
illisible, je veux la même représentation"):
- Layout switched from flex+fixed-width 224px columns to a CSS grid of
`repeat(N, minmax(0, 1fr))` so the 15 tactic columns share the container
width equally. No more horizontal scroll on a standard desktop.
- Cells now show NAME ONLY (matches mitre.org). The external_id (TA00xx /
T1xxx / T1xxx.xxx) is preserved in the chip selection bar at the top and
in the `title` hover tooltip on every cell — surfaces on demand, doesn't
consume cell real estate.
- Font: switched to `font-sans` (IBM Plex Sans) at `text-xs` (12px) across
cells, matching the mitre.org typography. Headers use the same family at
the same size with a 10px sub-line for the technique count.
- Chevron icons: ▸ (collapsed) / ▾ (expanded) — small, sub-technique count
rendered inline beside the chevron.
- Helper line below the matrix tells the user where the IDs went.
Spec §F2 + testing-m4.md walkthrough rewritten to lock the new sizing rules
in (font-xs, no external_id in cells, hover/chip for the ID, no horizontal
scroll). spec-reviewer will see the matching contract.
DoD: make e2e → 34 passed. Selectors (data-testid + aria-pressed) unchanged
so the existing M4 e2e test still walks the new layout end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7dbe2dbc28 |
refactor(m4): flatten the MITRE picker into the attack.mitre.org matrix
The hierarchical 3-column drill-down was hard to scan and forced a stateful walk per tag. Replaced with a flat, columns-as-tactics matrix that mirrors attack.mitre.org/# — every cell is a one-click select target, with inline sub-technique expand via a `+N` chevron. - New endpoint GET /api/v1/mitre/matrix returns the full grid (tactics → techniques → sub-techniques nested) in a single ~55 KB response, so the SPA renders the whole matrix without firing 15 parallel queries. Two pytest tests added (nested structure + auth required). - MitreTagPicker.tsx rewritten as a horizontal-scrolling matrix: - Click a tactic header → select the tactic (cyan filled). - Click a technique cell → select the technique (orange filled). - Click the `+N` chevron → expand sub-techniques inline within the column. - Click a sub-technique → select (purple filled). - Single Filter field matches on external_id or name across all kinds. - Selection chips at the top, clickable to remove. - `aria-pressed` on every clickable cell for screen readers and Playwright. - e2e test updated to walk the new flow (click cell → assert aria-pressed, expand chevron, click sub, verify chip + JSON preview, filter to T1078). - Spec §F2 + §F12 + todo.md M4 entry updated to make the matrix layout the canonical UI for MITRE tagging (so future spec-reviewer passes accept it). - testing-m4.md walkthrough rewritten for the flat picker. DoD post-refactor: make test-api → 53 passed (was 51), make e2e → 34 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8a1dd58c83 |
feat(m4): frontend MitreTagPicker + /mitre showcase page
- lib/mitre.ts: shared types (MitreTactic, Technique, Subtechnique, MitreTag kind/id/external_id/name) + TanStack query keys. - components/MitreTagPicker.tsx: three-column controlled picker (tactic → technique → subtechnique), multi-select with chip-removal, autocomplete on each column, ARIA labels for screen readers. Returns MitreTag[] via value/onChange — drop-in for M5 template forms. - pages/MitrePage.tsx: status card (version, source URL, last_sync), admin- gated Trigger Sync button with success/error alerts, picker showcase, JSON preview of the current selection. - Layout adds MITRE nav link for any logged-in user; App.tsx adds the /mitre route under RequireAuth. HomePage roadmap bumped to next: M5 templates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bb23bf3928 |
feat(m3): RBAC — atomic perms, groups, users, admin SPA pages
Permission catalogue (services/permissions_seed.py)
- 31 atomic codes across 10 families: user.*, group.*, invitation.*,
test_template.*, scenario_template.*, mission.* (incl.
mission.write_red_fields + mission.write_blue_fields),
detection_level.{read,update}, setting.{read,update}, mitre.sync.
- Default bindings: admin = all 31; redteam = 8 (catalogue read + mission.
{read,create,update,archive,write_red_fields} + detection_level.read);
blueteam = 5 (catalogue read + mission.{read,write_blue_fields} +
detection_level.read).
- Seed runs at boot AND after /setup so a freshly truncated DB (via
/diag/reset) gets the bindings back via the bootstrap path. Idempotent +
additive (never removes a perm from a system group).
Users admin (services/users.py + api/users.py)
- list (q + is_active filter + pagination), get, patch (display_name /
locale / is_active with tri-state sentinel for clear-vs-unset),
soft-delete, set groups.
- Last-admin protection on update (deactivate), delete, and group-strip
(refusing to remove the admin group from the last active admin).
Groups admin (services/groups.py + api/groups.py)
- Full CRUD with system-group protection (no rename, no delete on
admin/redteam/blueteam).
- PUT /groups/{id}/permissions sets the perm list.
- Admin system group's perm set is locked to the full catalogue
(SystemGroupProtected → 409) — preserves the bypass invariant even if a
future refactor moves to perm-based checks.
Permissions read-only (api/permissions.py)
- GET /permissions returns the catalogue (admin or group.read holders).
/diag/reset extension
- After truncate + token mint, the limiter is also reset (limiter.reset())
so the Playwright suite doesn't hit 10/min budgets across spec files.
Guarded by limiter.enabled to no-op in APP_ENV=test.
Rate-limit scope (core/rate_limit.py)
- enabled = APP_ENV in ("prod", "staging"). A staging deployment serves
humans, so it gets the limits too. Dev/test stay unthrottled for
Playwright ergonomics. Spec §6 NF-security is an operator-facing
requirement.
Frontend chrome
- components/RequireAdmin.tsx + ui/Modal.tsx (reusable centered dialog
with accessible name + Escape + backdrop-click).
- Layout.tsx shows Admin nav links only when is_admin === true. Server
remains the arbiter — non-admins hitting /admin/* get redirected to /.
Frontend pages
- pages/AdminUsersPage.tsx, AdminGroupsPage.tsx, AdminInvitationsPage.tsx
with edit modals using TanStack Query mutations + multi-select for perms
grouped by family + copy-once invitation URL display.
- lib/admin.ts: shared types + query keys + groupPermsByFamily helper.
- lib/api.ts: apiPatch / apiPut / apiDelete added.
Playwright config (e2e/playwright.config.ts)
- workers: 1 + fullyParallel: false: spec files share the live Postgres,
so concurrent /diag/reset calls clobber each other. Intra-file order
preserved via test.describe.configure({ mode: 'serial' }).
Testing
- backend/tests/test_rbac.py: 15 integration tests (39 backend total — 1
health + 8 schema + 15 auth + 15 RBAC).
- e2e/tests/m3-rbac.spec.ts: 8 Playwright tests covering DoD §10 #2/#3
(28 e2e total — 8 M0 + 4 M1 + 8 M2 + 8 M3).
- tasks/testing-m3.md.
DoD: make test-api → 39 passed, make e2e → 28 passed. Spec-reviewer pass
applied (admin perm invariant + staging rate-limit scope).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
700b563297 |
feat(m2): auth, JWT, invitations, bootstrap, RTOps SPA pages
Crypto + tokens
- app/core/security.py: Argon2id PasswordHasher (time_cost=2, memory_cost=
64 MiB, parallelism=2) + opaque-token SHA-256 helpers (raw token shown
once, only the hash lives in the DB).
- app/core/jwt_tokens.py: HS256, claims iss/sub/type/jti/iat/exp. Access
1h, refresh 30d.
Services
- services/auth.py: login, refresh with token rotation + reuse-detection
chain revoke, logout (idempotent), change_password (forces logout-all).
- services/invitations.py: create, preview, accept, revoke. Default 7d TTL.
- services/bootstrap.py: seeds the 3 system groups (admin/redteam/blueteam),
consumes the install token, attaches the first user to admin.
- core/install_token.py: mints, persists in settings, marks consumed,
regenerate hook for /diag/reset.
API
- POST /setup (consume install token, create 1st admin) + GET /setup
(status).
- POST /auth/{login,refresh,logout,change-password} + GET /auth/me.
- POST /invitations + GET /invitations + GET /invitations/preview/<token> +
POST /invitations/accept/<token> + POST /invitations/<id>/revoke.
- POST /diag/reset: test-only kill switch (truncate auth tables + mint
fresh install token). Allowed in dev too (with WARNING log) so the e2e
suite can run against a make-up stack; production locked out.
Middleware
- @require_auth populates g.current_user (snapshot dataclass, session
closed before request handler runs).
- @require_perm(*codes): atomic perm union check; admin group bypasses.
Perm catalogue lands in M3, scaffolding here.
- flask-limiter: 10/min/IP on /auth/login & /auth/refresh, 5/min on
/auth/change-password & /setup, 10–20/min on invitation endpoints.
Disabled in APP_ENV=test.
CLI
- flask --app app.cli metamorph print-install-token [--force]
- flask --app app.cli metamorph seed-mitre (M4 placeholder)
Refresh cookie metamorph_refresh: HttpOnly + Secure (localhost is a secure
context for modern browsers) + SameSite=Strict + Path=/api/v1/auth/.
Email validation: app.api._validation.Email permissive RFC-shape regex so
internal TLDs (.local/.corp/.test) are accepted — pydantic.EmailStr's
deliverability check is too strict for red-team labs.
Frontend
- lib/{api,auth}.ts: access token in module memory, refresh cookie,
automatic 401-retry via /auth/refresh, useAuth() hook.
- components/{Layout,RequireAuth}.tsx + ui/{TextField,Alert}.tsx.
- pages/{Login,Setup,Register,Profile}.
Testing
- tests/test_auth_flow.py: 15 integration tests (24 backend total).
- e2e/tests/m2-auth.spec.ts: 8 Playwright tests (20 e2e total).
- tasks/testing-m2.md.
DoD: make test-api → 24 passed, make e2e → 20 passed; spec-reviewer pass
applied (Secure unconditional, refresh limit 10/min/IP).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f1fdf27012 |
feat(m0): bootstrap repo, design system, compose stack
- Repo scaffolding: .gitignore, .env.example, Makefile, docker-compose.yml, README.md, CHANGELOG.md, pre-commit config. - Three-service stack: api (Flask 3), db (postgres:16-alpine), front (nginx serving the Vite bundle). Named volumes metamorph_db + metamorph_evidence. - Backend skeleton: Flask app factory, JSON structured logging on stdout, GET /api/v1/health, multi-stage Dockerfile, pyproject.toml driven by uv, Pydantic Settings with secret guard rails (refuses to boot in non-dev with placeholders), APP_ENV gating. - Frontend skeleton: Vite + React 18 + TypeScript strict + TailwindCSS, RTOps design tokens from tasks/design.md, self-hosted JetBrains Mono / IBM Plex Sans via @fontsource, base UI primitives (Card/Tag/SectionHeader/FlowNode/ Button), home page wired to /api/v1/health. - Engine-agnostic Makefile: auto-detects docker or podman, picks the matching compose driver. Targets: up/down/build/rebuild/dev/lint/fmt/test/migrate/ seed-mitre/print-install-token/e2e/inspect-health. - Playwright suite: e2e/tests/m0-smoke.spec.ts (8 tests) + HTML + JUnit reports + traces on retry. - Docs: tasks/spec.md (finalized after Q&A), tasks/design.md, tasks/todo.md (14 milestones), tasks/testing-m0.md, tasks/lessons.md. DoD: make up + make health + make e2e all pass on podman 5.x (Fedora) and docker. TLS terminated by external reverse proxy (spec §6 NF-network). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |