Files
mimic/e2e/tests/us13-multi-techniques.spec.ts

196 lines
6.8 KiB
TypeScript
Raw Permalink Normal View History

/**
* 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<Simulation> {
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<void> {
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);
});
});