feat(admin): export/import configuration bundle for migration
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-06-02 15:49:51 +02:00
parent 922fb251da
commit eee0560aeb
3 changed files with 487 additions and 1 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useRef } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Loader2,
@@ -18,7 +18,11 @@ import {
ToggleRight,
BarChart3,
X,
Download,
Upload,
PackageOpen,
} from "lucide-react";
import client from "../api/client";
import {
triggerMitreSync,
triggerIntelScan,
@@ -688,6 +692,9 @@ export default function SystemPage() {
)}
</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>
@@ -732,6 +739,143 @@ export default function SystemPage() {
);
}
/* ── 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({