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:
Knacky
2026-06-10 19:50:11 +02:00
parent 53755a31d6
commit 5ff6ae8940
12 changed files with 1253 additions and 1 deletions

View File

@@ -12,12 +12,14 @@ import {
useTransitionSimulation,
useUpdateSimulation,
} from '@/hooks/useSimulations';
import { useC2Config } from '@/hooks/useC2';
import { FormField, TextArea, TextInput } from '@/components/FormField';
import { LoadingState } from '@/components/LoadingState';
import { ErrorState } from '@/components/ErrorState';
import { SimulationStatusBadge } from '@/components/SimulationStatusBadge';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { MitreTechniquesField } from '@/components/MitreTechniquesField';
import { ExecuteViaC2Modal } from '@/components/ExecuteViaC2Modal';
interface RedteamFormState {
name: string;
@@ -61,6 +63,13 @@ export function SimulationFormPage(): JSX.Element {
const { push } = useToast();
const { isAdmin, isRedteam, isSoc, canEditEngagements } = useAuth();
const canEditRT = isAdmin || isRedteam;
const c2ConfigQuery = useC2Config(
!isNew && typeof engagementId === 'number' ? engagementId : undefined,
{ enabled: !isNew && canEditRT },
);
const hasC2Config = c2ConfigQuery.data !== null && c2ConfigQuery.data !== undefined;
const detail = useSimulation(isNew ? undefined : simulationId);
const createMutation = useCreateSimulation(engagementId ?? 0);
const updateMutation = useUpdateSimulation(simulationId ?? 0, engagementId ?? 0);
@@ -72,6 +81,7 @@ export function SimulationFormPage(): JSX.Element {
const [nameError, setNameError] = useState<string | null>(null);
const [submitError, setSubmitError] = useState<string | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showC2Modal, setShowC2Modal] = useState(false);
useEffect(() => {
if (!isNew && detail.data) {
@@ -109,7 +119,6 @@ export function SimulationFormPage(): JSX.Element {
// US-18: Done = fully read-only, Reopen only
const isDone = status === 'done';
const canEditRT = isAdmin || isRedteam;
const socCanEdit = isSoc && (status === 'review_required' || status === 'done');
const socBlocked = isSoc && (status === 'pending' || status === 'in_progress');
@@ -383,6 +392,19 @@ export function SimulationFormPage(): JSX.Element {
rows={5}
/>
</FormField>
{!isDone && canEditRT && hasC2Config && (
<div className="pt-xs">
<button
type="button"
data-testid="c2-execute-btn"
className="btn-outline"
onClick={() => setShowC2Modal(true)}
>
Execute via C2
</button>
</div>
)}
</form>
{/* SOC card */}
@@ -512,6 +534,15 @@ export function SimulationFormPage(): JSX.Element {
onCancel={() => setShowDeleteConfirm(false)}
/>
)}
{showC2Modal && simulationId && typeof engagementId === 'number' && (
<ExecuteViaC2Modal
simulationId={simulationId}
engagementId={engagementId}
initialCommands={rt.commands}
onClose={() => setShowC2Modal(false)}
/>
)}
</div>
);
}