/** * US-9 — SOC analyst fills their part; redteam fields blocked. * Covers AC-9.1 → AC-9.4. */ import { test, expect } from '@playwright/test'; import { adminToken, createEngagement, deleteEngagement, deleteUserByUsername, ensureUser, login, makeClient, } from '../fixtures/api'; import { seedTokenInStorage } from '../fixtures/auth'; const REDTEAM_USER = 'us9-redteam'; const SOC_USER = 'us9-soc'; const PASS = 'us9-pass-strong'; interface Simulation { id: number; status: string; [key: string]: unknown; } async function createSimulation( token: string, engagementId: number, name = 'US-9 sim', ): Promise { const client = makeClient(token); const r = await client.post(`/engagements/${engagementId}/simulations`, { name }); if (r.status !== 201) { throw new Error(`create simulation failed: ${r.status} ${JSON.stringify(r.data)}`); } return r.data as Simulation; } async function deleteSimulation(token: string, simId: number): Promise { const client = makeClient(token); await client.delete(`/simulations/${simId}`); } async function advanceToReviewRequired( redteamToken: string, simId: number, ): Promise { const client = makeClient(redteamToken); // Trigger auto-transition to in_progress await client.patch(`/simulations/${simId}`, { name: 'ready' }); // Transition to review_required const r = await client.post(`/simulations/${simId}/transition`, { to: 'review_required', }); if (r.status !== 200) { throw new Error(`transition to review_required failed: ${r.status}`); } } test.describe('US-9 — SOC restricted edit', () => { let redteamToken: string; let socToken: string; let engagementId: number; test.beforeAll(async () => { await ensureUser(REDTEAM_USER, PASS, 'redteam'); await ensureUser(SOC_USER, PASS, 'soc'); redteamToken = (await login(REDTEAM_USER, PASS)).token; socToken = (await login(SOC_USER, PASS)).token; const eng = await createEngagement(redteamToken, { name: 'US-9 Test Engagement', start_date: '2026-01-01', }); engagementId = eng.id; }); test.afterAll(async () => { try { const tok = await adminToken(); await deleteEngagement(tok, engagementId); for (const u of [REDTEAM_USER, SOC_USER]) { await deleteUserByUsername(tok, u); } } catch { /* noop */ } }); test('AC-9.1 — soc PATCH with redteam field → 403 soc cannot edit redteam fields', async () => { const sim = await createSimulation(redteamToken, engagementId, 'AC-9.1 sim'); await advanceToReviewRequired(redteamToken, sim.id); const socClient = makeClient(socToken); // Redteam field in payload → 403 const r = await socClient.patch(`/simulations/${sim.id}`, { name: 'SOC tries to change name', }); expect(r.status).toBe(403); expect(r.data.error).toMatch(/soc cannot edit redteam fields/i); // SOC-only fields → 200 const rOk = await socClient.patch(`/simulations/${sim.id}`, { soc_comment: 'Detected', log_source: 'SIEM', logs: 'log entry', incident_number: 'INC-001', }); expect(rOk.status).toBe(200); await deleteSimulation(redteamToken, sim.id); }); test('AC-9.2 — soc PATCH blocked when status is pending or in_progress', async () => { const socClient = makeClient(socToken); // pending → 403 const simPending = await createSimulation(redteamToken, engagementId, 'AC-9.2 pending'); const rPending = await socClient.patch(`/simulations/${simPending.id}`, { soc_comment: 'too early', }); expect(rPending.status).toBe(403); expect(rPending.data.error).toMatch(/simulation not ready for SOC review/i); // in_progress → 403 const simInProgress = await createSimulation( redteamToken, engagementId, 'AC-9.2 in_progress', ); const rtClient = makeClient(redteamToken); await rtClient.patch(`/simulations/${simInProgress.id}`, { name: 'trigger' }); const rInProgress = await socClient.patch(`/simulations/${simInProgress.id}`, { soc_comment: 'still too early', }); expect(rInProgress.status).toBe(403); expect(rInProgress.data.error).toMatch(/simulation not ready for SOC review/i); await deleteSimulation(redteamToken, simPending.id); await deleteSimulation(redteamToken, simInProgress.id); }); test('AC-9.3 — edit page for soc: RT section read-only, SOC section editable when review_required', async ({ page, context, }) => { const sim = await createSimulation(redteamToken, engagementId, 'AC-9.3 sim'); await advanceToReviewRequired(redteamToken, sim.id); await seedTokenInStorage(context, socToken); await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`); // RT fields are disabled await expect(page.locator('#sim-name')).toBeDisabled(); await expect(page.locator('#sim-description')).toBeDisabled(); await expect(page.locator('#sim-commands')).toBeDisabled(); // SOC fields are enabled await expect(page.locator('#sim-log-source')).toBeEnabled(); await expect(page.locator('#sim-logs')).toBeEnabled(); await expect(page.locator('#sim-soc-comment')).toBeEnabled(); await expect(page.locator('#sim-incident')).toBeEnabled(); await deleteSimulation(redteamToken, sim.id); }); test('AC-9.4 — soc visits pending/in_progress simulation: banner visible, SOC fields disabled', async ({ page, context, }) => { // Test with pending status const simPending = await createSimulation(redteamToken, engagementId, 'AC-9.4 pending'); await seedTokenInStorage(context, socToken); await page.goto(`/engagements/${engagementId}/simulations/${simPending.id}/edit`); // Banner must be visible await expect(page.getByTestId('soc-blocked-banner')).toBeVisible(); await expect( page.getByText(/simulation not yet ready for review/i), ).toBeVisible(); // SOC fields are disabled await expect(page.locator('#sim-log-source')).toBeDisabled(); await expect(page.locator('#sim-soc-comment')).toBeDisabled(); // Test with in_progress status const simIP = await createSimulation(redteamToken, engagementId, 'AC-9.4 in_progress'); const rtClient = makeClient(redteamToken); await rtClient.patch(`/simulations/${simIP.id}`, { name: 'trigger' }); await page.goto(`/engagements/${engagementId}/simulations/${simIP.id}/edit`); await expect(page.getByTestId('soc-blocked-banner')).toBeVisible(); await expect(page.locator('#sim-log-source')).toBeDisabled(); await deleteSimulation(redteamToken, simPending.id); await deleteSimulation(redteamToken, simIP.id); }); });