import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { MarkdownField } from '@/components/MarkdownField'; import { MitreTagPicker } from '@/components/MitreTagPicker'; import { Alert } from '@/components/ui/Alert'; import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; import { Modal } from '@/components/ui/Modal'; import { SectionHeader } from '@/components/ui/SectionHeader'; import { Tag } from '@/components/ui/Tag'; import { TextField } from '@/components/ui/TextField'; import { ApiError, apiDelete, apiGet, apiPost, apiPut, } from '@/lib/api'; import { type MitreTag, type MitreTagKind } from '@/lib/mitre'; import { buildTestQueryString, templateKeys, type CreateTestTemplatePayload, type OpsecLevel, type TestTemplate, type TestTemplateFilters, type TestTemplateListResponse, } from '@/lib/templates'; const OPSEC_LEVELS: OpsecLevel[] = ['low', 'medium', 'high']; interface FormState { name: string; description: string; objective: string; procedure_md: string; prerequisites_md: string; expected_result_red_md: string; expected_detection_blue_md: string; opsec_level: OpsecLevel; tags: string; expected_iocs: string; mitre_tags: MitreTag[]; } function blankForm(): FormState { return { name: '', description: '', objective: '', procedure_md: '', prerequisites_md: '', expected_result_red_md: '', expected_detection_blue_md: '', opsec_level: 'medium', tags: '', expected_iocs: '', mitre_tags: [], }; } function toForm(t: TestTemplate): FormState { return { name: t.name, description: t.description ?? '', objective: t.objective ?? '', procedure_md: t.procedure_md ?? '', prerequisites_md: t.prerequisites_md ?? '', expected_result_red_md: t.expected_result_red_md ?? '', expected_detection_blue_md: t.expected_detection_blue_md ?? '', opsec_level: t.opsec_level, tags: t.tags.join(', '), expected_iocs: t.expected_iocs.join(', '), mitre_tags: t.mitre_tags.map((tag) => ({ kind: tag.kind as MitreTagKind, id: tag.external_id, external_id: tag.external_id, name: tag.name, })), }; } function csvToList(s: string): string[] { return s.split(',').map((x) => x.trim()).filter(Boolean); } function toPayload(form: FormState): CreateTestTemplatePayload { return { name: form.name.trim(), description: form.description || null, objective: form.objective || null, procedure_md: form.procedure_md || null, prerequisites_md: form.prerequisites_md || null, expected_result_red_md: form.expected_result_red_md || null, expected_detection_blue_md: form.expected_detection_blue_md || null, opsec_level: form.opsec_level, tags: csvToList(form.tags), expected_iocs: csvToList(form.expected_iocs), mitre_tags: form.mitre_tags.map((t) => ({ kind: t.kind, external_id: t.external_id })), }; } function useTestTemplates(filters: TestTemplateFilters) { return useQuery({ queryKey: templateKeys.tests(filters), queryFn: () => apiGet(`/test-templates${buildTestQueryString(filters)}`), }); } export function AdminTestsPage() { const qc = useQueryClient(); const [filters, setFilters] = useState({}); const [editing, setEditing] = useState(null); const [creating, setCreating] = useState(false); const [form, setForm] = useState(blankForm()); const [error, setError] = useState(null); const tests = useTestTemplates(filters); const create = useMutation({ mutationFn: (payload: CreateTestTemplatePayload) => apiPost('/test-templates', payload), onSuccess: async () => { setCreating(false); setForm(blankForm()); setError(null); await qc.invalidateQueries({ queryKey: ['templates', 'tests'] }); }, onError: (e) => setError(humanError(e)), }); const update = useMutation({ mutationFn: ({ id, payload }: { id: string; payload: CreateTestTemplatePayload }) => apiPut(`/test-templates/${id}`, payload), onSuccess: async () => { setEditing(null); setError(null); await qc.invalidateQueries({ queryKey: ['templates', 'tests'] }); }, onError: (e) => setError(humanError(e)), }); const remove = useMutation({ mutationFn: (id: string) => apiDelete<{ ok: boolean }>(`/test-templates/${id}`), onSuccess: async () => { await qc.invalidateQueries({ queryKey: ['templates', 'tests'] }); }, }); function openCreate() { setForm(blankForm()); setError(null); setCreating(true); } function openEdit(t: TestTemplate) { setForm(toForm(t)); setError(null); setEditing(t); } function submit() { const payload = toPayload(form); if (!payload.name) { setError('Name is required.'); return; } if (editing) update.mutate({ id: editing.id, payload }); else create.mutate(payload); } const isModalOpen = creating || editing !== null; return ( <>
setFilters((f) => ({ ...f, q: e.target.value || undefined }))} data-testid="filter-q" /> setFilters((f) => ({ ...f, tactic: e.target.value || undefined }))} data-testid="filter-tactic" /> setFilters((f) => ({ ...f, tag: e.target.value || undefined }))} data-testid="filter-tag" />
{tests.isError && Failed to load tests.}
{tests.isLoading &&

Loading…

} {tests.data?.items.map((t) => (
opsec: {t.opsec_level} {t.mitre_tags.map((tag) => ( {tag.external_id} ))} {t.tags.map((tg) => ( #{tg} ))}
))} {tests.data && tests.data.items.length === 0 && !tests.isLoading && (

No tests match the current filters.

)}
{ setCreating(false); setEditing(null); }} testid="test-template-modal" > {error && {error}}
setForm((f) => ({ ...f, name: e.target.value }))} data-testid="form-name" /> setForm((f) => ({ ...f, description: e.target.value }))} data-testid="form-description" /> setForm((f) => ({ ...f, objective: e.target.value }))} /> setForm((f) => ({ ...f, procedure_md: v }))} data-testid="form-procedure" hint="Step-by-step playbook. Markdown supported." /> setForm((f) => ({ ...f, prerequisites_md: v }))} />
setForm((f) => ({ ...f, expected_result_red_md: v }))} rows={4} /> setForm((f) => ({ ...f, expected_detection_blue_md: v }))} rows={4} />
setForm((f) => ({ ...f, tags: e.target.value }))} data-testid="form-tags" hint="e.g. phish, persistence, quick-win" /> setForm((f) => ({ ...f, expected_iocs: e.target.value }))} hint="Indicators the blue team should look for" />

MITRE ATT&CK tags

setForm((f) => ({ ...f, mitre_tags: next }))} />
); } function humanError(e: unknown): string { if (e instanceof ApiError) { const p = e.payload as { error?: string; message?: string } | null; return p?.message ?? p?.error ?? `HTTP ${e.status}`; } return e instanceof Error ? e.message : 'Unexpected error'; }