feat(frontend): c2 tasks panel + history import (sprint 8 phase 2)
- Add getC2Tasks / listCallbackHistory / importC2 API functions + types - useC2Tasks with 2500ms polling (stops when all tasks completed) - useC2CallbackHistory, useImportC2 hooks - C2TaskStatusBadge, C2TasksPanel (expandable output rows, polling indicator) - C2CallbackPicker extracted as shared component (reused in both modals) - ImportC2HistoryModal: 2-step callback picker → paginated history table - SimulationFormPage: RT card + tasks panel share left grid column; Import C2 history button - 37 new tests (api/c2, C2TasksPanel, ImportC2HistoryModal, SimulationFormPage panel visibility) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,9 @@ import {
|
||||
deleteC2Config,
|
||||
executeC2,
|
||||
getC2Config,
|
||||
getC2Tasks,
|
||||
importC2,
|
||||
listCallbackHistory,
|
||||
listCallbacks,
|
||||
putC2Config,
|
||||
testC2Config,
|
||||
@@ -134,3 +137,63 @@ describe('executeC2', () => {
|
||||
expect(body.commands).toEqual(['whoami']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getC2Tasks', () => {
|
||||
it('GET /simulations/:id/c2/tasks returns tasks list', async () => {
|
||||
mock.onGet('/simulations/7/c2/tasks').reply(200, {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
mythic_task_display_id: 10,
|
||||
callback_display_id: 1,
|
||||
command: 'whoami',
|
||||
params: null,
|
||||
status: 'completed',
|
||||
completed: true,
|
||||
output: 'NT AUTHORITY\\SYSTEM',
|
||||
mapping_applied: true,
|
||||
created_at: '2026-06-10T10:00:00',
|
||||
completed_at: '2026-06-10T10:00:05',
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await getC2Tasks(7);
|
||||
expect(result.tasks).toHaveLength(1);
|
||||
expect(result.tasks[0].status).toBe('completed');
|
||||
expect(result.tasks[0].output).toBe('NT AUTHORITY\\SYSTEM');
|
||||
expect(mock.history['get'][0].url).toBe('/simulations/7/c2/tasks');
|
||||
});
|
||||
});
|
||||
|
||||
describe('listCallbackHistory', () => {
|
||||
it('GET with page/page_size params', async () => {
|
||||
mock.onGet('/engagements/1/c2/callbacks/2/history').reply(200, {
|
||||
tasks: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 25,
|
||||
});
|
||||
const result = await listCallbackHistory(1, 2, { page: 1, pageSize: 25 });
|
||||
expect(result.total).toBe(0);
|
||||
const req = mock.history['get'][0];
|
||||
expect(req.url).toBe('/engagements/1/c2/callbacks/2/history');
|
||||
expect(req.params).toMatchObject({ page: 1, page_size: 25 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('importC2', () => {
|
||||
it('POST /simulations/:id/c2/import with task_display_ids', async () => {
|
||||
mock.onPost('/simulations/7/c2/import').reply(200, { imported: 3, skipped: 1 });
|
||||
const result = await importC2(7, {
|
||||
callback_display_id: 2,
|
||||
task_display_ids: [10, 11, 12, 13],
|
||||
});
|
||||
expect(result.imported).toBe(3);
|
||||
expect(result.skipped).toBe(1);
|
||||
const req = mock.history['post'][0];
|
||||
expect(req.url).toBe('/simulations/7/c2/import');
|
||||
const body = JSON.parse(req.data as string);
|
||||
expect(body.callback_display_id).toBe(2);
|
||||
expect(body.task_display_ids).toEqual([10, 11, 12, 13]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user