Files
Aegis/frontend/src/pages/SystemPage.tsx
T
kitos 70d5274448 feat(admin): export/import configuration bundle for migration
Backend: GET/POST /api/v1/admin/export-config and /import-config
  Export includes (sensitive values redacted):
  - system_configs (email/jira settings)
  - webhook_configs (secrets redacted)
  - sso_configs (private key redacted)
  - scoring_config (weights)
  - test_templates (source=custom only)
  - users (no passwords/tokens, must_change_password=True on import)
  Import is idempotent — upsert by natural keys, safe to run multiple times.

Frontend: ExportImportSection in SystemPage (admin only)
  - 'Export Configuration' → downloads aegis-config-YYYY-MM-DD.json
  - 'Import Configuration' → file picker, sends JSON, shows summary
  - Visual checklist of what is/isn't included in the export
2026-06-02 15:49:51 +02:00

1293 lines
54 KiB
TypeScript

import { useState, useRef } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Loader2,
AlertCircle,
RefreshCw,
Server,
Database,
HardDrive,
Clock,
CheckCircle,
XCircle,
Shield,
Search,
FlaskConical,
Plus,
ToggleLeft,
ToggleRight,
BarChart3,
X,
Download,
Upload,
PackageOpen,
} from "lucide-react";
import client from "../api/client";
import {
triggerMitreSync,
triggerIntelScan,
getSchedulerStatus,
type SyncMitreResponse,
type IntelScanResponse,
} from "../api/system";
import {
getTemplateStats,
getAllTemplates,
getTemplateById,
createTemplate,
updateTemplate,
toggleTemplateActive,
bulkActivateTemplates,
type TemplateStats,
type CreateTemplatePayload,
} from "../api/test-templates";
import type { TestTemplate } from "../types/models";
export default function SystemPage() {
const queryClient = useQueryClient();
const [syncResult, setSyncResult] = useState<SyncMitreResponse | null>(null);
const [intelResult, setIntelResult] = useState<IntelScanResponse | null>(null);
const [showCreateForm, setShowCreateForm] = useState(false);
const [bulkConfirm, setBulkConfirm] = useState<"activate" | "deactivate" | null>(null);
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null);
// ── Existing queries ─────────────────────────────────────────────
const {
data: schedulerStatus,
isLoading: statusLoading,
error: statusError,
} = useQuery({
queryKey: ["scheduler-status"],
queryFn: getSchedulerStatus,
refetchInterval: 30000,
});
// ── Template queries ─────────────────────────────────────────────
const {
data: templateStats,
isLoading: statsLoading,
} = useQuery({
queryKey: ["template-stats"],
queryFn: getTemplateStats,
});
const {
data: templates,
isLoading: templatesLoading,
} = useQuery({
queryKey: ["templates-admin"],
queryFn: () => getAllTemplates({ limit: 200 }),
});
const {
data: selectedTemplate,
isLoading: selectedTemplateLoading,
} = useQuery({
queryKey: ["template-detail", selectedTemplateId],
queryFn: () => getTemplateById(selectedTemplateId!),
enabled: !!selectedTemplateId,
});
// ── Mutations ────────────────────────────────────────────────────
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 bulkActivateMutation = useMutation({
mutationFn: (activate: boolean) => bulkActivateTemplates(activate),
onSuccess: () => {
setBulkConfirm(null);
queryClient.invalidateQueries({ queryKey: ["templates-admin"] });
queryClient.invalidateQueries({ queryKey: ["template-stats"] });
queryClient.invalidateQueries({ queryKey: ["test-templates"] });
},
});
const toggleActiveMutation = useMutation({
mutationFn: (id: string) => toggleTemplateActive(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["templates-admin"] });
queryClient.invalidateQueries({ queryKey: ["template-stats"] });
queryClient.invalidateQueries({ queryKey: ["test-templates"] });
},
});
const createTemplateMutation = useMutation({
mutationFn: (payload: CreateTemplatePayload) => createTemplate(payload),
onSuccess: () => {
setShowCreateForm(false);
queryClient.invalidateQueries({ queryKey: ["templates-admin"] });
queryClient.invalidateQueries({ queryKey: ["template-stats"] });
queryClient.invalidateQueries({ queryKey: ["test-templates"] });
},
});
const updateTemplateMutation = useMutation({
mutationFn: ({ id, payload }: { id: string; payload: Partial<CreateTemplatePayload> }) =>
updateTemplate(id, payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["templates-admin"] });
queryClient.invalidateQueries({ queryKey: ["test-templates"] });
queryClient.invalidateQueries({ queryKey: ["template-detail", selectedTemplateId] });
},
});
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-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, templates, 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>
{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>
)}
{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">
{syncResult.status === "started" ? "Sync Started" : "Sync Complete"}
</span>
</div>
<p className="mt-1 text-sm text-gray-400">{syncResult.message}</p>
</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>
{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>
)}
{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>
{/* ────────────────────────────────────────────────────────────────
TEMPLATE ADMINISTRATION (T-124)
──────────────────────────────────────────────────────────────── */}
{/* Template Catalog Stats */}
<div className="grid gap-6 lg:grid-cols-1">
{/* Template Catalog Stats */}
<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-yellow-500/10 p-3">
<BarChart3 className="h-6 w-6 text-yellow-400" />
</div>
<div className="flex-1">
<h2 className="text-lg font-semibold text-white">Catalog Statistics</h2>
<p className="mt-1 text-sm text-gray-400">
Overview of the test template catalog.
</p>
{statsLoading ? (
<div className="mt-4 flex items-center justify-center py-4">
<Loader2 className="h-5 w-5 animate-spin text-cyan-400" />
</div>
) : templateStats ? (
<div className="mt-4 space-y-4">
{/* Totals */}
<div className="grid grid-cols-3 gap-3">
<div className="rounded-lg border border-gray-700 bg-gray-800/50 p-3 text-center">
<p className="text-2xl font-bold text-cyan-400">{templateStats.total}</p>
<p className="text-xs text-gray-400">Total</p>
</div>
<div className="rounded-lg border border-gray-700 bg-gray-800/50 p-3 text-center">
<p className="text-2xl font-bold text-green-400">{templateStats.active}</p>
<p className="text-xs text-gray-400">Active</p>
</div>
<div className="rounded-lg border border-gray-700 bg-gray-800/50 p-3 text-center">
<p className="text-2xl font-bold text-gray-400">{templateStats.inactive}</p>
<p className="text-xs text-gray-400">Inactive</p>
</div>
</div>
{/* By source */}
<div>
<p className="text-xs font-medium uppercase text-gray-500 mb-2">By Source</p>
<div className="flex flex-wrap gap-2">
{Object.entries(templateStats.by_source).map(([source, count]) => (
<span
key={source}
className="inline-flex items-center gap-1 rounded-full border border-gray-700 bg-gray-800 px-2.5 py-1 text-xs text-gray-300"
>
{source.replace(/_/g, " ")}
<span className="font-medium text-cyan-400">{count}</span>
</span>
))}
{Object.keys(templateStats.by_source).length === 0 && (
<span className="text-xs text-gray-500">No templates yet</span>
)}
</div>
</div>
{/* By platform */}
<div>
<p className="text-xs font-medium uppercase text-gray-500 mb-2">By Platform</p>
<div className="flex flex-wrap gap-2">
{Object.entries(templateStats.by_platform).map(([platform, count]) => (
<span
key={platform}
className="inline-flex items-center gap-1 rounded-full border border-gray-700 bg-gray-800 px-2.5 py-1 text-xs text-gray-300"
>
{platform}
<span className="font-medium text-cyan-400">{count}</span>
</span>
))}
{Object.keys(templateStats.by_platform).length === 0 && (
<span className="text-xs text-gray-500">No templates yet</span>
)}
</div>
</div>
</div>
) : null}
</div>
</div>
</div>
</div>
{/* Create Custom Template Form (modal-style inline) */}
{showCreateForm && (
<CreateTemplateForm
onClose={() => setShowCreateForm(false)}
onSubmit={(payload) => createTemplateMutation.mutate(payload)}
isPending={createTemplateMutation.isPending}
error={createTemplateMutation.isError ? (createTemplateMutation.error as Error)?.message : null}
/>
)}
{/* Bulk Activate Confirmation Modal */}
{bulkConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-md rounded-xl border border-gray-700 bg-gray-900 p-6 shadow-xl">
<h3 className="text-lg font-semibold text-white">
{bulkConfirm === "activate" ? "Activate All Templates" : "Deactivate All Templates"}
</h3>
<p className="mt-2 text-sm text-gray-400">
{bulkConfirm === "activate"
? "This will activate ALL templates in the catalog, including previously deactivated ones. All templates will become available for test creation."
: "This will deactivate ALL templates in the catalog. No templates will be available for test creation until reactivated."}
</p>
<p className="mt-2 text-sm font-medium text-yellow-400">
This action affects all {templateStats?.total || 0} templates.
</p>
<div className="mt-4 flex items-center justify-end gap-3">
<button
onClick={() => setBulkConfirm(null)}
className="rounded-lg border border-gray-700 bg-gray-800 px-4 py-2 text-sm font-medium text-gray-300 hover:border-gray-600 hover:text-white transition-colors"
>
Cancel
</button>
<button
onClick={() => bulkActivateMutation.mutate(bulkConfirm === "activate")}
disabled={bulkActivateMutation.isPending}
className={`flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white transition-colors disabled:opacity-50 ${
bulkConfirm === "activate"
? "bg-green-600 hover:bg-green-500"
: "bg-red-600 hover:bg-red-500"
}`}
>
{bulkActivateMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : bulkConfirm === "activate" ? (
<ToggleRight className="h-4 w-4" />
) : (
<ToggleLeft className="h-4 w-4" />
)}
{bulkActivateMutation.isPending
? "Processing..."
: bulkConfirm === "activate"
? "Activate All"
: "Deactivate All"}
</button>
</div>
</div>
</div>
)}
{/* Templates Management Table */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<FlaskConical className="h-5 w-5 text-cyan-400" />
Manage Templates
</h2>
<div className="flex items-center gap-2">
<button
onClick={() => setBulkConfirm("activate")}
className="flex items-center gap-1.5 rounded-lg border border-green-500/30 bg-green-900/20 px-3 py-2 text-sm font-medium text-green-400 hover:bg-green-900/40 transition-colors"
>
<ToggleRight className="h-4 w-4" />
Activate All
</button>
<button
onClick={() => setBulkConfirm("deactivate")}
className="flex items-center gap-1.5 rounded-lg border border-red-500/30 bg-red-900/20 px-3 py-2 text-sm font-medium text-red-400 hover:bg-red-900/40 transition-colors"
>
<ToggleLeft className="h-4 w-4" />
Deactivate All
</button>
<button
onClick={() => setShowCreateForm(!showCreateForm)}
className="flex items-center gap-1.5 rounded-lg bg-cyan-600 px-3 py-2 text-sm font-medium text-white hover:bg-cyan-500 transition-colors"
>
<Plus className="h-4 w-4" />
Create Custom
</button>
</div>
</div>
{templatesLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-cyan-400" />
</div>
) : templates && templates.length > 0 ? (
<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">Name</th>
<th className="pb-3 px-4 font-medium text-gray-400">Technique</th>
<th className="pb-3 px-4 font-medium text-gray-400">Source</th>
<th className="pb-3 px-4 font-medium text-gray-400">Platform</th>
<th className="pb-3 px-4 font-medium text-gray-400">Status</th>
<th className="pb-3 pl-4 font-medium text-gray-400">Action</th>
</tr>
</thead>
<tbody>
{(templates as TestTemplate[]).map((tpl) => (
<tr
key={tpl.id}
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
>
<td className="py-3 pr-4">
<button
onClick={() => setSelectedTemplateId(tpl.id)}
className="text-left font-medium text-cyan-400 hover:text-cyan-300 hover:underline truncate block max-w-[200px] transition-colors"
title="Click to view/edit"
>
{tpl.name}
</button>
</td>
<td className="py-3 px-4">
<span className="font-mono text-xs text-cyan-400">
{tpl.mitre_technique_id}
</span>
</td>
<td className="py-3 px-4">
<span
className={`inline-flex rounded-full border px-2 py-0.5 text-xs font-medium ${
tpl.source === "atomic_red_team"
? "bg-red-900/50 text-red-400 border-red-500/30"
: tpl.source === "mitre"
? "bg-blue-900/50 text-blue-400 border-blue-500/30"
: "bg-gray-800/50 text-gray-400 border-gray-600/30"
}`}
>
{tpl.source.replace(/_/g, " ")}
</span>
</td>
<td className="py-3 px-4 text-gray-400 text-xs">
{tpl.platform || "-"}
</td>
<td className="py-3 px-4">
<span
className={`inline-flex rounded-full border px-2 py-0.5 text-xs font-medium ${
tpl.is_active
? "bg-green-900/50 text-green-400 border-green-500/30"
: "bg-gray-800/50 text-gray-500 border-gray-600/30"
}`}
>
{tpl.is_active ? "Active" : "Inactive"}
</span>
</td>
<td className="py-3 pl-4">
<button
onClick={() => toggleActiveMutation.mutate(tpl.id)}
disabled={toggleActiveMutation.isPending}
className={`flex items-center gap-1 text-xs font-medium transition-colors ${
tpl.is_active
? "text-red-400 hover:text-red-300"
: "text-green-400 hover:text-green-300"
}`}
title={tpl.is_active ? "Deactivate" : "Activate"}
>
{tpl.is_active ? (
<>
<ToggleRight className="h-4 w-4" />
Deactivate
</>
) : (
<>
<ToggleLeft className="h-4 w-4" />
Activate
</>
)}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="py-8 text-center text-gray-400">
No templates found. Import from Atomic Red Team or create a custom template.
</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">
<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>
<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>
<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>
<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>
{/* Export / Import Configuration */}
<ExportImportSection />
{/* 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>
{/* Template Detail Modal */}
{selectedTemplateId && (
selectedTemplateLoading ? (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
<Loader2 className="h-8 w-8 animate-spin text-cyan-400" />
</div>
) : selectedTemplate ? (
<TemplateDetailModal
template={selectedTemplate}
onClose={() => setSelectedTemplateId(null)}
onSave={(id, payload) => updateTemplateMutation.mutate({ id, payload })}
onToggleActive={(id) => toggleActiveMutation.mutate(id)}
isSaving={updateTemplateMutation.isPending}
isToggling={toggleActiveMutation.isPending}
/>
) : null
)}
</div>
);
}
/* ── Export / Import Configuration ───────────────────────────────── */
function ExportImportSection() {
const fileRef = useRef<HTMLInputElement>(null);
const [importing, setImporting] = useState(false);
const [importResult, setImportResult] = useState<{
status: string;
summary: Record<string, number>;
warnings: string[];
} | null>(null);
const [importError, setImportError] = useState<string | null>(null);
const handleExport = async () => {
try {
const resp = await client.get("/admin/export-config", {
responseType: "blob",
});
const url = URL.createObjectURL(resp.data);
const a = document.createElement("a");
a.href = url;
const date = new Date().toISOString().slice(0, 10);
a.download = `aegis-config-${date}.json`;
a.click();
URL.revokeObjectURL(url);
} catch (e) {
alert("Export failed: " + (e as Error).message);
}
};
const handleImportFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
e.target.value = "";
setImportError(null);
setImportResult(null);
setImporting(true);
try {
const text = await file.text();
const json = JSON.parse(text);
const resp = await client.post("/admin/import-config", json);
setImportResult(resp.data);
} catch (err: unknown) {
const msg =
(err as { response?: { data?: { detail?: string } } })?.response?.data?.detail ??
(err as Error)?.message ??
"Import failed";
setImportError(String(msg));
} finally {
setImporting(false);
}
};
return (
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<div className="flex items-center gap-2 mb-1">
<PackageOpen className="h-5 w-5 text-cyan-400" />
<h2 className="text-lg font-semibold text-white">Configuration Export / Import</h2>
</div>
<p className="mb-5 text-sm text-gray-400">
Export all platform configuration to a JSON file for backup or migration.
Import restores settings, webhooks, custom templates and users on a fresh instance.
</p>
{/* What is exported */}
<div className="mb-5 rounded-lg border border-gray-800 bg-gray-800/30 p-4">
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-gray-500">What is included</p>
<div className="grid gap-x-8 gap-y-1 text-xs text-gray-400 sm:grid-cols-2">
{[
["✅", "Email & Jira settings"],
["✅", "SSO / SAML configuration"],
["✅", "Webhooks"],
["✅", "Scoring weights"],
["✅", "Custom test templates"],
["✅", "Users (roles & status)"],
["🔒", "Passwords / API tokens → REDACTED"],
["❌", "Tests, campaigns, techniques data"],
].map(([icon, text]) => (
<div key={text} className="flex items-center gap-1.5">
<span>{icon}</span>
<span>{text}</span>
</div>
))}
</div>
</div>
{/* Actions */}
<div className="flex flex-wrap gap-3">
<button
onClick={handleExport}
className="flex items-center gap-2 rounded-lg bg-cyan-600 px-4 py-2 text-sm font-medium text-white hover:bg-cyan-500 transition-colors"
>
<Download className="h-4 w-4" />
Export Configuration
</button>
<input ref={fileRef} type="file" accept=".json" onChange={handleImportFile} className="hidden" />
<button
onClick={() => fileRef.current?.click()}
disabled={importing}
className="flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-800 px-4 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 disabled:opacity-50 transition-colors"
>
{importing ? <Loader2 className="h-4 w-4 animate-spin" /> : <Upload className="h-4 w-4" />}
{importing ? "Importing…" : "Import Configuration"}
</button>
</div>
{/* Import result */}
{importResult && (
<div className="mt-4 rounded-lg border border-green-500/30 bg-green-500/5 p-4 space-y-2">
<p className="text-sm font-medium text-green-400 flex items-center gap-2">
<CheckCircle className="h-4 w-4" /> Import completed successfully
</p>
<div className="grid gap-2 text-xs text-gray-400 sm:grid-cols-3">
{Object.entries(importResult.summary).map(([key, val]) => (
<div key={key} className="flex items-center justify-between rounded bg-gray-800 px-2 py-1">
<span className="capitalize">{key.replace(/_/g, " ")}</span>
<span className="font-mono text-cyan-400">{val}</span>
</div>
))}
</div>
{importResult.warnings.length > 0 && (
<ul className="mt-2 space-y-0.5 text-[11px] text-amber-400/80 list-disc list-inside">
{importResult.warnings.map((w) => <li key={w}>{w}</li>)}
</ul>
)}
</div>
)}
{importError && (
<div className="mt-4 rounded-lg border border-red-500/30 bg-red-900/20 p-3 text-sm text-red-400">
{importError}
</div>
)}
</div>
);
}
/* ── Create Template Form (inline modal) ──────────────────────────── */
function CreateTemplateForm({
onClose,
onSubmit,
isPending,
error,
}: {
onClose: () => void;
onSubmit: (payload: CreateTemplatePayload) => void;
isPending: boolean;
error: string | null;
}) {
const [form, setForm] = useState<CreateTemplatePayload>({
mitre_technique_id: "",
name: "",
description: "",
source: "custom",
attack_procedure: "",
expected_detection: "",
platform: "",
tool_suggested: "",
severity: "",
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!form.mitre_technique_id || !form.name) return;
onSubmit(form);
};
return (
<div className="rounded-xl border border-cyan-500/30 bg-gray-900 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<Plus className="h-5 w-5 text-cyan-400" />
Create Custom Template
</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-white transition-colors"
>
<X className="h-5 w-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
{/* MITRE Technique ID */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">
MITRE Technique ID *
</label>
<input
type="text"
value={form.mitre_technique_id}
onChange={(e) => setForm({ ...form, mitre_technique_id: e.target.value })}
placeholder="e.g. T1059.001"
required
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
/>
</div>
{/* Name */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">
Template Name *
</label>
<input
type="text"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="Test template name"
required
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
/>
</div>
{/* Platform */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">
Platform
</label>
<select
value={form.platform || ""}
onChange={(e) => setForm({ ...form, platform: e.target.value || undefined })}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
>
<option value="">Select platform...</option>
<option value="windows">Windows</option>
<option value="linux">Linux</option>
<option value="macos">macOS</option>
</select>
</div>
{/* Severity */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">
Severity
</label>
<select
value={form.severity || ""}
onChange={(e) => setForm({ ...form, severity: e.target.value || undefined })}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
>
<option value="">Select severity...</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">
Description
</label>
<textarea
value={form.description || ""}
onChange={(e) => setForm({ ...form, description: e.target.value })}
placeholder="Template description..."
rows={2}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
/>
</div>
{/* Attack Procedure */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">
Attack Procedure
</label>
<textarea
value={form.attack_procedure || ""}
onChange={(e) => setForm({ ...form, attack_procedure: e.target.value })}
placeholder="Steps for the red team to execute..."
rows={3}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
/>
</div>
{/* Expected Detection */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">
Expected Detection
</label>
<textarea
value={form.expected_detection || ""}
onChange={(e) => setForm({ ...form, expected_detection: e.target.value })}
placeholder="What the blue team should detect..."
rows={2}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
/>
</div>
{/* Tool Suggested */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">
Suggested Tool
</label>
<input
type="text"
value={form.tool_suggested || ""}
onChange={(e) => setForm({ ...form, tool_suggested: e.target.value })}
placeholder="e.g. PowerShell, Cobalt Strike"
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
/>
</div>
{/* Error */}
{error && (
<div className="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">{error}</span>
</div>
</div>
)}
{/* Buttons */}
<div className="flex items-center gap-3">
<button
type="submit"
disabled={isPending || !form.mitre_technique_id || !form.name}
className="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"
>
{isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Plus className="h-4 w-4" />
)}
{isPending ? "Creating..." : "Create Template"}
</button>
<button
type="button"
onClick={onClose}
className="rounded-lg border border-gray-700 bg-gray-800 px-4 py-2 text-sm font-medium text-gray-300 hover:border-gray-600 hover:text-white transition-colors"
>
Cancel
</button>
</div>
</form>
</div>
);
}
/* ── Template Detail / Edit Modal ────────────────────────────────── */
function TemplateDetailModal({
template,
onClose,
onSave,
onToggleActive,
isSaving,
isToggling,
}: {
template: TestTemplate;
onClose: () => void;
onSave: (id: string, payload: Partial<CreateTemplatePayload>) => void;
onToggleActive: (id: string) => void;
isSaving: boolean;
isToggling: boolean;
}) {
const [form, setForm] = useState<Partial<CreateTemplatePayload>>({
name: template.name,
description: template.description ?? "",
attack_procedure: template.attack_procedure ?? "",
expected_detection: template.expected_detection ?? "",
platform: template.platform ?? "",
tool_suggested: template.tool_suggested ?? "",
severity: template.severity ?? "",
mitre_technique_id: template.mitre_technique_id,
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave(template.id, form);
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
<div className="w-full max-w-2xl max-h-[90vh] overflow-y-auto rounded-xl border border-gray-700 bg-gray-900 shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between border-b border-gray-800 px-6 py-4">
<div>
<h2 className="text-lg font-semibold text-white">Edit Template</h2>
<p className="mt-0.5 text-xs text-gray-400 font-mono">{template.mitre_technique_id}</p>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-white transition-colors">
<X className="h-5 w-5" />
</button>
</div>
{/* Meta badges */}
<div className="flex flex-wrap gap-2 px-6 py-3 border-b border-gray-800">
<span
className={`inline-flex rounded-full border px-2 py-0.5 text-xs font-medium ${
template.source === "atomic_red_team"
? "bg-red-900/50 text-red-400 border-red-500/30"
: template.source === "mitre"
? "bg-blue-900/50 text-blue-400 border-blue-500/30"
: "bg-gray-800/50 text-gray-400 border-gray-600/30"
}`}
>
{template.source.replace(/_/g, " ")}
</span>
<span
className={`inline-flex rounded-full border px-2 py-0.5 text-xs font-medium ${
template.is_active
? "bg-green-900/50 text-green-400 border-green-500/30"
: "bg-gray-800/50 text-gray-500 border-gray-600/30"
}`}
>
{template.is_active ? "Active" : "Inactive"}
</span>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4 px-6 py-4">
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">Template Name</label>
<input
type="text"
value={form.name ?? ""}
onChange={(e) => setForm({ ...form, name: e.target.value })}
required
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">MITRE Technique ID</label>
<input
type="text"
value={form.mitre_technique_id ?? ""}
onChange={(e) => setForm({ ...form, mitre_technique_id: e.target.value })}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">Platform</label>
<select
value={form.platform ?? ""}
onChange={(e) => setForm({ ...form, platform: e.target.value || undefined })}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
>
<option value="">None</option>
<option value="windows">Windows</option>
<option value="linux">Linux</option>
<option value="macos">macOS</option>
<option value="cloud">Cloud</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">Severity</label>
<select
value={form.severity ?? ""}
onChange={(e) => setForm({ ...form, severity: e.target.value || undefined })}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
>
<option value="">None</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">Description</label>
<textarea
value={form.description ?? ""}
onChange={(e) => setForm({ ...form, description: e.target.value })}
rows={2}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">Attack Procedure</label>
<textarea
value={form.attack_procedure ?? ""}
onChange={(e) => setForm({ ...form, attack_procedure: e.target.value })}
rows={3}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">Expected Detection</label>
<textarea
value={form.expected_detection ?? ""}
onChange={(e) => setForm({ ...form, expected_detection: e.target.value })}
rows={2}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">Suggested Tool</label>
<input
type="text"
value={form.tool_suggested ?? ""}
onChange={(e) => setForm({ ...form, tool_suggested: e.target.value })}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
/>
</div>
{/* Action buttons */}
<div className="flex items-center gap-3 pt-2 border-t border-gray-800">
<button
type="submit"
disabled={isSaving}
className="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"
>
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <CheckCircle className="h-4 w-4" />}
{isSaving ? "Saving..." : "Save Changes"}
</button>
<button
type="button"
onClick={() => onToggleActive(template.id)}
disabled={isToggling}
className={`flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors disabled:opacity-50 ${
template.is_active
? "border-red-500/30 bg-red-900/20 text-red-400 hover:bg-red-900/40"
: "border-green-500/30 bg-green-900/20 text-green-400 hover:bg-green-900/40"
}`}
>
{isToggling ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : template.is_active ? (
<ToggleRight className="h-4 w-4" />
) : (
<ToggleLeft className="h-4 w-4" />
)}
{template.is_active ? "Deactivate" : "Activate"}
</button>
<button
type="button"
onClick={onClose}
className="ml-auto rounded-lg border border-gray-700 bg-gray-800 px-4 py-2 text-sm font-medium text-gray-300 hover:border-gray-600 hover:text-white transition-colors"
>
Close
</button>
</div>
</form>
</div>
</div>
);
}