feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
import { useEffect, useState, type FormEvent } from 'react';
|
|
|
|
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
|
|
|
|
import { extractApiError } from '@/api/client';
|
|
|
|
|
import type { EngagementInput, EngagementStatus } from '@/api/types';
|
|
|
|
|
import {
|
|
|
|
|
useCreateEngagement,
|
|
|
|
|
useEngagement,
|
|
|
|
|
usePatchEngagement,
|
|
|
|
|
} from '@/hooks/useEngagements';
|
feat(frontend): c2 config card + execute modal (sprint 8 phase 1)
- frontend/src/api/c2.ts: 6 typed API calls (getC2Config, putC2Config,
deleteC2Config, testC2Config, listCallbacks, executeC2) following the
frozen M1+M2 backend contracts
- frontend/src/api/types.ts: C2Config, C2ConfigInput, C2TestResult,
C2Callback, C2CallbacksResponse, C2Task, C2ExecuteInput, C2ExecuteResponse
- frontend/src/hooks/useC2.ts: useC2Config, useUpdateC2Config,
useDeleteC2Config, useTestC2Config, useC2Callbacks, useExecuteC2
- frontend/src/components/C2ConfigCard.tsx: engagement-scoped C2 config
card (url + write-only token + verify-tls + save/delete/test-connection),
503 disabled state, ConfirmDialog on delete
- frontend/src/components/ExecuteViaC2Modal.tsx: callback picker table
(mono data cells), commands textarea pre-filled from rt.commands,
Launch disabled until row selected + non-empty commands
- frontend/src/pages/EngagementFormPage.tsx: embed C2ConfigCard in edit
mode only, admin+redteam only (canEditEngagements gate)
- frontend/src/pages/SimulationFormPage.tsx: Execute via C2 button in RT
card, visible only when !isDone && canEditRT && hasC2Config; opens modal
- Tests: 33 new tests across api/c2, components/C2ConfigCard,
components/ExecuteViaC2Modal, EngagementFormPage, SimulationFormPage
(172 total, 139 baseline + 33 new, all passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 19:50:11 +02:00
|
|
|
import { useAuth } from '@/hooks/useAuth';
|
feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
import { useToast } from '@/hooks/useToast';
|
|
|
|
|
import { FormField, Select, TextArea, TextInput } from '@/components/FormField';
|
|
|
|
|
import { LoadingState } from '@/components/LoadingState';
|
|
|
|
|
import { ErrorState } from '@/components/ErrorState';
|
feat(frontend): c2 config card + execute modal (sprint 8 phase 1)
- frontend/src/api/c2.ts: 6 typed API calls (getC2Config, putC2Config,
deleteC2Config, testC2Config, listCallbacks, executeC2) following the
frozen M1+M2 backend contracts
- frontend/src/api/types.ts: C2Config, C2ConfigInput, C2TestResult,
C2Callback, C2CallbacksResponse, C2Task, C2ExecuteInput, C2ExecuteResponse
- frontend/src/hooks/useC2.ts: useC2Config, useUpdateC2Config,
useDeleteC2Config, useTestC2Config, useC2Callbacks, useExecuteC2
- frontend/src/components/C2ConfigCard.tsx: engagement-scoped C2 config
card (url + write-only token + verify-tls + save/delete/test-connection),
503 disabled state, ConfirmDialog on delete
- frontend/src/components/ExecuteViaC2Modal.tsx: callback picker table
(mono data cells), commands textarea pre-filled from rt.commands,
Launch disabled until row selected + non-empty commands
- frontend/src/pages/EngagementFormPage.tsx: embed C2ConfigCard in edit
mode only, admin+redteam only (canEditEngagements gate)
- frontend/src/pages/SimulationFormPage.tsx: Execute via C2 button in RT
card, visible only when !isDone && canEditRT && hasC2Config; opens modal
- Tests: 33 new tests across api/c2, components/C2ConfigCard,
components/ExecuteViaC2Modal, EngagementFormPage, SimulationFormPage
(172 total, 139 baseline + 33 new, all passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 19:50:11 +02:00
|
|
|
import { C2ConfigCard } from '@/components/C2ConfigCard';
|
feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
|
|
|
|
|
const STATUS_OPTIONS: { value: EngagementStatus; label: string }[] = [
|
|
|
|
|
{ value: 'planned', label: 'Planned' },
|
|
|
|
|
{ value: 'active', label: 'Active' },
|
|
|
|
|
{ value: 'closed', label: 'Closed' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
interface FormState {
|
|
|
|
|
name: string;
|
|
|
|
|
description: string;
|
|
|
|
|
start_date: string;
|
|
|
|
|
end_date: string;
|
|
|
|
|
status: EngagementStatus;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const EMPTY: FormState = {
|
|
|
|
|
name: '',
|
|
|
|
|
description: '',
|
|
|
|
|
start_date: '',
|
|
|
|
|
end_date: '',
|
|
|
|
|
status: 'planned',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function validate(state: FormState): Partial<Record<keyof FormState, string>> {
|
|
|
|
|
const errors: Partial<Record<keyof FormState, string>> = {};
|
|
|
|
|
if (!state.name.trim()) errors.name = 'Name is required';
|
|
|
|
|
if (!state.start_date) errors.start_date = 'Start date is required';
|
|
|
|
|
if (state.end_date && state.start_date && state.end_date < state.start_date) {
|
|
|
|
|
errors.end_date = 'End date must be on or after start date';
|
|
|
|
|
}
|
|
|
|
|
return errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function EngagementFormPage(): JSX.Element {
|
|
|
|
|
const { id } = useParams<{ id: string }>();
|
|
|
|
|
const editing = Boolean(id);
|
|
|
|
|
const numericId = id ? Number(id) : undefined;
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const { push } = useToast();
|
feat(frontend): c2 config card + execute modal (sprint 8 phase 1)
- frontend/src/api/c2.ts: 6 typed API calls (getC2Config, putC2Config,
deleteC2Config, testC2Config, listCallbacks, executeC2) following the
frozen M1+M2 backend contracts
- frontend/src/api/types.ts: C2Config, C2ConfigInput, C2TestResult,
C2Callback, C2CallbacksResponse, C2Task, C2ExecuteInput, C2ExecuteResponse
- frontend/src/hooks/useC2.ts: useC2Config, useUpdateC2Config,
useDeleteC2Config, useTestC2Config, useC2Callbacks, useExecuteC2
- frontend/src/components/C2ConfigCard.tsx: engagement-scoped C2 config
card (url + write-only token + verify-tls + save/delete/test-connection),
503 disabled state, ConfirmDialog on delete
- frontend/src/components/ExecuteViaC2Modal.tsx: callback picker table
(mono data cells), commands textarea pre-filled from rt.commands,
Launch disabled until row selected + non-empty commands
- frontend/src/pages/EngagementFormPage.tsx: embed C2ConfigCard in edit
mode only, admin+redteam only (canEditEngagements gate)
- frontend/src/pages/SimulationFormPage.tsx: Execute via C2 button in RT
card, visible only when !isDone && canEditRT && hasC2Config; opens modal
- Tests: 33 new tests across api/c2, components/C2ConfigCard,
components/ExecuteViaC2Modal, EngagementFormPage, SimulationFormPage
(172 total, 139 baseline + 33 new, all passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 19:50:11 +02:00
|
|
|
const { canEditEngagements } = useAuth();
|
feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
|
|
|
|
|
const detail = useEngagement(editing ? numericId : undefined);
|
|
|
|
|
const createMutation = useCreateEngagement();
|
|
|
|
|
const patchMutation = usePatchEngagement(numericId ?? 0);
|
|
|
|
|
|
|
|
|
|
const [form, setForm] = useState<FormState>(EMPTY);
|
|
|
|
|
const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});
|
|
|
|
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
// Hydrate edit form when data arrives.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (editing && detail.data) {
|
|
|
|
|
setForm({
|
|
|
|
|
name: detail.data.name,
|
|
|
|
|
description: detail.data.description ?? '',
|
|
|
|
|
start_date: detail.data.start_date,
|
|
|
|
|
end_date: detail.data.end_date ?? '',
|
|
|
|
|
status: detail.data.status,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [editing, detail.data]);
|
|
|
|
|
|
|
|
|
|
if (editing && detail.isLoading) return <LoadingState label="Loading engagement…" />;
|
|
|
|
|
if (editing && detail.isError) {
|
|
|
|
|
return (
|
|
|
|
|
<ErrorState
|
|
|
|
|
message={extractApiError(detail.error, 'Could not load engagement')}
|
|
|
|
|
onRetry={() => detail.refetch()}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onSubmit = async (e: FormEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setSubmitError(null);
|
|
|
|
|
const v = validate(form);
|
|
|
|
|
setErrors(v);
|
|
|
|
|
if (Object.keys(v).length > 0) return;
|
|
|
|
|
|
|
|
|
|
const payload: EngagementInput = {
|
|
|
|
|
name: form.name.trim(),
|
|
|
|
|
start_date: form.start_date,
|
|
|
|
|
status: form.status,
|
|
|
|
|
};
|
|
|
|
|
if (form.description.trim()) payload.description = form.description.trim();
|
|
|
|
|
// PATCH with null clears end_date; POST with omitted leaves it null
|
|
|
|
|
if (editing) {
|
|
|
|
|
// Always include end_date for edit: '' → null to clear, otherwise value
|
|
|
|
|
payload.end_date = form.end_date === '' ? null : form.end_date;
|
|
|
|
|
} else if (form.end_date) {
|
|
|
|
|
payload.end_date = form.end_date;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (editing && numericId) {
|
|
|
|
|
await patchMutation.mutateAsync(payload);
|
|
|
|
|
push('Engagement updated', 'success');
|
|
|
|
|
navigate(`/engagements/${numericId}`);
|
|
|
|
|
} else {
|
|
|
|
|
const created = await createMutation.mutateAsync(payload);
|
|
|
|
|
push('Engagement created', 'success');
|
|
|
|
|
navigate(`/engagements/${created.id}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setSubmitError(extractApiError(err, 'Could not save engagement'));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const submitting = createMutation.isPending || patchMutation.isPending;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col gap-xl max-w-2xl">
|
|
|
|
|
<header>
|
2026-06-09 18:44:47 +02:00
|
|
|
<h1 className="text-[32px] font-medium leading-none">
|
feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
{editing ? 'Edit engagement' : 'New engagement'}
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="text-charcoal text-[16px] mt-sm">
|
|
|
|
|
{editing
|
|
|
|
|
? 'Update the engagement metadata.'
|
|
|
|
|
: 'Create a new red team mission to host simulations.'}
|
|
|
|
|
</p>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<form onSubmit={onSubmit} noValidate className="card-product flex flex-col gap-md">
|
|
|
|
|
<FormField label="Name" htmlFor="eng-name" required error={errors.name}>
|
|
|
|
|
<TextInput
|
|
|
|
|
id="eng-name"
|
|
|
|
|
name="name"
|
|
|
|
|
value={form.name}
|
|
|
|
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
|
|
|
|
<FormField label="Description" htmlFor="eng-description">
|
|
|
|
|
<TextArea
|
|
|
|
|
id="eng-description"
|
|
|
|
|
name="description"
|
|
|
|
|
value={form.description}
|
|
|
|
|
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-md">
|
|
|
|
|
<FormField
|
|
|
|
|
label="Start date"
|
|
|
|
|
htmlFor="eng-start"
|
|
|
|
|
required
|
|
|
|
|
error={errors.start_date}
|
|
|
|
|
>
|
|
|
|
|
<TextInput
|
|
|
|
|
id="eng-start"
|
|
|
|
|
type="date"
|
|
|
|
|
name="start_date"
|
|
|
|
|
value={form.start_date}
|
|
|
|
|
onChange={(e) => setForm({ ...form, start_date: e.target.value })}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
label="End date"
|
|
|
|
|
htmlFor="eng-end"
|
|
|
|
|
hint="Leave empty to clear / leave open-ended"
|
|
|
|
|
error={errors.end_date}
|
|
|
|
|
>
|
|
|
|
|
<TextInput
|
|
|
|
|
id="eng-end"
|
|
|
|
|
type="date"
|
|
|
|
|
name="end_date"
|
|
|
|
|
value={form.end_date}
|
|
|
|
|
onChange={(e) => setForm({ ...form, end_date: e.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<FormField label="Status" htmlFor="eng-status" required>
|
|
|
|
|
<Select
|
|
|
|
|
id="eng-status"
|
|
|
|
|
name="status"
|
|
|
|
|
value={form.status}
|
|
|
|
|
onChange={(e) => setForm({ ...form, status: e.target.value as EngagementStatus })}
|
|
|
|
|
options={STATUS_OPTIONS}
|
|
|
|
|
/>
|
|
|
|
|
</FormField>
|
|
|
|
|
|
|
|
|
|
{submitError ? (
|
|
|
|
|
<div role="alert" className="text-[14px] text-bloom-deep">
|
|
|
|
|
{submitError}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-md pt-sm">
|
|
|
|
|
<button type="submit" className="btn-primary" disabled={submitting}>
|
|
|
|
|
{submitting ? 'Saving…' : editing ? 'Save changes' : 'Create engagement'}
|
|
|
|
|
</button>
|
|
|
|
|
<Link
|
|
|
|
|
to={editing && numericId ? `/engagements/${numericId}` : '/engagements'}
|
|
|
|
|
className="btn-outline-ink"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
feat(frontend): c2 config card + execute modal (sprint 8 phase 1)
- frontend/src/api/c2.ts: 6 typed API calls (getC2Config, putC2Config,
deleteC2Config, testC2Config, listCallbacks, executeC2) following the
frozen M1+M2 backend contracts
- frontend/src/api/types.ts: C2Config, C2ConfigInput, C2TestResult,
C2Callback, C2CallbacksResponse, C2Task, C2ExecuteInput, C2ExecuteResponse
- frontend/src/hooks/useC2.ts: useC2Config, useUpdateC2Config,
useDeleteC2Config, useTestC2Config, useC2Callbacks, useExecuteC2
- frontend/src/components/C2ConfigCard.tsx: engagement-scoped C2 config
card (url + write-only token + verify-tls + save/delete/test-connection),
503 disabled state, ConfirmDialog on delete
- frontend/src/components/ExecuteViaC2Modal.tsx: callback picker table
(mono data cells), commands textarea pre-filled from rt.commands,
Launch disabled until row selected + non-empty commands
- frontend/src/pages/EngagementFormPage.tsx: embed C2ConfigCard in edit
mode only, admin+redteam only (canEditEngagements gate)
- frontend/src/pages/SimulationFormPage.tsx: Execute via C2 button in RT
card, visible only when !isDone && canEditRT && hasC2Config; opens modal
- Tests: 33 new tests across api/c2, components/C2ConfigCard,
components/ExecuteViaC2Modal, EngagementFormPage, SimulationFormPage
(172 total, 139 baseline + 33 new, all passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 19:50:11 +02:00
|
|
|
|
|
|
|
|
{editing && numericId && canEditEngagements && (
|
|
|
|
|
<C2ConfigCard engagementId={numericId} />
|
|
|
|
|
)}
|
feat: sprint 1 — auth + CRUD engagements
Ship the first feature end-to-end on the UI: users log in with JWT,
admins manage user accounts, and any authenticated user (per RBAC)
can create, list, view, edit, and delete engagements.
Backend (Flask + SQLAlchemy + SQLite, 63 pytest)
- User / Engagement models, Alembic 0001 initial schema
- argon2 password hashing, JWT bearer (60-min TTL), @login_required
and @role_required decorators
- 13 API endpoints under /api/*, including last-admin protection on
DELETE/PATCH user and JSON 404 on unknown /api/* paths
- `flask create-admin` CLI with duplicate / short-password handling
Frontend (React + Vite + Tailwind + TanStack Query, 20 vitest)
- Inter font bundled locally (no CDN), DESIGN.md tokens in Tailwind
- LoginPage / EngagementsList / EngagementForm / EngagementDetail /
UsersAdmin pages with role-aware UI
- Layout, ProtectedRoute, StatusBadge, FormField, LoadingState,
ErrorState, EmptyState, Toast + provider
- Axios client: Bearer interceptor, 401 → purge + /login + "Session
expirée" toast, 403 → "Accès refusé" toast (declarative <Navigate>
for already-authed users, Fragment-keyed admin user rows)
Deployment
- Single multistage Dockerfile (node:20-alpine → python:3.12-slim)
- docker/entrypoint.sh runs `flask db upgrade` before `flask run`
- Makefile: build/start/stop/restart/update/logs/create-admin/
update-mitre/test-{backend,frontend,e2e}/clean
- .env.example documenting MIMIC_JWT_SECRET / MIMIC_DB_PATH / MIMIC_PORT
- SQLite at /data/mimic.sqlite on named volume mimic-data
Acceptance suite (Playwright, 36 tests, all 27 ACs)
- e2e/ scaffold with playwright.config + auth/api fixtures
- One spec per user story (us1-bootstrap through us6-deployment)
- Portable via MIMIC_CONTAINER_CMD / MIMIC_BASE_URL (docker or podman)
Docs
- README.md with quick-start and architecture overview
- CHANGELOG.md updated with Sprint 1 deliverables
- pyrightconfig.json so the Python LSP sees backend/.venv and
resolves the `backend.app.*` absolute imports
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:37:53 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|