feat: Phase 8 - Frontend main views (T-026 to T-031)

Implement all main frontend views for the MITRE ATT&CK coverage platform:

- T-026: Dashboard with coverage summary cards and tactic breakdown table

- T-027: Interactive ATT&CK matrix with filtering by status, tactic, platform

- T-028: Technique detail page with tests, intel items, and review actions

- T-029: Test creation form with technique selector and validation

- T-030: Test detail page with drag and drop evidence upload and download

- T-031: System admin panel with MITRE sync and intel scan controls

New components: CoverageSummaryCard, TacticCoverageChart, AttackMatrix, TechniqueCell, TestForm, EvidenceUpload, EvidenceList

New API modules: metrics.ts, techniques.ts, tests.ts, evidence.ts, system.ts

All views use TanStack Query for data fetching with proper loading and error states. Role-based UI controls for admin/lead actions.
This commit is contained in:
2026-02-06 16:21:14 +01:00
parent 591b5df250
commit cb447f3803
22 changed files with 3092 additions and 27 deletions
+365 -3
View File
@@ -1,8 +1,370 @@
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Loader2,
AlertCircle,
RefreshCw,
Server,
Database,
HardDrive,
Clock,
CheckCircle,
XCircle,
Shield,
Search,
} from "lucide-react";
import {
triggerMitreSync,
triggerIntelScan,
getSchedulerStatus,
type SyncMitreResponse,
type IntelScanResponse,
} from "../api/system";
export default function SystemPage() {
const queryClient = useQueryClient();
const [syncResult, setSyncResult] = useState<SyncMitreResponse | null>(null);
const [intelResult, setIntelResult] = useState<IntelScanResponse | null>(null);
const {
data: schedulerStatus,
isLoading: statusLoading,
error: statusError,
} = useQuery({
queryKey: ["scheduler-status"],
queryFn: getSchedulerStatus,
refetchInterval: 30000, // Refresh every 30 seconds
});
const mitreSyncMutation = useMutation({
mutationFn: triggerMitreSync,
onSuccess: (data) => {
setSyncResult(data);
queryClient.invalidateQueries({ queryKey: ["techniques"] });
queryClient.invalidateQueries({ queryKey: ["metrics"] });
},
});
const intelScanMutation = useMutation({
mutationFn: triggerIntelScan,
onSuccess: (data) => {
setIntelResult(data);
queryClient.invalidateQueries({ queryKey: ["techniques"] });
},
});
const formatNextRun = (dateStr: string | null) => {
if (!dateStr) return "Not scheduled";
const date = new Date(dateStr);
return date.toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short",
});
};
return (
<div className="space-y-4">
<h1 className="text-2xl font-bold text-white">System</h1>
<p className="text-gray-400">System administration panel coming soon.</p>
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-white">System Administration</h1>
<p className="mt-1 text-sm text-gray-400">
Manage synchronization jobs and system status
</p>
</div>
{/* Actions Grid */}
<div className="grid gap-6 lg:grid-cols-2">
{/* MITRE Sync */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<div className="flex items-start gap-4">
<div className="rounded-lg bg-cyan-500/10 p-3">
<Shield className="h-6 w-6 text-cyan-400" />
</div>
<div className="flex-1">
<h2 className="text-lg font-semibold text-white">MITRE ATT&CK Sync</h2>
<p className="mt-1 text-sm text-gray-400">
Synchronize techniques from the MITRE ATT&CK framework via TAXII or GitHub fallback.
</p>
{/* Status */}
{schedulerStatus && (
<div className="mt-4 flex items-center gap-2 text-sm">
<Clock className="h-4 w-4 text-gray-500" />
<span className="text-gray-400">Next automatic sync:</span>
<span className="text-gray-300">
{formatNextRun(
schedulerStatus.jobs.find((j) => j.id === "mitre_sync")?.next_run_time || null
)}
</span>
</div>
)}
{/* Result */}
{syncResult && (
<div className="mt-4 rounded-lg border border-green-500/30 bg-green-900/20 p-3">
<div className="flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-400" />
<span className="text-sm font-medium text-green-400">Sync Complete</span>
</div>
<div className="mt-2 grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-gray-400">New techniques:</span>
<span className="ml-2 font-medium text-white">{syncResult.new}</span>
</div>
<div>
<span className="text-gray-400">Updated:</span>
<span className="ml-2 font-medium text-white">{syncResult.updated}</span>
</div>
</div>
</div>
)}
{mitreSyncMutation.isError && (
<div className="mt-4 rounded-lg border border-red-500/30 bg-red-900/20 p-3">
<div className="flex items-center gap-2">
<XCircle className="h-4 w-4 text-red-400" />
<span className="text-sm text-red-400">
Sync failed: {(mitreSyncMutation.error as Error)?.message}
</span>
</div>
</div>
)}
<button
onClick={() => mitreSyncMutation.mutate()}
disabled={mitreSyncMutation.isPending}
className="mt-4 flex items-center gap-2 rounded-lg bg-cyan-600 px-4 py-2 text-sm font-medium text-white hover:bg-cyan-500 disabled:opacity-50 transition-colors"
>
{mitreSyncMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
{mitreSyncMutation.isPending ? "Syncing..." : "Sync Now"}
</button>
</div>
</div>
</div>
{/* Intel Scan */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<div className="flex items-start gap-4">
<div className="rounded-lg bg-purple-500/10 p-3">
<Search className="h-6 w-6 text-purple-400" />
</div>
<div className="flex-1">
<h2 className="text-lg font-semibold text-white">Threat Intel Scan</h2>
<p className="mt-1 text-sm text-gray-400">
Scan RSS feeds and security blogs for new threat intelligence related to techniques.
</p>
{/* Status */}
{schedulerStatus && (
<div className="mt-4 flex items-center gap-2 text-sm">
<Clock className="h-4 w-4 text-gray-500" />
<span className="text-gray-400">Next automatic scan:</span>
<span className="text-gray-300">
{formatNextRun(
schedulerStatus.jobs.find((j) => j.id === "intel_scan")?.next_run_time || null
)}
</span>
</div>
)}
{/* Result */}
{intelResult && (
<div className="mt-4 rounded-lg border border-green-500/30 bg-green-900/20 p-3">
<div className="flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-400" />
<span className="text-sm font-medium text-green-400">Scan Complete</span>
</div>
<div className="mt-2 text-sm">
<span className="text-gray-400">New intel items:</span>
<span className="ml-2 font-medium text-white">{intelResult.new_items}</span>
</div>
</div>
)}
{intelScanMutation.isError && (
<div className="mt-4 rounded-lg border border-red-500/30 bg-red-900/20 p-3">
<div className="flex items-center gap-2">
<XCircle className="h-4 w-4 text-red-400" />
<span className="text-sm text-red-400">
Scan failed: {(intelScanMutation.error as Error)?.message}
</span>
</div>
</div>
)}
<button
onClick={() => intelScanMutation.mutate()}
disabled={intelScanMutation.isPending}
className="mt-4 flex items-center gap-2 rounded-lg bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-500 disabled:opacity-50 transition-colors"
>
{intelScanMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Search className="h-4 w-4" />
)}
{intelScanMutation.isPending ? "Scanning..." : "Scan Now"}
</button>
</div>
</div>
</div>
</div>
{/* System Information */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<h2 className="mb-4 text-lg font-semibold text-white">System Information</h2>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{/* Backend Status */}
<div className="rounded-lg border border-gray-800 bg-gray-800/50 p-4">
<div className="flex items-center gap-3">
<Server className="h-5 w-5 text-green-400" />
<div>
<p className="text-xs font-medium uppercase text-gray-500">Backend</p>
<p className="text-sm font-medium text-green-400">Online</p>
</div>
</div>
</div>
{/* Database Status */}
<div className="rounded-lg border border-gray-800 bg-gray-800/50 p-4">
<div className="flex items-center gap-3">
<Database className="h-5 w-5 text-green-400" />
<div>
<p className="text-xs font-medium uppercase text-gray-500">PostgreSQL</p>
<p className="text-sm font-medium text-green-400">Connected</p>
</div>
</div>
</div>
{/* MinIO Status */}
<div className="rounded-lg border border-gray-800 bg-gray-800/50 p-4">
<div className="flex items-center gap-3">
<HardDrive className="h-5 w-5 text-green-400" />
<div>
<p className="text-xs font-medium uppercase text-gray-500">MinIO Storage</p>
<p className="text-sm font-medium text-green-400">Available</p>
</div>
</div>
</div>
{/* Scheduler Status */}
<div className="rounded-lg border border-gray-800 bg-gray-800/50 p-4">
<div className="flex items-center gap-3">
<Clock
className={`h-5 w-5 ${
schedulerStatus?.running ? "text-green-400" : "text-yellow-400"
}`}
/>
<div>
<p className="text-xs font-medium uppercase text-gray-500">Scheduler</p>
<p
className={`text-sm font-medium ${
schedulerStatus?.running ? "text-green-400" : "text-yellow-400"
}`}
>
{statusLoading
? "Checking..."
: schedulerStatus?.running
? "Running"
: "Stopped"}
</p>
</div>
</div>
</div>
</div>
</div>
{/* Scheduled Jobs */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<h2 className="mb-4 text-lg font-semibold text-white">Scheduled Jobs</h2>
{statusLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-cyan-400" />
</div>
) : statusError ? (
<div className="flex items-center justify-center gap-2 py-8 text-red-400">
<AlertCircle className="h-5 w-5" />
<span>Failed to load scheduler status</span>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-800">
<th className="pb-3 pr-4 font-medium text-gray-400">Job ID</th>
<th className="pb-3 px-4 font-medium text-gray-400">Name</th>
<th className="pb-3 px-4 font-medium text-gray-400">Next Run</th>
<th className="pb-3 pl-4 font-medium text-gray-400">Status</th>
</tr>
</thead>
<tbody>
{schedulerStatus?.jobs.map((job) => (
<tr
key={job.id}
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
>
<td className="py-3 pr-4">
<code className="rounded bg-gray-800 px-2 py-0.5 text-xs text-cyan-400">
{job.id}
</code>
</td>
<td className="py-3 px-4 text-gray-200">{job.name}</td>
<td className="py-3 px-4 text-gray-400">
{formatNextRun(job.next_run_time)}
</td>
<td className="py-3 pl-4">
{job.next_run_time ? (
<span className="inline-flex items-center gap-1 text-green-400">
<CheckCircle className="h-3.5 w-3.5" />
Scheduled
</span>
) : (
<span className="inline-flex items-center gap-1 text-yellow-400">
<Clock className="h-3.5 w-3.5" />
Pending
</span>
)}
</td>
</tr>
))}
{(!schedulerStatus?.jobs || schedulerStatus.jobs.length === 0) && (
<tr>
<td colSpan={4} className="py-8 text-center text-gray-400">
No scheduled jobs found
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</div>
{/* Version Info */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<h2 className="mb-4 text-lg font-semibold text-white">Version Information</h2>
<dl className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div>
<dt className="text-xs font-medium uppercase text-gray-500">Platform</dt>
<dd className="mt-1 text-sm text-gray-300">Aegis v0.1.0</dd>
</div>
<div>
<dt className="text-xs font-medium uppercase text-gray-500">Backend</dt>
<dd className="mt-1 text-sm text-gray-300">FastAPI + Python 3.11</dd>
</div>
<div>
<dt className="text-xs font-medium uppercase text-gray-500">Frontend</dt>
<dd className="mt-1 text-sm text-gray-300">React 19 + TypeScript</dd>
</div>
<div>
<dt className="text-xs font-medium uppercase text-gray-500">Database</dt>
<dd className="mt-1 text-sm text-gray-300">PostgreSQL 15</dd>
</div>
</dl>
</div>
</div>
);
}