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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
|
|
|
import {
|
|
|
|
|
deleteC2Config,
|
|
|
|
|
executeC2,
|
|
|
|
|
getC2Config,
|
2026-06-10 20:11:12 +02:00
|
|
|
getC2Tasks,
|
|
|
|
|
importC2,
|
|
|
|
|
listCallbackHistory,
|
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
|
|
|
listCallbacks,
|
|
|
|
|
putC2Config,
|
|
|
|
|
testC2Config,
|
|
|
|
|
} from '@/api/c2';
|
2026-06-10 20:11:12 +02:00
|
|
|
import type {
|
|
|
|
|
C2ConfigInput,
|
|
|
|
|
C2ExecuteInput,
|
|
|
|
|
C2ImportInput,
|
|
|
|
|
C2TasksResponse,
|
|
|
|
|
} from '@/api/types';
|
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
|
|
|
|
|
|
|
|
function c2ConfigKey(engagementId: number) {
|
|
|
|
|
return ['c2-config', engagementId] as const;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function c2CallbacksKey(engagementId: number) {
|
|
|
|
|
return ['c2-callbacks', engagementId] as const;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 20:11:12 +02:00
|
|
|
function c2TasksKey(simulationId: number) {
|
|
|
|
|
return ['c2-tasks', simulationId] as const;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function c2HistoryKey(engagementId: number, callbackDisplayId: number, page: number, pageSize: number) {
|
|
|
|
|
return ['c2-history', engagementId, callbackDisplayId, page, pageSize] as const;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
function simulationKey(id: number) {
|
|
|
|
|
return ['simulations', id] as const;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useC2Config(engagementId: number | undefined, options?: { enabled?: boolean }) {
|
|
|
|
|
const enabled =
|
|
|
|
|
typeof engagementId === 'number' &&
|
|
|
|
|
!Number.isNaN(engagementId) &&
|
|
|
|
|
(options?.enabled !== false);
|
|
|
|
|
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: engagementId ? c2ConfigKey(engagementId) : ['c2-config', 'none'],
|
|
|
|
|
queryFn: () => getC2Config(engagementId as number),
|
|
|
|
|
enabled,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useUpdateC2Config(engagementId: number) {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: (input: C2ConfigInput) => putC2Config(engagementId, input),
|
|
|
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: c2ConfigKey(engagementId) }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useDeleteC2Config(engagementId: number) {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: () => deleteC2Config(engagementId),
|
|
|
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: c2ConfigKey(engagementId) }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useTestC2Config(engagementId: number) {
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: () => testC2Config(engagementId),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useC2Callbacks(engagementId: number | undefined, options?: { enabled?: boolean }) {
|
|
|
|
|
const enabled =
|
|
|
|
|
typeof engagementId === 'number' &&
|
|
|
|
|
!Number.isNaN(engagementId) &&
|
|
|
|
|
(options?.enabled !== false);
|
|
|
|
|
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: engagementId ? c2CallbacksKey(engagementId) : ['c2-callbacks', 'none'],
|
|
|
|
|
queryFn: () => listCallbacks(engagementId as number),
|
|
|
|
|
enabled,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 20:11:12 +02:00
|
|
|
export function useC2Tasks(simulationId: number | undefined, options?: { enabled?: boolean }) {
|
|
|
|
|
const enabled =
|
|
|
|
|
typeof simulationId === 'number' &&
|
|
|
|
|
!Number.isNaN(simulationId) &&
|
|
|
|
|
(options?.enabled !== false);
|
|
|
|
|
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: simulationId ? c2TasksKey(simulationId) : ['c2-tasks', 'none'],
|
|
|
|
|
queryFn: () => getC2Tasks(simulationId as number),
|
|
|
|
|
enabled,
|
|
|
|
|
// Poll every 2500 ms while any task is incomplete; stop when all done.
|
|
|
|
|
refetchInterval: (query: { state: { data?: C2TasksResponse } }) =>
|
|
|
|
|
query.state.data?.tasks?.some((t) => !t.completed) ? 2500 : false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
export function useExecuteC2(simulationId: number, engagementId: number) {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: (input: C2ExecuteInput) => executeC2(simulationId, input),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: simulationKey(simulationId) });
|
|
|
|
|
qc.invalidateQueries({ queryKey: ['engagements', engagementId, 'simulations'] });
|
2026-06-10 20:11:12 +02:00
|
|
|
qc.invalidateQueries({ queryKey: c2TasksKey(simulationId) });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useC2CallbackHistory(
|
|
|
|
|
engagementId: number | undefined,
|
|
|
|
|
callbackDisplayId: number | null,
|
|
|
|
|
options?: { page?: number; pageSize?: number; enabled?: boolean },
|
|
|
|
|
) {
|
|
|
|
|
const page = options?.page ?? 1;
|
|
|
|
|
const pageSize = options?.pageSize ?? 25;
|
|
|
|
|
const enabled =
|
|
|
|
|
typeof engagementId === 'number' &&
|
|
|
|
|
!Number.isNaN(engagementId) &&
|
|
|
|
|
callbackDisplayId !== null &&
|
|
|
|
|
(options?.enabled !== false);
|
|
|
|
|
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey:
|
|
|
|
|
engagementId && callbackDisplayId !== null
|
|
|
|
|
? c2HistoryKey(engagementId, callbackDisplayId, page, pageSize)
|
|
|
|
|
: ['c2-history', 'none'],
|
|
|
|
|
queryFn: () =>
|
|
|
|
|
listCallbackHistory(engagementId as number, callbackDisplayId as number, {
|
|
|
|
|
page,
|
|
|
|
|
pageSize,
|
|
|
|
|
}),
|
|
|
|
|
enabled,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useImportC2(simulationId: number) {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: (input: C2ImportInput) => importC2(simulationId, input),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
qc.invalidateQueries({ queryKey: c2TasksKey(simulationId) });
|
|
|
|
|
qc.invalidateQueries({ queryKey: simulationKey(simulationId) });
|
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
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|