2026-06-10 20:11:12 +02:00
|
|
|
import { Fragment, useState } from 'react';
|
|
|
|
|
import { ChevronRight, ChevronDown } from 'lucide-react';
|
|
|
|
|
import { useC2Tasks } from '@/hooks/useC2';
|
|
|
|
|
import { C2TaskStatusBadge } from './C2TaskStatusBadge';
|
|
|
|
|
|
|
|
|
|
interface C2TasksPanelProps {
|
|
|
|
|
simulationId: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function C2TasksPanel({ simulationId }: C2TasksPanelProps): JSX.Element {
|
|
|
|
|
const query = useC2Tasks(simulationId, { enabled: true });
|
|
|
|
|
const [expandedIds, setExpandedIds] = useState<Set<number>>(new Set());
|
|
|
|
|
|
|
|
|
|
const tasks = query.data?.tasks ?? [];
|
|
|
|
|
const isRefreshing = query.isFetching && !query.isLoading;
|
|
|
|
|
|
|
|
|
|
function toggleExpand(id: number, completed: boolean) {
|
|
|
|
|
if (!completed) return;
|
|
|
|
|
setExpandedIds((prev) => {
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
if (next.has(id)) {
|
|
|
|
|
next.delete(id);
|
|
|
|
|
} else {
|
|
|
|
|
next.add(id);
|
|
|
|
|
}
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
data-testid="c2-tasks-panel"
|
|
|
|
|
className="card-product flex flex-col gap-md"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<h3 className="text-[16px] font-medium text-ink">C2 Tasks</h3>
|
|
|
|
|
{isRefreshing && (
|
|
|
|
|
<span
|
|
|
|
|
data-testid="c2-task-refresh-indicator"
|
|
|
|
|
className="text-[12px] text-graphite"
|
|
|
|
|
>
|
|
|
|
|
Refreshing…
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{tasks.length === 0 ? (
|
|
|
|
|
<div className="border border-hairline rounded-none px-md py-md">
|
|
|
|
|
<p className="text-[14px] text-graphite">
|
|
|
|
|
No C2 tasks yet. Use Execute via C2 to launch commands.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="border border-hairline overflow-x-auto">
|
|
|
|
|
<table className="w-full text-[14px]">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr className="bg-cloud border-b border-hairline">
|
|
|
|
|
<th className="px-md py-sm text-left font-medium text-ink w-8" aria-label="Expand" />
|
|
|
|
|
<th className="px-md py-sm text-left font-medium text-ink">Task</th>
|
|
|
|
|
<th className="px-md py-sm text-left font-medium text-ink">Command</th>
|
|
|
|
|
<th className="px-md py-sm text-left font-medium text-ink">Source</th>
|
|
|
|
|
<th className="px-md py-sm text-left font-medium text-ink">Status</th>
|
|
|
|
|
<th className="px-md py-sm text-left font-medium text-ink">Completed at</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{tasks.map((task) => {
|
|
|
|
|
const isExpanded = expandedIds.has(task.id);
|
|
|
|
|
const canExpand = task.completed && Boolean(task.output);
|
|
|
|
|
return (
|
|
|
|
|
<Fragment key={task.id}>
|
|
|
|
|
<tr
|
|
|
|
|
data-testid="c2-task-row"
|
|
|
|
|
onClick={() => toggleExpand(task.id, task.completed)}
|
2026-06-10 20:22:45 +02:00
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (canExpand && (e.key === 'Enter' || e.key === ' ')) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
toggleExpand(task.id, task.completed);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
tabIndex={canExpand ? 0 : undefined}
|
|
|
|
|
role={canExpand ? 'button' : undefined}
|
|
|
|
|
aria-expanded={canExpand ? isExpanded : undefined}
|
|
|
|
|
className={`border-b border-hairline ${canExpand ? 'cursor-pointer hover:bg-cloud focus:outline-none focus-visible:ring-2 focus-visible:ring-primary' : ''}`}
|
2026-06-10 20:11:12 +02:00
|
|
|
>
|
|
|
|
|
<td className="px-md py-sm text-graphite">
|
|
|
|
|
{canExpand ? (
|
|
|
|
|
isExpanded ? (
|
|
|
|
|
<ChevronDown size={14} aria-hidden />
|
|
|
|
|
) : (
|
|
|
|
|
<ChevronRight size={14} aria-hidden />
|
|
|
|
|
)
|
|
|
|
|
) : null}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-md py-sm font-mono">#{task.mythic_task_display_id}</td>
|
|
|
|
|
<td
|
|
|
|
|
className="px-md py-sm font-mono max-w-[200px] truncate"
|
|
|
|
|
title={task.command}
|
|
|
|
|
>
|
|
|
|
|
{task.command}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-md py-sm">
|
2026-06-10 20:22:45 +02:00
|
|
|
<span className="badge-pill-outline">
|
|
|
|
|
{task.source === 'mimic' ? 'MIMIC' : 'IMPORT'}
|
2026-06-10 20:11:12 +02:00
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-md py-sm">
|
|
|
|
|
<C2TaskStatusBadge status={task.status} />
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-md py-sm font-mono text-graphite">
|
|
|
|
|
{task.completed_at ?? '—'}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{isExpanded && task.output && (
|
|
|
|
|
<tr className="border-b border-hairline bg-cloud">
|
|
|
|
|
<td colSpan={6} className="px-md py-sm">
|
|
|
|
|
<pre
|
|
|
|
|
data-testid="c2-task-output"
|
|
|
|
|
className="font-mono text-[12px] whitespace-pre-wrap text-ink"
|
|
|
|
|
>
|
|
|
|
|
{task.output}
|
|
|
|
|
</pre>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
</Fragment>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|