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>
This commit is contained in:
81
frontend/src/hooks/useC2.ts
Normal file
81
frontend/src/hooks/useC2.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
deleteC2Config,
|
||||
executeC2,
|
||||
getC2Config,
|
||||
listCallbacks,
|
||||
putC2Config,
|
||||
testC2Config,
|
||||
} from '@/api/c2';
|
||||
import type { C2ConfigInput, C2ExecuteInput } from '@/api/types';
|
||||
|
||||
function c2ConfigKey(engagementId: number) {
|
||||
return ['c2-config', engagementId] as const;
|
||||
}
|
||||
|
||||
function c2CallbacksKey(engagementId: number) {
|
||||
return ['c2-callbacks', engagementId] as const;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user