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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user