feat: Security Audit Tab im Admin-Panel mit nftables/SSH/Fail2ban-Checks

This commit is contained in:
sysops
2026-03-17 15:09:01 +01:00
parent b6fa668002
commit 4668150727
3 changed files with 227 additions and 0 deletions
+101
View File
@@ -17,6 +17,7 @@ import {
getSystemStats,
uploadMailFiles,
getUploadProgress,
getSecurityAudit,
type User,
type AuditEntry,
type SMTPStatus,
@@ -24,6 +25,8 @@ import {
type ServiceStatus,
type SystemStats,
type UploadJob,
type SecurityCheck,
type SecurityAuditResult,
} from "@/lib/api";
import { Navbar } from "@/components/navbar";
import { Button } from "@/components/ui/button";
@@ -119,6 +122,11 @@ export default function AdminPage() {
const [auditPage, setAuditPage] = useState(1);
const [auditLoading, setAuditLoading] = useState(false);
// Security audit state
const [securityAudit, setSecurityAudit] = useState<SecurityAuditResult | null>(null);
const [securityLoading, setSecurityLoading] = useState(false);
const [securityError, setSecurityError] = useState("");
// Upload state
const [uploadDragging, setUploadDragging] = useState(false);
const [uploadJob, setUploadJob] = useState<UploadJob | null>(null);
@@ -341,6 +349,19 @@ export default function AdminPage() {
}
}
async function runSecurityAudit() {
setSecurityLoading(true);
setSecurityError("");
try {
const result = await getSecurityAudit();
setSecurityAudit(result);
} catch {
setSecurityError("Security-Audit konnte nicht ausgeführt werden.");
} finally {
setSecurityLoading(false);
}
}
const auditTotalPages = Math.ceil(auditTotal / AUDIT_PAGE_SIZE);
return (
@@ -363,6 +384,7 @@ export default function AdminPage() {
<TabsTrigger value="users">Benutzer</TabsTrigger>
<TabsTrigger value="audit">Audit-Log</TabsTrigger>
<TabsTrigger value="import">Import</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
<TabsTrigger value="modules">Module</TabsTrigger>
</TabsList>
@@ -1182,6 +1204,85 @@ export default function AdminPage() {
)}
</TabsContent>
{/* ── Security Audit ── */}
<TabsContent value="security" className="mt-4 space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">Security Audit</h2>
{securityAudit && (
<p className="text-xs text-muted-foreground mt-0.5">
Zuletzt geprüft: {new Date(securityAudit.run_at).toLocaleString("de-DE")}
</p>
)}
</div>
<Button onClick={runSecurityAudit} disabled={securityLoading} size="sm">
{securityLoading ? "Prüfe..." : "Jetzt prüfen"}
</Button>
</div>
{securityError && (
<Alert variant="destructive">
<AlertDescription>{securityError}</AlertDescription>
</Alert>
)}
{!securityAudit && !securityLoading && !securityError && (
<div className="rounded-lg border border-dashed p-10 text-center text-sm text-muted-foreground">
Klicke auf &ldquo;Jetzt prüfen&rdquo; um den Security-Audit zu starten.
</div>
)}
{securityLoading && (
<div className="space-y-3">
{Array.from({ length: 7 }).map((_, i) => (
<Skeleton key={i} className="h-14 w-full rounded-lg" />
))}
</div>
)}
{securityAudit && !securityLoading && (
<div className="space-y-2">
{securityAudit.checks.map((check: SecurityCheck, i: number) => (
<Card key={i}>
<CardContent className="p-4 flex items-start gap-3">
<span className={`mt-0.5 h-2.5 w-2.5 flex-shrink-0 rounded-full ${
check.status === "ok" ? "bg-green-500" :
check.status === "warning" ? "bg-yellow-400" :
"bg-red-500"
}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{check.name}</span>
<Badge
variant={check.status === "ok" ? "default" : check.status === "warning" ? "secondary" : "destructive"}
className={`text-xs ${check.status === "ok" ? "bg-green-100 text-green-800 hover:bg-green-100" : check.status === "warning" ? "bg-yellow-100 text-yellow-800 hover:bg-yellow-100" : ""}`}
>
{check.status === "ok" ? "OK" : check.status === "warning" ? "Warnung" : "Fehler"}
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-0.5 font-mono">{check.message}</p>
</div>
</CardContent>
</Card>
))}
{/* Summary */}
<div className="mt-4 grid grid-cols-3 gap-3 text-center text-sm">
{[
{ label: "OK", color: "bg-green-50 text-green-700", count: securityAudit.checks.filter((c: SecurityCheck) => c.status === "ok").length },
{ label: "Warnungen", color: "bg-yellow-50 text-yellow-700", count: securityAudit.checks.filter((c: SecurityCheck) => c.status === "warning").length },
{ label: "Fehler", color: "bg-red-50 text-red-700", count: securityAudit.checks.filter((c: SecurityCheck) => c.status === "error").length },
].map((s) => (
<div key={s.label} className={`rounded p-3 ${s.color}`}>
<p className="text-2xl font-bold">{s.count}</p>
<p className="text-xs">{s.label}</p>
</div>
))}
</div>
</div>
)}
</TabsContent>
<TabsContent value="modules" className="mt-4">
<ModulesTab />
</TabsContent>