/** * US-13 — redteam selects multiple MITRE techniques per simulation. * Covers AC-13.1 → AC-13.5 (API / data contract focus). */ import { test, expect } from '@playwright/test'; import { adminToken, createEngagement, deleteEngagement, deleteUserByUsername, ensureUser, login, makeClient, } from '../fixtures/api'; const REDTEAM_USER = 'us13-redteam'; const SOC_USER = 'us13-soc'; const PASS = 'us13-pass-strong'; interface Simulation { id: number; status: string; techniques: { id: string; name: string; tactics: string[] }[]; [key: string]: unknown; } async function createSimulation( token: string, engagementId: number, name = 'US-13 sim', ): Promise { const client = makeClient(token); const r = await client.post(`/engagements/${engagementId}/simulations`, { name }); if (r.status !== 201) throw new Error(`create sim: ${r.status} ${JSON.stringify(r.data)}`); return r.data as Simulation; } async function deleteSimulation(token: string, simId: number): Promise { await makeClient(token).delete(`/simulations/${simId}`); } test.describe('US-13 — multi-technique simulations', () => { 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-13 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-13.1 — simulation serialisation has techniques array, not scalar MITRE fields', async () => { const sim = await createSimulation(redteamToken, engagementId, 'AC-13.1 sim'); expect(Array.isArray(sim.techniques)).toBe(true); expect(sim.techniques).toHaveLength(0); expect(sim).not.toHaveProperty('mitre_technique_id'); expect(sim).not.toHaveProperty('mitre_technique_name'); await deleteSimulation(redteamToken, sim.id); }); test('AC-13.2 — migration: new simulations start with techniques = []', async () => { // Migration is tested implicitly: every new simulation created via POST must // return techniques: [] (no scalar columns present). const sim = await createSimulation(redteamToken, engagementId, 'AC-13.2 migration sim'); expect(sim.techniques).toEqual([]); await deleteSimulation(redteamToken, sim.id); }); test('AC-13.3 — serialisation enriches each technique entry with tactics from bundle', async () => { const sim = await createSimulation(redteamToken, engagementId, 'AC-13.3 sim'); const client = makeClient(redteamToken); const r = await client.patch(`/simulations/${sim.id}`, { technique_ids: ['T1059', 'T1078'], }); expect(r.status).toBe(200); const techniques: { id: string; name: string; tactics: string[] }[] = r.data.techniques; expect(techniques).toHaveLength(2); // Each entry has id, name, and tactics (derived from bundle at serialize time) for (const t of techniques) { expect(t).toHaveProperty('id'); expect(t).toHaveProperty('name'); expect(Array.isArray(t.tactics)).toBe(true); expect(t.tactics.length).toBeGreaterThan(0); } const t1059 = techniques.find((t) => t.id === 'T1059'); expect(t1059).toBeTruthy(); expect(t1059!.name).toBe('Command and Scripting Interpreter'); expect(t1059!.tactics).toContain('execution'); await deleteSimulation(redteamToken, sim.id); }); test('AC-13.4 — PATCH technique_ids: valid IDs stored, unknown ID → 400', async () => { const sim = await createSimulation(redteamToken, engagementId, 'AC-13.4 sim'); const client = makeClient(redteamToken); // Valid IDs const rOk = await client.patch(`/simulations/${sim.id}`, { technique_ids: ['T1059', 'T1078', 'T1566'], }); expect(rOk.status).toBe(200); const ids = (rOk.data.techniques as { id: string }[]).map((t) => t.id); expect(ids).toContain('T1059'); expect(ids).toContain('T1078'); expect(ids).toContain('T1566'); // Unknown ID → 400 const rBad = await client.patch(`/simulations/${sim.id}`, { technique_ids: ['T9999'], }); expect(rBad.status).toBe(400); expect(rBad.data.error).toMatch(/unknown technique id.*T9999/i); // Dedup: sending T1059 twice keeps only one entry in order const rDedup = await client.patch(`/simulations/${sim.id}`, { technique_ids: ['T1059', 'T1078', 'T1059'], }); expect(rDedup.status).toBe(200); const dedupIds = (rDedup.data.techniques as { id: string }[]).map((t) => t.id); expect(dedupIds).toEqual(['T1059', 'T1078']); await deleteSimulation(redteamToken, sim.id); }); test('AC-13.4 — SOC PATCH technique_ids → 403', async () => { const sim = await createSimulation(redteamToken, engagementId, 'AC-13.4 soc block'); // Advance to review_required so SOC can attempt a patch const rtClient = makeClient(redteamToken); await rtClient.patch(`/simulations/${sim.id}`, { name: 'trigger' }); await rtClient.post(`/simulations/${sim.id}/transition`, { to: 'review_required' }); const socClient = makeClient(socToken); const r = await socClient.patch(`/simulations/${sim.id}`, { technique_ids: ['T1059'], }); expect(r.status).toBe(403); expect(r.data.error).toMatch(/soc cannot edit redteam fields/i); await deleteSimulation(redteamToken, sim.id); }); test('AC-13.5 — auto-transition pending→in_progress triggered by non-empty technique_ids', async () => { const sim = await createSimulation(redteamToken, engagementId, 'AC-13.5 auto-transition'); expect(sim.status).toBe('pending'); const client = makeClient(redteamToken); // Non-empty technique_ids → triggers auto-transition const r = await client.patch(`/simulations/${sim.id}`, { technique_ids: ['T1059'], }); expect(r.status).toBe(200); expect(r.data.status).toBe('in_progress'); await deleteSimulation(redteamToken, sim.id); }); test('AC-13.5 — empty technique_ids does NOT trigger auto-transition', async () => { const sim = await createSimulation(redteamToken, engagementId, 'AC-13.5 no-trigger'); expect(sim.status).toBe('pending'); const client = makeClient(redteamToken); const r = await client.patch(`/simulations/${sim.id}`, { technique_ids: [], }); expect(r.status).toBe(200); expect(r.data.status).toBe('pending'); await deleteSimulation(redteamToken, sim.id); }); });