2026-05-26 11:35:29 +02:00
|
|
|
/**
|
|
|
|
|
* US-7 — redteam creates a simulation inside an engagement.
|
|
|
|
|
* Covers AC-7.1 → AC-7.6.
|
|
|
|
|
*/
|
|
|
|
|
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 = 'us7-redteam';
|
|
|
|
|
const SOC_USER = 'us7-soc';
|
|
|
|
|
const PASS = 'us7-pass-strong';
|
|
|
|
|
|
|
|
|
|
interface Simulation {
|
|
|
|
|
id: number;
|
|
|
|
|
engagement_id: number;
|
|
|
|
|
name: string;
|
|
|
|
|
status: string;
|
|
|
|
|
created_at: string;
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createSimulation(
|
|
|
|
|
token: string,
|
|
|
|
|
engagementId: number,
|
|
|
|
|
payload: { name: string },
|
|
|
|
|
): Promise<Simulation> {
|
|
|
|
|
const client = makeClient(token);
|
|
|
|
|
const r = await client.post(`/engagements/${engagementId}/simulations`, payload);
|
|
|
|
|
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<void> {
|
|
|
|
|
const client = makeClient(token);
|
|
|
|
|
await client.delete(`/simulations/${simId}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test.describe('US-7 — simulation create', () => {
|
|
|
|
|
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-7 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-7.1 — POST creates simulation with status pending, name required', async () => {
|
|
|
|
|
const sim = await createSimulation(redteamToken, engagementId, { name: 'Test sim 7.1' });
|
|
|
|
|
expect(sim.id).toBeTruthy();
|
|
|
|
|
expect(sim.engagement_id).toBe(engagementId);
|
|
|
|
|
expect(sim.name).toBe('Test sim 7.1');
|
|
|
|
|
expect(sim.status).toBe('pending');
|
|
|
|
|
expect(sim.created_at).toBeTruthy();
|
|
|
|
|
|
|
|
|
|
// name required: blank name → 400
|
|
|
|
|
const client = makeClient(redteamToken);
|
|
|
|
|
const r = await client.post(`/engagements/${engagementId}/simulations`, { name: '' });
|
|
|
|
|
expect(r.status).toBe(400);
|
|
|
|
|
|
|
|
|
|
await deleteSimulation(redteamToken, sim.id);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('AC-7.2 — soc role → 403 on POST', async () => {
|
|
|
|
|
const client = makeClient(socToken);
|
|
|
|
|
const r = await client.post(`/engagements/${engagementId}/simulations`, {
|
|
|
|
|
name: 'soc-blocked',
|
|
|
|
|
});
|
|
|
|
|
expect(r.status).toBe(403);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('AC-7.3 — unknown engagement → 404; existing engagement with no sims → empty list', async () => {
|
|
|
|
|
const client = makeClient(redteamToken);
|
|
|
|
|
|
|
|
|
|
const r404 = await client.post('/engagements/999999/simulations', { name: 'ghost' });
|
|
|
|
|
expect(r404.status).toBe(404);
|
|
|
|
|
|
|
|
|
|
// Create a fresh engagement with no sims and verify list is empty
|
|
|
|
|
const freshEng = await createEngagement(redteamToken, {
|
|
|
|
|
name: 'US-7 empty engagement',
|
|
|
|
|
start_date: '2026-01-01',
|
|
|
|
|
});
|
|
|
|
|
const listR = await client.get(`/engagements/${freshEng.id}/simulations`);
|
|
|
|
|
expect(listR.status).toBe(200);
|
|
|
|
|
expect(listR.data).toEqual([]);
|
|
|
|
|
|
|
|
|
|
await deleteEngagement(redteamToken, freshEng.id);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('AC-7.4 — GET list returns sims ordered by created_at desc', async () => {
|
|
|
|
|
const client = makeClient(redteamToken);
|
|
|
|
|
|
|
|
|
|
const s1 = await createSimulation(redteamToken, engagementId, { name: 'First sim' });
|
|
|
|
|
// Small delay so created_at timestamps differ
|
|
|
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
|
|
|
const s2 = await createSimulation(redteamToken, engagementId, { name: 'Second sim' });
|
|
|
|
|
|
|
|
|
|
const listR = await client.get(`/engagements/${engagementId}/simulations`);
|
|
|
|
|
expect(listR.status).toBe(200);
|
|
|
|
|
const list: Simulation[] = listR.data;
|
|
|
|
|
expect(list.length).toBeGreaterThanOrEqual(2);
|
|
|
|
|
// Most recent first
|
|
|
|
|
const ids = list.map((s) => s.id);
|
|
|
|
|
expect(ids.indexOf(s2.id)).toBeLessThan(ids.indexOf(s1.id));
|
|
|
|
|
|
|
|
|
|
await deleteSimulation(redteamToken, s1.id);
|
|
|
|
|
await deleteSimulation(redteamToken, s2.id);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('AC-7.5 — /engagements/:eid shows Simulations section with list + "Nouvelle simulation" button for redteam', async ({
|
|
|
|
|
page,
|
|
|
|
|
context,
|
|
|
|
|
}) => {
|
|
|
|
|
const sim = await createSimulation(redteamToken, engagementId, { name: 'Visible sim' });
|
|
|
|
|
|
|
|
|
|
await seedTokenInStorage(context, redteamToken);
|
|
|
|
|
await page.goto(`/engagements/${engagementId}`);
|
|
|
|
|
|
|
|
|
|
// Simulations section visible
|
|
|
|
|
await expect(page.getByRole('heading', { name: /simulations/i })).toBeVisible();
|
|
|
|
|
|
|
|
|
|
// Required columns
|
|
|
|
|
for (const col of ['Name', 'MITRE', 'Status', 'Executed at']) {
|
|
|
|
|
await expect(page.getByRole('columnheader', { name: new RegExp(col, 'i') })).toBeVisible();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The created simulation row is visible
|
|
|
|
|
await expect(page.getByRole('row', { name: /Visible sim/i })).toBeVisible();
|
|
|
|
|
|
2026-05-26 16:13:33 +02:00
|
|
|
// "New simulation" button visible for redteam
|
2026-05-26 11:35:29 +02:00
|
|
|
await expect(
|
2026-05-26 16:13:33 +02:00
|
|
|
page.getByRole('link', { name: /new simulation/i }),
|
2026-05-26 11:35:29 +02:00
|
|
|
).toBeVisible();
|
|
|
|
|
|
2026-05-26 16:13:33 +02:00
|
|
|
// SOC should NOT see "New simulation" button
|
2026-05-26 11:35:29 +02:00
|
|
|
await seedTokenInStorage(context, socToken);
|
|
|
|
|
await page.goto(`/engagements/${engagementId}`);
|
2026-05-26 16:13:33 +02:00
|
|
|
await expect(page.getByRole('link', { name: /new simulation/i })).toHaveCount(0);
|
2026-05-26 11:35:29 +02:00
|
|
|
|
|
|
|
|
await deleteSimulation(redteamToken, sim.id);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('AC-7.6 — clicking a simulation row navigates to /engagements/:eid/simulations/:sid/edit', async ({
|
|
|
|
|
page,
|
|
|
|
|
context,
|
|
|
|
|
}) => {
|
|
|
|
|
const sim = await createSimulation(redteamToken, engagementId, { name: 'Click me sim' });
|
|
|
|
|
|
|
|
|
|
await seedTokenInStorage(context, redteamToken);
|
|
|
|
|
await page.goto(`/engagements/${engagementId}`);
|
|
|
|
|
|
|
|
|
|
// Click the sim name link
|
|
|
|
|
await page.getByRole('link', { name: 'Click me sim' }).click();
|
|
|
|
|
await page.waitForURL(
|
|
|
|
|
new RegExp(`/engagements/${engagementId}/simulations/${sim.id}/edit`),
|
|
|
|
|
);
|
|
|
|
|
await expect(page.url()).toContain(
|
|
|
|
|
`/engagements/${engagementId}/simulations/${sim.id}/edit`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await deleteSimulation(redteamToken, sim.id);
|
|
|
|
|
});
|
|
|
|
|
});
|