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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user