Covers AC-7.1→AC-7.6, AC-8.1→AC-8.6, AC-9.1→AC-9.4, AC-10.1→AC-10.5, AC-11.1→AC-11.5, AC-12.1→AC-12.4 (32 new tests, all passing). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
201 lines
6.6 KiB
TypeScript
201 lines
6.6 KiB
TypeScript
/**
|
|
* 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<Simulation> {
|
|
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<void> {
|
|
const client = makeClient(token);
|
|
await client.delete(`/simulations/${simId}`);
|
|
}
|
|
|
|
async function advanceToReviewRequired(
|
|
redteamToken: string,
|
|
simId: number,
|
|
): Promise<void> {
|
|
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 pas encore en revue/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);
|
|
});
|
|
});
|