diff --git a/frontend/src/mocks/fixtures.ts b/frontend/src/mocks/fixtures.ts new file mode 100644 index 0000000..66853c6 --- /dev/null +++ b/frontend/src/mocks/fixtures.ts @@ -0,0 +1,182 @@ +/** + * Sprint 0 fixtures — purely static, no API call. + * Shape is suggestive, not authoritative; final shape lands when the backend + * publishes OpenAPI in sprint 1+. + */ + +export interface MockEngagement { + id: string; + client: string; + codename: string; + status: 'planning' | 'active' | 'reporting' | 'archived'; + startDate: string; + endDate: string; + c2Type: 'mythic' | 'home'; + operators: number; + socAnalysts: number; +} + +export interface MockTtp { + id: string; + technique: string; + subtechnique?: string; + name: string; + payloadType: 'cmd' | 'powershell' | 'bof' | 'dotnet_assembly' | 'pe_exe' | 'shellcode'; + source: 'custom' | 'atr' | 'mission'; + tags: string[]; + stealth?: 'variant' | null; +} + +export interface MockHost { + id: string; + hostname: string; + ip: string; + os: string; + c2Session: string; + status: 'live' | 'idle' | 'lost'; +} + +export interface MockRunStep { + id: string; + orderIdx: number; + ttpName: string; + technique: string; + host: string; + payloadType: string; + status: 'pending' | 'running' | 'success' | 'failed' | 'aborted' | 'skipped'; + startedAt?: string; + endedAt?: string; + exitCode?: number; + output?: string; + detection?: { + level: 'detected' | 'partial' | 'missed'; + source: 'edr' | 'ndr' | 'siem' | 'manual'; + latencyMs: number; + by: string; + }; + evidence?: { + status: 'success' | 'partial' | 'failed'; + note: string; + by: string; + }; + cleanup?: { + status: 'pending' | 'success' | 'failed' | 'partial'; + }; +} + +export const MOCK_ENGAGEMENTS: MockEngagement[] = [ + { + id: 'eng_42', + client: 'Démo Client X', + codename: 'OPERATION RUSTED ANCHOR', + status: 'active', + startDate: '2026-05-18', + endDate: '2026-05-30', + c2Type: 'mythic', + operators: 4, + socAnalysts: 3, + }, + { + id: 'eng_41', + client: 'Banque Régionale Y', + codename: 'OPERATION PALE TIDE', + status: 'reporting', + startDate: '2026-04-22', + endDate: '2026-05-12', + c2Type: 'home', + operators: 4, + socAnalysts: 2, + }, + { + id: 'eng_38', + client: 'Cabinet Z', + codename: 'OPERATION LONG SHADOW', + status: 'archived', + startDate: '2026-02-10', + endDate: '2026-03-04', + c2Type: 'mythic', + operators: 2, + socAnalysts: 2, + }, +]; + +export const MOCK_HOSTS: MockHost[] = [ + { id: 'h_01', hostname: 'WIN-EXEC-04', ip: '10.42.1.18', os: 'Windows 11 22H2', c2Session: 'cbk-7a31', status: 'live' }, + { id: 'h_02', hostname: 'SRV-DC-01', ip: '10.42.0.5', os: 'Windows Server 2022', c2Session: 'cbk-7a32', status: 'live' }, + { id: 'h_03', hostname: 'WIN-DEV-12', ip: '10.42.4.66', os: 'Windows 10 22H2', c2Session: 'cbk-7a33', status: 'idle' }, +]; + +export const MOCK_TTPS: MockTtp[] = [ + { id: 't_001', technique: 'T1059.001', name: 'PowerShell Encoded Command', payloadType: 'powershell', source: 'mission', tags: ['execution', 'fileless'], stealth: 'variant' }, + { id: 't_002', technique: 'T1003.001', name: 'LSASS Memory Dump (BOF)', payloadType: 'bof', source: 'custom', tags: ['credential-access'], stealth: 'variant' }, + { id: 't_003', technique: 'T1055', name: 'Process Injection via APC', payloadType: 'shellcode', source: 'custom', tags: ['defense-evasion'] }, + { id: 't_004', technique: 'T1070.004', name: 'Indicator Removal — File Deletion', payloadType: 'cmd', source: 'atr', tags: ['defense-evasion'] }, + { id: 't_005', technique: 'T1135', name: 'Network Share Discovery', payloadType: 'cmd', source: 'atr', tags: ['discovery'] }, + { id: 't_006', technique: 'T1218.011', name: 'Rundll32 Execution', payloadType: 'cmd', source: 'mission', tags: ['defense-evasion'], stealth: 'variant' }, + { id: 't_007', technique: 'T1547.001', name: 'Registry Run Key Persistence', payloadType: 'cmd', source: 'atr', tags: ['persistence'] }, + { id: 't_008', technique: 'T1033', name: 'System Owner Discovery', payloadType: 'cmd', source: 'atr', tags: ['discovery'] }, +]; + +export const MOCK_RUN_STEPS: MockRunStep[] = [ + { + id: 's_01', + orderIdx: 1, + ttpName: 'System Owner Discovery', + technique: 'T1033', + host: 'WIN-EXEC-04', + payloadType: 'cmd', + status: 'success', + startedAt: '14:02:11', + endedAt: '14:02:12', + exitCode: 0, + output: 'mimic\\j.doe', + detection: { level: 'detected', source: 'edr', latencyMs: 1840, by: 'soc.alice' }, + evidence: { status: 'success', note: 'whoami returned domain user, identity confirmed.', by: 'rt.mickey' }, + cleanup: { status: 'success' }, + }, + { + id: 's_02', + orderIdx: 2, + ttpName: 'Network Share Discovery', + technique: 'T1135', + host: 'WIN-EXEC-04', + payloadType: 'cmd', + status: 'success', + startedAt: '14:02:18', + endedAt: '14:02:20', + exitCode: 0, + output: '\\\\SRV-FILE-01\\Public\n\\\\SRV-FILE-01\\Backup', + detection: { level: 'partial', source: 'siem', latencyMs: 14200, by: 'soc.alice' }, + evidence: { status: 'success', note: 'Two shares enumerated.', by: 'rt.mickey' }, + cleanup: { status: 'success' }, + }, + { + id: 's_03', + orderIdx: 3, + ttpName: 'PowerShell Encoded Command', + technique: 'T1059.001', + host: 'WIN-EXEC-04', + payloadType: 'powershell', + status: 'running', + startedAt: '14:02:31', + output: 'transmitting…', + }, + { + id: 's_04', + orderIdx: 4, + ttpName: 'LSASS Memory Dump (BOF)', + technique: 'T1003.001', + host: 'WIN-EXEC-04', + payloadType: 'bof', + status: 'pending', + }, + { + id: 's_05', + orderIdx: 5, + ttpName: 'Registry Run Key Persistence', + technique: 'T1547.001', + host: 'WIN-DEV-12', + payloadType: 'cmd', + status: 'pending', + }, +]; diff --git a/frontend/src/mocks/session.ts b/frontend/src/mocks/session.ts new file mode 100644 index 0000000..74dc68d --- /dev/null +++ b/frontend/src/mocks/session.ts @@ -0,0 +1,60 @@ +import type { SessionUser } from '@/types/roles'; + +/** + * Sprint 0 mock — no backend yet. The session is selected from /login + * and persisted in sessionStorage so route navigations preserve role. + * Real auth lands later (D-003: local user/password v1, Keycloak OIDC v2). + */ + +const STORAGE_KEY = 'mimic.mock.session'; + +export const MOCK_SESSIONS: Record = { + rt_operator: { + id: 'usr_001', + displayName: 'M. Dubreuil', + role: 'rt_operator', + engagementId: 'eng_42', + engagementName: 'Démo Client X', + }, + rt_lead: { + id: 'usr_002', + displayName: 'A. Verlhac', + role: 'rt_lead', + engagementId: 'eng_42', + engagementName: 'Démo Client X', + }, + soc_analyst: { + id: 'usr_soc_07', + displayName: 'SOC · session #07', + role: 'soc_analyst', + engagementId: 'eng_42', + engagementName: 'Démo Client X', + }, +}; + +export function readMockSession(): SessionUser | null { + try { + const raw = sessionStorage.getItem(STORAGE_KEY); + if (!raw) return null; + const parsed: unknown = JSON.parse(raw); + if ( + typeof parsed === 'object' && + parsed !== null && + 'role' in parsed && + typeof (parsed as SessionUser).role === 'string' + ) { + return parsed as SessionUser; + } + return null; + } catch { + return null; + } +} + +export function writeMockSession(user: SessionUser): void { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(user)); +} + +export function clearMockSession(): void { + sessionStorage.removeItem(STORAGE_KEY); +} diff --git a/frontend/src/screens/audit/AuditPage.tsx b/frontend/src/screens/audit/AuditPage.tsx new file mode 100644 index 0000000..40f8c08 --- /dev/null +++ b/frontend/src/screens/audit/AuditPage.tsx @@ -0,0 +1,69 @@ +import { Panel } from '@/components/ui/Panel'; +import { Pill } from '@/components/ui/Pill'; + +interface AuditEntry { + ts: string; + actor: string; + role: 'rt_operator' | 'rt_lead' | 'soc_analyst' | 'system'; + action: string; + resource: string; +} + +const MOCK_AUDIT: AuditEntry[] = [ + { ts: '14:48:31', actor: 'a.verlhac', role: 'rt_lead', action: 'report.generate', resource: 'report:rep_042_v1' }, + { ts: '14:32:09', actor: 'rt.mickey', role: 'rt_operator', action: 'cleanup.trigger', resource: 'run_step:s_02' }, + { ts: '14:31:55', actor: 'soc.alice', role: 'soc_analyst', action: 'detection.add', resource: 'run_step:s_02' }, + { ts: '14:02:31', actor: 'a.verlhac', role: 'rt_lead', action: 'run.start', resource: 'run:RUN-2026-05-21-A03' }, + { ts: '14:02:30', actor: 'system', role: 'system', action: 'run.snapshot', resource: 'run:RUN-2026-05-21-A03' }, + { ts: '13:55:14', actor: 'a.verlhac', role: 'rt_lead', action: 'soc_session.issue', resource: 'soc_session:msoc_071' }, + { ts: '13:40:02', actor: 'rt.mickey', role: 'rt_operator', action: 'ttp.create', resource: 'ttp:t_006' }, +]; + +const ROLE_TONE = { + rt_operator: 'rt', + rt_lead: 'rt', + soc_analyst: 'soc', + system: 'neutral', +} as const; + +export function AuditPage() { + return ( +
+
+
// Audit log · F13 · lead RT only
+

+ Append-only journal +

+

+ Postgres write-only role, weekly rotation as JSON Lines compressed. Active retention 30 days. +

+
+ + last 50 entries · oldest at the bottom}> +
    + {MOCK_AUDIT.map((entry, idx) => ( +
  1. + + {entry.ts} + + + {entry.actor} + + {entry.role} + + {entry.action} + + + {entry.resource} + +
  2. + ))} +
+
+
+ ); +} diff --git a/frontend/src/screens/cockpit/LiveCockpitPage.tsx b/frontend/src/screens/cockpit/LiveCockpitPage.tsx new file mode 100644 index 0000000..8d987e6 --- /dev/null +++ b/frontend/src/screens/cockpit/LiveCockpitPage.tsx @@ -0,0 +1,389 @@ +import { useMemo, useState } from 'react'; +import { Panel } from '@/components/ui/Panel'; +import { Pill } from '@/components/ui/Pill'; +import { Button } from '@/components/ui/Button'; +import { MOCK_RUN_STEPS } from '@/mocks/fixtures'; +import type { MockRunStep } from '@/mocks/fixtures'; +import { useSession } from '@/session/useSession'; +import { isLead, isSOC } from '@/types/roles'; + +/** + * Live cockpit — the cornerstone screen for F5/F6/F7/F8/F15. + * Three columns: + * [ Steps timeline ] [ Step detail (output, evidence, detection) ] [ Side rail ] + * + * The control bar at the top is reserved to lead RT (F6); SOC analysts see + * the cotation surface in the right column. Operators see evidence capture. + */ + +const STATUS_TONE: Record = { + success: 'success', + running: 'running', + pending: 'pending', + failed: 'failed', + aborted: 'aborted', + skipped: 'aborted', +}; + +export function LiveCockpitPage() { + const { user } = useSession(); + const steps = MOCK_RUN_STEPS; + const [selectedId, setSelectedId] = useState( + steps.find((s) => s.status === 'running')?.id ?? steps[0]?.id ?? '', + ); + + const selected = useMemo( + () => steps.find((s) => s.id === selectedId) ?? steps[0], + [steps, selectedId], + ); + + const stats = useMemo(() => { + const total = steps.length; + const done = steps.filter((s) => s.status === 'success' || s.status === 'failed').length; + const detected = steps.filter((s) => s.detection?.level === 'detected').length; + const missed = steps.filter((s) => s.detection?.level === 'missed').length; + const partial = steps.filter((s) => s.detection?.level === 'partial').length; + return { total, done, detected, missed, partial }; + }, [steps]); + + return ( +
+ {/* Run header + control bar */} +
+
+
// Live run · pollign 500 ms
+

+ RUN-2026-05-21-A03 · Démo Client X +

+
+ scenario · purple_kickoff_v1 + · + c2 · mythic + · + started 14:02:11 UTC +
+
+ +
+ + + + +
+ + {isLead(user?.role ?? 'rt_operator') && ( +
+ + + + +
+ )} +
+ +
+ {/* Column 1 — steps timeline */} +
+
+ Steps · timeline +
+
    + {steps.map((step) => { + const isSelected = step.id === selected?.id; + return ( +
  • + +
  • + ); + })} +
+
+ + {/* Column 2 — selected step detail */} +
+ {selected ? : } +
+ + {/* Column 3 — side rail (cotation / evidence by role) */} +
+ {selected && ( + <> + {isSOC(user?.role ?? 'soc_analyst') ? ( + + ) : ( + <> + + + + + )} + + )} +
+
+
+ ); +} + +function Stat({ + label, + value, + tone, +}: { + label: string; + value: string; + tone?: 'detected' | 'partial' | 'missed'; +}) { + const color = + tone === 'detected' + ? 'var(--status-detected)' + : tone === 'partial' + ? 'var(--status-partial)' + : tone === 'missed' + ? 'var(--status-missed)' + : 'var(--fg-default)'; + return ( +
+
{label}
+
+ {value} +
+
+ ); +} + +function detectionColor(level: 'detected' | 'partial' | 'missed'): string { + if (level === 'detected') return 'var(--status-detected)'; + if (level === 'partial') return 'var(--status-partial)'; + return 'var(--status-missed)'; +} + +function StepDetail({ step }: { step: MockRunStep }) { + return ( + <> +
+
+
+ // Step #{String(step.orderIdx).padStart(2, '0')} · {step.technique} +
+

+ {step.ttpName} +

+
+ + + {step.status.toUpperCase()} + +
+ +
+ + + + + +
+ + +
+          {step.output ?? '— no output —'}
+        
+
+ + +
+          {`# MIMIC-RUN:RUN-2026-05-21-A03\nMIMIC_RUN_ID=RUN-2026-05-21-A03 ${step.payloadType} -- ${step.ttpName.toLowerCase().replace(/\s+/g, '-')}`}
+        
+
+ + ); +} + +function Field({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function Empty() { + return ( +
+ Select a step to inspect. +
+ ); +} + +function DetectionPanel({ step, readonly }: { step: MockRunStep; readonly?: boolean }) { + const levels: Array<'detected' | 'partial' | 'missed'> = ['detected', 'partial', 'missed']; + const sources = ['EDR', 'NDR', 'SIEM', 'manual']; + return ( + +
+
+ {levels.map((l) => { + const active = step.detection?.level === l; + return ( + + ); + })} +
+
+ {sources.map((s) => ( + + {s} + + ))} +
+ {step.detection && ( +
+ latency: {step.detection.latencyMs.toLocaleString()} ms +
+ by: {step.detection.by} +
+ )} +
+
+ ); +} + +function EvidencePanel({ step }: { step: MockRunStep }) { + return ( + +
+ {step.evidence ? ( + <> +
+ + {step.evidence.status} + + + · by {step.evidence.by} + +
+

+ {step.evidence.note} +

+ + ) : ( +

No evidence captured yet.

+ )} + +
+
+ ); +} + +function CleanupPanel({ step }: { step: MockRunStep }) { + return ( + +
+
+ + {step.cleanup?.status ?? 'pending'} + + jinja2 · resolved +
+ +
+
+ ); +} diff --git a/frontend/src/screens/composer/ScenarioComposerPage.tsx b/frontend/src/screens/composer/ScenarioComposerPage.tsx new file mode 100644 index 0000000..cfdd5a6 --- /dev/null +++ b/frontend/src/screens/composer/ScenarioComposerPage.tsx @@ -0,0 +1,180 @@ +import { Panel } from '@/components/ui/Panel'; +import { Pill } from '@/components/ui/Pill'; +import { Button } from '@/components/ui/Button'; +import { MOCK_TTPS, MOCK_HOSTS, MOCK_RUN_STEPS } from '@/mocks/fixtures'; + +/** + * Scenario composer (F3) — three-column layout: + * [ TTP library filter ] [ Step sequence (drag/drop)] [ Inspector / params ] + * + * Wireframe-only: drag-drop is mocked; the inspector reflects the first step. + */ +export function ScenarioComposerPage() { + const draftSteps = MOCK_RUN_STEPS.slice(0, 5); + + return ( +
+
+
+
// Scenario composer · draft
+

+ PURPLE_KICKOFF_V1 +

+
+ engagement · eng_42 · c2_type · mythic (locked) +
+
+
+ + +
+
+ +
+ {/* TTP picker */} +
+
+
TTP library · 8 entries
+ +
+
    + {MOCK_TTPS.map((ttp) => ( +
  • +
    + + {ttp.technique} + + {ttp.stealth && stealth} +
    +
    + {ttp.name} +
    +
    + {ttp.payloadType} + · + {ttp.source} +
    +
  • + ))} +
+
+ + {/* Step sequence */} +
+ {draftSteps.length} steps}> +
    + {draftSteps.map((step) => ( +
  1. + + {String(step.orderIdx).padStart(2, '0')} + +
    +
    + {step.ttpName} +
    +
    + {step.technique} · {step.payloadType} +
    +
    +
    + {step.host} +
    +
    + +5000 ms +
    + +
  2. + ))} +
+
+ +
+
+ Drop TTP here to append a step
+
+
+ + {/* Inspector */} +
+
Inspector · step #03
+ +
+ + + +
+
+ +
+ {MOCK_HOSTS.map((h) => ( +
+ {h.hostname} + {h.status} +
+ ))} +
+
+ +
{`{# resolved server-side before send #}
+del /F /Q %TEMP%\\{{ outputs.text }}.dat
+exit 0`}
+
+
+
+
+ ); +} + +function KV({ k, v }: { k: string; v: string }) { + return ( +
+ {k} + {v} +
+ ); +} diff --git a/frontend/src/screens/engagements/EngagementsPage.tsx b/frontend/src/screens/engagements/EngagementsPage.tsx new file mode 100644 index 0000000..c4de7d2 --- /dev/null +++ b/frontend/src/screens/engagements/EngagementsPage.tsx @@ -0,0 +1,124 @@ +import type { ReactNode } from 'react'; +import { Link } from 'react-router-dom'; +import { Panel } from '@/components/ui/Panel'; +import { Pill } from '@/components/ui/Pill'; +import { Button } from '@/components/ui/Button'; +import { MOCK_ENGAGEMENTS } from '@/mocks/fixtures'; +import type { MockEngagement } from '@/mocks/fixtures'; + +const STATUS_TONE: Record = { + active: 'running', + reporting: 'soc', + archived: 'pending', + planning: 'success', +}; + +export function EngagementsPage() { + return ( +
+
+
+
// Engagements
+

+ Mission roster +

+

+ Each engagement is a multi-tenant container. Pick one to access its hosts, scenarios, + runs, and reports. +

+
+ +
+ + + {MOCK_ENGAGEMENTS.length} entries · sorted by start date + + } + > + + + + + + + + + + + + + + {MOCK_ENGAGEMENTS.map((eng, idx) => ( + + + + + + + + + + + ))} + +
CodenameClientStatusC2OperatorsSOCWindow +
+
+ {eng.codename} +
+
+ {eng.id} +
+
{eng.client} + + + {eng.status} + + + {eng.c2Type.toUpperCase()} + + {eng.operators} + + {eng.socAnalysts} + + + {eng.startDate} → {eng.endDate} + + + + + +
+
+
+ ); +} + +function Th({ children, align = 'left' }: { children?: ReactNode; align?: 'left' | 'right' }) { + return ( + + {children} + + ); +} + +function Td({ children, align = 'left' }: { children?: ReactNode; align?: 'left' | 'right' }) { + return ( + + {children} + + ); +} diff --git a/frontend/src/screens/library/TtpLibraryPage.tsx b/frontend/src/screens/library/TtpLibraryPage.tsx new file mode 100644 index 0000000..a8a0041 --- /dev/null +++ b/frontend/src/screens/library/TtpLibraryPage.tsx @@ -0,0 +1,120 @@ +import type { ReactNode } from 'react'; +import { Panel } from '@/components/ui/Panel'; +import { Pill } from '@/components/ui/Pill'; +import { Button } from '@/components/ui/Button'; +import { MOCK_TTPS } from '@/mocks/fixtures'; + +export function TtpLibraryPage() { + return ( +
+
+
+
// TTP library · F1/F2
+

+ Maison · ATT&CK mapped TTPs +

+

+ CRUD over the internal red-team library. Stealth variants travel here between missions. +

+
+
+ + + +
+
+ +
+ + t.stealth).length)} /> + t.source === 'custom').length} · ${MOCK_TTPS.filter((t) => t.source === 'atr').length} · ${MOCK_TTPS.filter((t) => t.source === 'mission').length}`} /> +
+ + filter · technique, tag, payload, source}> + + + + + + + + + + + + + {MOCK_TTPS.map((ttp, idx) => ( + + + + + + + + + + ))} + +
TechniqueNamePayloadSourceTagsStealth +
{ttp.technique}{ttp.name} + + {ttp.payloadType} + + + {ttp.source} + +
+ {ttp.tags.map((t) => ( + + {t} + + ))} +
+
{ttp.stealth ? variant : } + +
+
+
+ ); +} + +function Stat({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
+ {value} +
+
+ ); +} + +function Th({ children }: { children?: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/frontend/src/screens/login/LoginPage.tsx b/frontend/src/screens/login/LoginPage.tsx new file mode 100644 index 0000000..3c989d3 --- /dev/null +++ b/frontend/src/screens/login/LoginPage.tsx @@ -0,0 +1,300 @@ +import { useState, type FormEvent, type ReactNode } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Logo } from '@/components/brand/Logo'; +import { Button } from '@/components/ui/Button'; +import { Pill } from '@/components/ui/Pill'; +import { useSession } from '@/session/useSession'; +import { MOCK_SESSIONS } from '@/mocks/session'; +import type { Role } from '@/types/roles'; + +type Mode = 'rt' | 'soc'; + +/** + * Login screen — two distinct paths. + * + * RT operators authenticate via username/password (D-003 v1, OIDC v2). + * SOC analysts use a one-shot token (D-006: bcrypt-hashed soc_session, + * clear value delivered out-of-band by the lead RT). + * + * Sprint 0 mock: no validation. Picking a role assumes that role's mock + * session and lands the user inside the shell. + */ +export function LoginPage() { + const [mode, setMode] = useState('rt'); + const [pickedRtRole, setPickedRtRole] = useState<'rt_operator' | 'rt_lead'>('rt_lead'); + const { signIn } = useSession(); + const navigate = useNavigate(); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + const role: Role = mode === 'soc' ? 'soc_analyst' : pickedRtRole; + const user = MOCK_SESSIONS[role]; + if (!user) return; + signIn(user); + void navigate(mode === 'soc' ? '/runs' : '/engagements', { replace: true }); + }; + + return ( +
+ {/* Left rail — masthead, identity, telemetry of the platform itself */} + + + {/* Right column — the auth form, instrument-panel inset card */} +
+
+ {/* Mode switch — segmented, role-tinted */} +
+ setMode('rt')} + tone="rt" + label="RT — operator" + hint="username + password" + /> + setMode('soc')} + tone="soc" + label="SOC — analyst" + hint="session token" + /> +
+ +
+
+

+ {mode === 'rt' ? 'Operator sign-in' : 'SOC session'} +

+ {mode === 'rt' ? 'RT' : 'SOC'} +
+ + {mode === 'rt' ? ( + + ) : ( + + )} + +
+

+ {mode === 'rt' ? 'OIDC Keycloak — v2' : 'Token delivered out-of-band'} +

+ +
+ + +

+ mimic.rt.local · session ssrf-protected · audit log live +

+
+
+
+ ); +} + +interface ModeTabProps { + active: boolean; + onClick: () => void; + tone: 'rt' | 'soc'; + label: string; + hint: string; +} + +function ModeTab({ active, onClick, tone, label, hint }: ModeTabProps) { + return ( + + ); +} + +function RtForm({ + picked, + onPick, +}: { + picked: 'rt_operator' | 'rt_lead'; + onPick: (r: 'rt_operator' | 'rt_lead') => void; +}) { + return ( +
+ + +
+ Mock role (sprint 0 only) +
+ onPick('rt_operator')}> + RT Operator + + onPick('rt_lead')}> + RT Lead + +
+
+
+ ); +} + +function SocForm() { + return ( +
+ +

+ Your token was delivered out-of-band by the lead RT. +
+ Scope: single engagement, read-only on telemetry, write on detection coting. +

+
+ ); +} + +interface FieldProps { + label: string; + placeholder?: string; + type?: string; + autoComplete?: string; + spellCheck?: boolean; + mono?: boolean; +} + +function Field({ label, placeholder, type = 'text', autoComplete, spellCheck, mono }: FieldProps) { + const id = label.toLowerCase().replace(/\s+/g, '-'); + return ( +
+ + +
+ ); +} + +function RolePick({ + active, + onClick, + children, +}: { + active: boolean; + onClick: () => void; + children: ReactNode; +}) { + return ( + + ); +} diff --git a/frontend/src/screens/report/ReportPage.tsx b/frontend/src/screens/report/ReportPage.tsx new file mode 100644 index 0000000..28bab36 --- /dev/null +++ b/frontend/src/screens/report/ReportPage.tsx @@ -0,0 +1,230 @@ +import { Panel } from '@/components/ui/Panel'; +import { Pill } from '@/components/ui/Pill'; +import { Button } from '@/components/ui/Button'; +import { MOCK_RUN_STEPS } from '@/mocks/fixtures'; + +/** + * Report view (F9) — restricted MITRE matrix (techniques played only) + narration + * + table + integrity hash. PDF / JSON / Markdown export buttons. + * + * Sprint 0 mock: matrix built from MOCK_RUN_STEPS detections. + */ + +const TACTIC_ORDER = [ + 'Reconnaissance', + 'Resource Dev', + 'Initial Access', + 'Execution', + 'Persistence', + 'Privilege Esc', + 'Defense Evasion', + 'Credential Access', + 'Discovery', + 'Lateral Mvmt', + 'Collection', + 'C2', + 'Exfiltration', + 'Impact', +]; + +interface MatrixCell { + technique: string; + name: string; + level: 'detected' | 'partial' | 'missed'; + tactic: string; +} + +const MATRIX: MatrixCell[] = [ + { technique: 'T1033', name: 'System Owner', level: 'detected', tactic: 'Discovery' }, + { technique: 'T1135', name: 'Network Share', level: 'partial', tactic: 'Discovery' }, + { technique: 'T1059.001', name: 'PowerShell', level: 'detected', tactic: 'Execution' }, + { technique: 'T1003.001', name: 'LSASS Dump', level: 'missed', tactic: 'Credential Access' }, + { technique: 'T1547.001', name: 'Run Key', level: 'partial', tactic: 'Persistence' }, +]; + +export function ReportPage() { + return ( +
+
+
+
// Mission report · F9
+

+ OPERATION RUSTED ANCHOR — coverage report +

+
+ engagement · eng_42 · generated 2026-05-21 16:48 UTC · version 1 +
+
+
+ + + +
+
+ +
+ + c.level === 'detected').length)} tone="detected" /> + c.level === 'partial').length)} tone="partial" /> + c.level === 'missed').length)} tone="missed" /> +
+ + 5 techniques across 4 tactics}> +
+
+ {TACTIC_ORDER.map((tactic) => ( +
+ {tactic} +
+ ))} + {TACTIC_ORDER.map((tactic) => { + const cells = MATRIX.filter((c) => c.tactic === tactic); + return ( +
+ {cells.length === 0 ? ( +
+ — +
+ ) : ( + cells.map((c) => ( +
+
+ {c.technique} +
+
+ {c.name} +
+
+ )) + )} +
+ ); + })} +
+
+ + + +
+
+
+ + +
    + {MOCK_RUN_STEPS.map((step) => ( +
  1. + + #{String(step.orderIdx).padStart(2, '0')} + +
    +
    + {step.ttpName} +
    +
    + {step.technique} · {step.host} · {step.payloadType} +
    +
    +
    + {step.detection ? ( + {step.detection.level} + ) : ( + no cotation + )} +
    +
    + {step.detection ? `${step.detection.latencyMs} ms` : '—'} +
    +
  2. + ))} +
+
+ +
+ // integrity + + sha256 · 7c1f3a8e2b9d44a05f6b1c0eaa37d29c8d4f29c0e3e2a8b15a7f0c41d8e2b53d + +
+
+ ); +} + +function Stat({ + label, + value, + tone, +}: { + label: string; + value: string; + tone?: 'detected' | 'partial' | 'missed'; +}) { + const color = + tone === 'detected' + ? 'var(--status-detected)' + : tone === 'partial' + ? 'var(--status-partial)' + : tone === 'missed' + ? 'var(--status-missed)' + : 'var(--fg-default)'; + return ( +
+
{label}
+
+ {value} +
+
+ ); +} + +function LegendDot({ color, label }: { color: string; label: string }) { + return ( + + + {label} + + ); +}