feat: Security Audit Auto-Fix Buttons + manuelle Dokumentation (docs/security-audit.md)

This commit is contained in:
sysops
2026-03-17 15:23:31 +01:00
parent 4668150727
commit 5e69c29f16
4 changed files with 498 additions and 40 deletions
+100 -40
View File
@@ -18,6 +18,7 @@ import {
uploadMailFiles,
getUploadProgress,
getSecurityAudit,
fixSecurityIssue,
type User,
type AuditEntry,
type SMTPStatus,
@@ -126,6 +127,8 @@ export default function AdminPage() {
const [securityAudit, setSecurityAudit] = useState<SecurityAuditResult | null>(null);
const [securityLoading, setSecurityLoading] = useState(false);
const [securityError, setSecurityError] = useState("");
const [fixLoading, setFixLoading] = useState<string | null>(null);
const [fixMessage, setFixMessage] = useState("");
// Upload state
const [uploadDragging, setUploadDragging] = useState(false);
@@ -352,6 +355,7 @@ export default function AdminPage() {
async function runSecurityAudit() {
setSecurityLoading(true);
setSecurityError("");
setFixMessage("");
try {
const result = await getSecurityAudit();
setSecurityAudit(result);
@@ -362,6 +366,22 @@ export default function AdminPage() {
}
}
async function runFix(action: string) {
setFixLoading(action);
setFixMessage("");
setSecurityError("");
try {
const res = await fixSecurityIssue(action);
setFixMessage(res.message);
// Re-run audit automatically to reflect new state
await runSecurityAudit();
} catch (e: unknown) {
setSecurityError(e instanceof Error ? e.message : "Fix fehlgeschlagen.");
} finally {
setFixLoading(null);
}
}
const auditTotalPages = Math.ceil(auditTotal / AUDIT_PAGE_SIZE);
return (
@@ -1215,7 +1235,7 @@ export default function AdminPage() {
</p>
)}
</div>
<Button onClick={runSecurityAudit} disabled={securityLoading} size="sm">
<Button onClick={runSecurityAudit} disabled={securityLoading || fixLoading !== null} size="sm">
{securityLoading ? "Prüfe..." : "Jetzt prüfen"}
</Button>
</div>
@@ -1226,6 +1246,12 @@ export default function AdminPage() {
</Alert>
)}
{fixMessage && (
<Alert>
<AlertDescription className="text-green-700">{fixMessage}</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.
@@ -1240,47 +1266,81 @@ export default function AdminPage() {
</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>
))}
{securityAudit && !securityLoading && (() => {
// Map check names to fix actions
const fixActions: Record<string, { action: string; label: string }> = {
"Fail2ban": { action: "install_fail2ban", label: "Installieren & aktivieren" },
"Firewall (nftables)": { action: "enable_firewall", label: "Firewall aktivieren" },
"SSH Passwort-Auth": { action: "fix_ssh_password_auth", label: "Deaktivieren" },
"SSH Root-Login": { action: "fix_ssh_root_login", label: "Auf prohibit-password setzen" },
};
{/* 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>
))}
return (
<div className="space-y-2">
{securityAudit.checks.map((check: SecurityCheck, i: number) => {
const fix = check.status !== "ok" ? fixActions[check.name] : undefined;
return (
<Card key={i}>
<CardContent className="p-4 flex items-start gap-3">
<span className={`mt-1 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 flex-wrap">
<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>
{fix && (
<Button
size="sm"
variant="outline"
className="flex-shrink-0 text-xs"
disabled={fixLoading !== null}
onClick={() => runFix(fix.action)}
>
{fixLoading === fix.action ? "Wird behoben..." : fix.label}
</Button>
)}
{check.name === "HTTPS (TLS)" && check.status !== "ok" && (
<a
href="https://certbot.eff.org/instructions?os=debianbuster&tab=standard"
target="_blank"
rel="noopener noreferrer"
>
<Button size="sm" variant="outline" className="flex-shrink-0 text-xs">
Anleitung
</Button>
</a>
)}
</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>
</div>
)}
);
})()}
</TabsContent>
<TabsContent value="modules" className="mt-4">