"use client" import { useState, useEffect } from "react" import { Button } from "./ui/button" import { Input } from "./ui/input" import { Label } from "./ui/label" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card" import { Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut, Key, Copy, Eye, EyeOff, Trash2, RefreshCw, Clock, ShieldCheck, Globe, FileKey, AlertTriangle, Flame, Bug, Search, Download, Power, PowerOff, Plus, Minus, Activity, Settings, Ban, FileText, Printer, Play, BarChart3, TriangleAlert, ChevronDown, ArrowDownLeft, ArrowUpRight, ChevronRight, Network, Zap, Pencil, Check, X, } from "lucide-react" import { getApiUrl, fetchApi } from "../lib/api-config" import { TwoFactorSetup } from "./two-factor-setup" import { ScriptTerminalModal } from "./script-terminal-modal" interface ApiTokenEntry { id: string name: string token_prefix: string created_at: string expires_at: string revoked: boolean } export function Security() { const [authEnabled, setAuthEnabled] = useState(false) const [totpEnabled, setTotpEnabled] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState("") const [success, setSuccess] = useState("") // Setup form state const [showSetupForm, setShowSetupForm] = useState(false) const [username, setUsername] = useState("") const [password, setPassword] = useState("") const [confirmPassword, setConfirmPassword] = useState("") // Change password form state const [showChangePassword, setShowChangePassword] = useState(false) const [currentPassword, setCurrentPassword] = useState("") const [newPassword, setNewPassword] = useState("") const [confirmNewPassword, setConfirmNewPassword] = useState("") const [show2FASetup, setShow2FASetup] = useState(false) const [show2FADisable, setShow2FADisable] = useState(false) const [disable2FAPassword, setDisable2FAPassword] = useState("") // API Token state management const [showApiTokenSection, setShowApiTokenSection] = useState(false) const [apiToken, setApiToken] = useState("") const [apiTokenVisible, setApiTokenVisible] = useState(false) const [tokenPassword, setTokenPassword] = useState("") const [tokenTotpCode, setTokenTotpCode] = useState("") const [generatingToken, setGeneratingToken] = useState(false) const [tokenCopied, setTokenCopied] = useState(false) // Token list state const [existingTokens, setExistingTokens] = useState([]) const [loadingTokens, setLoadingTokens] = useState(false) const [revokingTokenId, setRevokingTokenId] = useState(null) const [tokenName, setTokenName] = useState("API Token") // Proxmox Firewall state const [firewallLoading, setFirewallLoading] = useState(true) const [firewallData, setFirewallData] = useState<{ pve_firewall_installed: boolean pve_firewall_active: boolean cluster_fw_enabled: boolean host_fw_enabled: boolean rules_count: number rules: Array<{ raw: string; direction?: string; action?: string; dport?: string; p?: string; source?: string; source_file?: string; section?: string; rule_index: number }> monitor_port_open: boolean } | null>(null) const [firewallAction, setFirewallAction] = useState(false) const [showAddRule, setShowAddRule] = useState(false) const [newRule, setNewRule] = useState({ direction: "IN", action: "ACCEPT", protocol: "tcp", dport: "", sport: "", source: "", iface: "", comment: "", level: "host", }) const [addingRule, setAddingRule] = useState(false) const [deletingRuleIdx, setDeletingRuleIdx] = useState(null) const [expandedRuleKey, setExpandedRuleKey] = useState(null) const [editingRuleKey, setEditingRuleKey] = useState(null) const [editRule, setEditRule] = useState({ direction: "IN", action: "ACCEPT", protocol: "tcp", dport: "", sport: "", source: "", iface: "", comment: "", level: "host", }) const [savingRule, setSavingRule] = useState(false) // Security Tools state const [toolsLoading, setToolsLoading] = useState(true) const [fail2banInfo, setFail2banInfo] = useState<{ installed: boolean; active: boolean; version: string; jails: string[]; banned_ips_count: number } | null>(null) const [lynisInfo, setLynisInfo] = useState<{ installed: boolean; version: string; last_scan: string | null; hardening_index: number | null } | null>(null) const [showFail2banInstaller, setShowFail2banInstaller] = useState(false) const [showLynisInstaller, setShowLynisInstaller] = useState(false) // Lynis audit state interface LynisWarning { test_id: string; severity: string; description: string; solution: string; proxmox_context?: string; proxmox_expected?: boolean; proxmox_severity?: string } interface LynisSuggestion { test_id: string; description: string; solution: string; details: string; proxmox_context?: string; proxmox_expected?: boolean; proxmox_severity?: string } interface LynisCheck { name: string; status: string; detail?: string } interface LynisSection { name: string; checks: LynisCheck[] } interface LynisReport { datetime_start: string; datetime_end: string; lynis_version: string os_name: string; os_version: string; os_fullname: string; hostname: string hardening_index: number | null; tests_performed: number warnings: LynisWarning[]; suggestions: LynisSuggestion[] categories: Record installed_packages: number; kernel_version: string firewall_active: boolean; malware_scanner: boolean sections: LynisSection[] proxmox_adjusted_score?: number proxmox_expected_warnings?: number proxmox_expected_suggestions?: number proxmox_context_applied?: boolean } const [lynisAuditRunning, setLynisAuditRunning] = useState(false) const [lynisReport, setLynisReport] = useState(null) const [lynisReportLoading, setLynisReportLoading] = useState(false) const [lynisShowReport, setLynisShowReport] = useState(false) const [lynisActiveTab, setLynisActiveTab] = useState<"overview" | "warnings" | "suggestions" | "checks">("overview") // Fail2Ban detailed state interface BannedIp { ip: string type: "local" | "external" | "unknown" } interface JailDetail { name: string currently_failed: number total_failed: number currently_banned: number total_banned: number banned_ips: BannedIp[] findtime: string bantime: string maxretry: string } interface F2bEvent { timestamp: string jail: string ip: string action: "ban" | "unban" | "found" } const [f2bDetails, setF2bDetails] = useState<{ installed: boolean; active: boolean; version: string; jails: JailDetail[] } | null>(null) const [f2bActivity, setF2bActivity] = useState([]) const [f2bDetailsLoading, setF2bDetailsLoading] = useState(false) const [f2bUnbanning, setF2bUnbanning] = useState(null) const [f2bActiveTab, setF2bActiveTab] = useState<"jails" | "activity">("jails") const [f2bEditingJail, setF2bEditingJail] = useState(null) const [f2bJailConfig, setF2bJailConfig] = useState<{maxretry: string; bantime: string; findtime: string; permanent: boolean}>({ maxretry: "", bantime: "", findtime: "", permanent: false, }) const [f2bSavingConfig, setF2bSavingConfig] = useState(false) const [f2bApplyingJails, setF2bApplyingJails] = useState(false) // SSL/HTTPS state const [sslEnabled, setSslEnabled] = useState(false) const [sslSource, setSslSource] = useState<"none" | "proxmox" | "custom">("none") const [sslCertPath, setSslCertPath] = useState("") const [sslKeyPath, setSslKeyPath] = useState("") const [proxmoxCertAvailable, setProxmoxCertAvailable] = useState(false) const [proxmoxCertInfo, setProxmoxCertInfo] = useState<{subject?: string; expires?: string; issuer?: string; is_self_signed?: boolean} | null>(null) const [loadingSsl, setLoadingSsl] = useState(true) const [configuringSsl, setConfiguringSsl] = useState(false) const [sslRestarting, setSslRestarting] = useState(false) const [showCustomCertForm, setShowCustomCertForm] = useState(false) const [customCertPath, setCustomCertPath] = useState("") const [customKeyPath, setCustomKeyPath] = useState("") useEffect(() => { checkAuthStatus() loadApiTokens() loadSslStatus() loadFirewallStatus() loadSecurityTools() }, []) const loadFirewallStatus = async () => { try { setFirewallLoading(true) const data = await fetchApi("/api/security/firewall/status") if (data.success) { setFirewallData({ pve_firewall_installed: data.pve_firewall_installed, pve_firewall_active: data.pve_firewall_active, cluster_fw_enabled: data.cluster_fw_enabled, host_fw_enabled: data.host_fw_enabled, rules_count: data.rules_count, rules: data.rules || [], monitor_port_open: data.monitor_port_open, }) } } catch { // Silently fail } finally { setFirewallLoading(false) } } const loadSecurityTools = async () => { try { setToolsLoading(true) const data = await fetchApi("/api/security/tools") if (data.success && data.tools) { setFail2banInfo(data.tools.fail2ban || null) setLynisInfo(data.tools.lynis || null) } } catch { // Silently fail } finally { setToolsLoading(false) } } const loadFail2banDetails = async () => { try { setF2bDetailsLoading(true) const [detailsRes, activityRes] = await Promise.all([ fetchApi("/api/security/fail2ban/details"), fetchApi("/api/security/fail2ban/activity"), ]) if (detailsRes.success) { setF2bDetails({ installed: detailsRes.installed, active: detailsRes.active, version: detailsRes.version, jails: detailsRes.jails || [], }) } if (activityRes.success) { setF2bActivity(activityRes.events || []) } } catch { // Silently fail } finally { setF2bDetailsLoading(false) } } const handleUnbanIp = async (jail: string, ip: string) => { const key = `${jail}:${ip}` setF2bUnbanning(key) setError("") setSuccess("") try { const data = await fetchApi("/api/security/fail2ban/unban", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jail, ip }), }) if (data.success) { setSuccess(data.message || `IP ${ip} unbanned from ${jail}`) loadFail2banDetails() loadSecurityTools() } else { setError(data.message || "Failed to unban IP") } } catch (err) { setError(err instanceof Error ? err.message : "Failed to unban IP") } finally { setF2bUnbanning(null) } } const handleApplyMissingJails = async () => { setF2bApplyingJails(true) setError("") setSuccess("") try { const data = await fetchApi("/api/security/fail2ban/apply-jails", { method: "POST", }) if (data.success) { setSuccess(data.message || "Missing jails applied successfully") // Reload to see the new jails await loadFail2banDetails() loadSecurityTools() } else { setError(data.message || "Failed to apply missing jails") } } catch (err) { setError(err instanceof Error ? err.message : "Failed to apply missing jails") } finally { setF2bApplyingJails(false) } } // --- Lynis audit handlers --- const handleRunLynisAudit = async () => { setLynisAuditRunning(true) setError("") setSuccess("") try { const data = await fetchApi("/api/security/lynis/run", { method: "POST" }) if (data.success) { // Poll for completion const pollInterval = setInterval(async () => { try { const status = await fetchApi("/api/security/lynis/status") if (!status.running) { clearInterval(pollInterval) setLynisAuditRunning(false) if (status.progress === "completed") { setSuccess("Security audit completed successfully") loadSecurityTools() loadLynisReport() } else { setError(status.progress || "Audit failed") } } } catch { clearInterval(pollInterval) setLynisAuditRunning(false) } }, 3000) } else { setError(data.message || "Failed to start audit") setLynisAuditRunning(false) } } catch (err) { setError(err instanceof Error ? err.message : "Failed to start audit") setLynisAuditRunning(false) } } const loadLynisReport = async () => { setLynisReportLoading(true) try { const data = await fetchApi("/api/security/lynis/report") if (data.success && data.report) { setLynisReport(data.report) } } catch { // ignore } finally { setLynisReportLoading(false) } } // Load report on mount if lynis is installed useEffect(() => { if (lynisInfo?.installed && lynisInfo?.last_scan) { loadLynisReport() } }, [lynisInfo?.installed, lynisInfo?.last_scan]) const openJailConfig = (jail: JailDetail) => { const bt = parseInt(jail.bantime, 10) const isPermanent = bt === -1 setF2bEditingJail(jail.name) setF2bJailConfig({ maxretry: jail.maxretry, bantime: isPermanent ? "" : jail.bantime, findtime: jail.findtime, permanent: isPermanent, }) } const handleSaveJailConfig = async () => { if (!f2bEditingJail) return setF2bSavingConfig(true) setError("") setSuccess("") try { const payload: Record = { jail: f2bEditingJail } if (f2bJailConfig.maxretry) payload.maxretry = parseInt(f2bJailConfig.maxretry, 10) if (f2bJailConfig.permanent) { payload.bantime = -1 } else if (f2bJailConfig.bantime) { payload.bantime = parseInt(f2bJailConfig.bantime, 10) } if (f2bJailConfig.findtime) payload.findtime = parseInt(f2bJailConfig.findtime, 10) const data = await fetchApi("/api/security/fail2ban/jail/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) if (data.success) { setSuccess(data.message || "Jail configuration updated") setF2bEditingJail(null) loadFail2banDetails() } else { setError(data.message || "Failed to update jail config") } } catch (err) { setError(err instanceof Error ? err.message : "Failed to update jail config") } finally { setF2bSavingConfig(false) } } // Load fail2ban details when basic info shows it's installed and active useEffect(() => { if (fail2banInfo?.installed && fail2banInfo?.active) { loadFail2banDetails() } }, [fail2banInfo?.installed, fail2banInfo?.active]) const formatBanTime = (seconds: string) => { const s = parseInt(seconds, 10) if (s === -1) return "Permanent" if (isNaN(s) || s <= 0) return seconds if (s < 60) return `${s}s` if (s < 3600) return `${Math.floor(s / 60)}m` if (s < 86400) return `${Math.floor(s / 3600)}h` return `${Math.floor(s / 86400)}d` } const handleAddRule = async () => { if (!newRule.dport && !newRule.source) { setError("Please specify at least a destination port or source address") return } setAddingRule(true) setError("") setSuccess("") try { const data = await fetchApi("/api/security/firewall/rules", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newRule), }) if (data.success) { setSuccess(data.message || "Rule added successfully") setShowAddRule(false) setNewRule({ direction: "IN", action: "ACCEPT", protocol: "tcp", dport: "", sport: "", source: "", iface: "", comment: "", level: "host" }) loadFirewallStatus() } else { setError(data.message || "Failed to add rule") } } catch (err) { setError(err instanceof Error ? err.message : "Failed to add rule") } finally { setAddingRule(false) } } const handleDeleteRule = async (ruleIndex: number, level: string) => { setDeletingRuleIdx(ruleIndex) setError("") setSuccess("") try { const data = await fetchApi("/api/security/firewall/rules", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rule_index: ruleIndex, level }), }) if (data.success) { setSuccess(data.message || "Rule deleted") loadFirewallStatus() } else { setError(data.message || "Failed to delete rule") } } catch (err) { setError(err instanceof Error ? err.message : "Failed to delete rule") } finally { setDeletingRuleIdx(null) } } const handleFirewallToggle = async (level: "host" | "cluster", enable: boolean) => { setFirewallAction(true) setError("") setSuccess("") try { const endpoint = enable ? "/api/security/firewall/enable" : "/api/security/firewall/disable" const data = await fetchApi(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ level }), }) if (data.success) { setSuccess(data.message || `Firewall ${enable ? "enabled" : "disabled"} at ${level} level`) loadFirewallStatus() } else { setError(data.message || "Failed to update firewall") } } catch (err) { setError(err instanceof Error ? err.message : "Failed to update firewall") } finally { setFirewallAction(false) } } const handleMonitorPortToggle = async (add: boolean) => { setFirewallAction(true) setError("") setSuccess("") try { const data = await fetchApi("/api/security/firewall/monitor-port", { method: add ? "POST" : "DELETE", }) if (data.success) { setSuccess(data.message || `Monitor port rule ${add ? "added" : "removed"}`) loadFirewallStatus() } else { setError(data.message || "Failed to update monitor port rule") } } catch (err) { setError(err instanceof Error ? err.message : "Failed to update monitor port rule") } finally { setFirewallAction(false) } } const checkAuthStatus = async () => { try { const response = await fetch(getApiUrl("/api/auth/status")) const data = await response.json() setAuthEnabled(data.auth_enabled || false) setTotpEnabled(data.totp_enabled || false) } catch (err) { console.error("Failed to check auth status:", err) } } const handleEnableAuth = async () => { setError("") setSuccess("") if (!username || !password) { setError("Please fill in all fields") return } if (password !== confirmPassword) { setError("Passwords do not match") return } if (password.length < 6) { setError("Password must be at least 6 characters") return } setLoading(true) try { const response = await fetch(getApiUrl("/api/auth/setup"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password, enable_auth: true, }), }) const data = await response.json() if (!response.ok) { throw new Error(data.error || "Failed to enable authentication") } localStorage.setItem("proxmenux-auth-token", data.token) localStorage.setItem("proxmenux-auth-setup-complete", "true") setSuccess("Authentication enabled successfully!") setAuthEnabled(true) setShowSetupForm(false) setUsername("") setPassword("") setConfirmPassword("") } catch (err) { setError(err instanceof Error ? err.message : "Failed to enable authentication") } finally { setLoading(false) } } const handleDisableAuth = async () => { if ( !confirm( "Are you sure you want to disable authentication? This will remove password protection from your dashboard.", ) ) { return } setLoading(true) setError("") setSuccess("") try { const token = localStorage.getItem("proxmenux-auth-token") const response = await fetch(getApiUrl("/api/auth/disable"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }) const data = await response.json() if (!response.ok) { throw new Error(data.message || "Failed to disable authentication") } localStorage.removeItem("proxmenux-auth-token") localStorage.removeItem("proxmenux-auth-setup-complete") setSuccess("Authentication disabled successfully! Reloading...") setTimeout(() => { window.location.reload() }, 1000) } catch (err) { setError(err instanceof Error ? err.message : "Failed to disable authentication. Please try again.") } finally { setLoading(false) } } const handleChangePassword = async () => { setError("") setSuccess("") if (!currentPassword || !newPassword) { setError("Please fill in all fields") return } if (newPassword !== confirmNewPassword) { setError("New passwords do not match") return } if (newPassword.length < 6) { setError("Password must be at least 6 characters") return } setLoading(true) try { const response = await fetch(getApiUrl("/api/auth/change-password"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${localStorage.getItem("proxmenux-auth-token")}`, }, body: JSON.stringify({ current_password: currentPassword, new_password: newPassword, }), }) const data = await response.json() if (!response.ok) { throw new Error(data.error || "Failed to change password") } if (data.token) { localStorage.setItem("proxmenux-auth-token", data.token) } setSuccess("Password changed successfully!") setShowChangePassword(false) setCurrentPassword("") setNewPassword("") setConfirmNewPassword("") } catch (err) { setError(err instanceof Error ? err.message : "Failed to change password") } finally { setLoading(false) } } const handleDisable2FA = async () => { setError("") setSuccess("") if (!disable2FAPassword) { setError("Please enter your password") return } setLoading(true) try { const token = localStorage.getItem("proxmenux-auth-token") const response = await fetch(getApiUrl("/api/auth/totp/disable"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ password: disable2FAPassword }), }) const data = await response.json() if (!response.ok) { throw new Error(data.message || "Failed to disable 2FA") } setSuccess("2FA disabled successfully!") setTotpEnabled(false) setShow2FADisable(false) setDisable2FAPassword("") checkAuthStatus() } catch (err) { setError(err instanceof Error ? err.message : "Failed to disable 2FA") } finally { setLoading(false) } } const handleLogout = () => { localStorage.removeItem("proxmenux-auth-token") localStorage.removeItem("proxmenux-auth-setup-complete") window.location.reload() } const loadApiTokens = async () => { try { setLoadingTokens(true) const data = await fetchApi("/api/auth/api-tokens") if (data.success) { setExistingTokens(data.tokens || []) } } catch { // Silently fail - tokens section is optional } finally { setLoadingTokens(false) } } const handleRevokeToken = async (tokenId: string) => { if (!confirm("Are you sure you want to revoke this token? Any integration using it will stop working immediately.")) { return } setRevokingTokenId(tokenId) setError("") setSuccess("") try { const data = await fetchApi(`/api/auth/api-tokens/${tokenId}`, { method: "DELETE", }) if (data.success) { setSuccess("Token revoked successfully") setExistingTokens((prev) => prev.filter((t) => t.id !== tokenId)) } else { setError(data.message || "Failed to revoke token") } } catch (err) { setError(err instanceof Error ? err.message : "Failed to revoke token") } finally { setRevokingTokenId(null) } } const handleGenerateApiToken = async () => { setError("") setSuccess("") if (!tokenPassword) { setError("Please enter your password") return } if (totpEnabled && !tokenTotpCode) { setError("Please enter your 2FA code") return } setGeneratingToken(true) try { const data = await fetchApi("/api/auth/generate-api-token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password: tokenPassword, totp_token: totpEnabled ? tokenTotpCode : undefined, token_name: tokenName || "API Token", }), }) if (!data.success) { setError(data.message || data.error || "Failed to generate API token") return } if (!data.token) { setError("No token received from server") return } setApiToken(data.token) setSuccess("API token generated successfully! Make sure to copy it now as you won't be able to see it again.") setTokenPassword("") setTokenTotpCode("") setTokenName("API Token") loadApiTokens() } catch (err) { setError(err instanceof Error ? err.message : "Failed to generate API token. Please try again.") } finally { setGeneratingToken(false) } } const copyToClipboard = async (text: string) => { try { if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") { await navigator.clipboard.writeText(text) } else { const textarea = document.createElement("textarea") textarea.value = text textarea.style.position = "fixed" textarea.style.left = "-9999px" textarea.style.top = "-9999px" textarea.style.opacity = "0" document.body.appendChild(textarea) textarea.focus() textarea.select() document.execCommand("copy") document.body.removeChild(textarea) } return true } catch { return false } } const copyApiToken = async () => { const ok = await copyToClipboard(apiToken) if (ok) { setTokenCopied(true) setTimeout(() => setTokenCopied(false), 2000) } } const generatePrintableReport = (report: LynisReport) => { const adjScore = report.proxmox_adjusted_score ?? report.hardening_index const rawScore = report.hardening_index const displayScore = adjScore ?? rawScore const hasAdjustment = adjScore != null && rawScore != null && adjScore !== rawScore const scoreColor = displayScore === null ? "#888" : displayScore >= 70 ? "#16a34a" : displayScore >= 50 ? "#ca8a04" : "#dc2626" const scoreLabel = displayScore === null ? "N/A" : displayScore >= 70 ? "GOOD" : displayScore >= 50 ? "MODERATE" : "CRITICAL" const now = new Date().toLocaleString() const logoUrl = `${window.location.origin}/images/proxmenux-logo.png` const actionableWarnings = report.warnings.length - (report.proxmox_expected_warnings ?? 0) const actionableSuggestions = report.suggestions.length - (report.proxmox_expected_suggestions ?? 0) const totalExpected = (report.proxmox_expected_warnings ?? 0) + (report.proxmox_expected_suggestions ?? 0) return ` Security Audit Report - ${report.hostname || "ProxMenux"}
ProxMenux Security Audit Report Review the report, then print or save as PDF
\u2318P / Ctrl+P
ProxMenux

Security Audit Report

ProxMenux Monitor - Lynis System Audit

Date: ${now}
Auditor: Lynis ${report.lynis_version || ""}
ID: PMXA-${Date.now().toString(36).toUpperCase()}
1. Executive Summary
${displayScore ?? "N/A"}
${scoreLabel}

System Hardening Assessment${hasAdjustment ? " (Proxmox Adjusted)" : ""}

Audit of ${report.hostname || "Unknown"} running ${report.os_fullname || `${report.os_name} ${report.os_version}`.trim() || "Unknown OS"} (Proxmox VE). ${report.tests_performed} tests executed. ${actionableWarnings > 0 ? `${actionableWarnings} actionable warning(s)` : 'No actionable warnings'} and ${actionableSuggestions} actionable suggestion(s). ${totalExpected > 0 ? `${totalExpected} findings are expected behavior in Proxmox VE.` : ""}

${hasAdjustment ? `
Lynis raw: ${rawScore}/100 PVE adjusted: ${displayScore}/100
0 - Critical50 - Moderate70 - Good100
` : ""}
2. System Information
Hostname
${report.hostname || "N/A"}
Operating System
${report.os_fullname || `${report.os_name} ${report.os_version}`.trim() || "N/A"}
Kernel
${report.kernel_version || "N/A"}
Lynis Version
${report.lynis_version || "N/A"}
Report Date
${report.datetime_start ? report.datetime_start.replace("T", " ").substring(0, 16) : "N/A"}
Tests Performed
${report.tests_performed}
3. Security Posture Overview
${displayScore ?? "N/A"}/100
PVE Score (${scoreLabel})
${hasAdjustment ? `
Lynis raw: ${rawScore}
` : ""}
${actionableWarnings}
Actionable Warnings
${(report.proxmox_expected_warnings ?? 0) > 0 ? `
+${report.proxmox_expected_warnings} PVE expected
` : ""}
${actionableSuggestions}
Actionable Suggestions
${(report.proxmox_expected_suggestions ?? 0) > 0 ? `
+${report.proxmox_expected_suggestions} PVE expected
` : ""}
${report.tests_performed}
Tests Performed
Firewall
${report.firewall_active ? "Active" : "Inactive"}
Malware Scanner
${report.malware_scanner ? "Installed" : "Not Found"}
Installed Packages
${report.installed_packages || "N/A"}
4. Warnings (${report.warnings.length}${(report.proxmox_expected_warnings ?? 0) > 0 ? ` - ${actionableWarnings} actionable` : ""})

Issues that require attention and may represent security vulnerabilities.

${report.warnings.length === 0 ? '
No warnings detected. System appears to be well-configured.
' : report.warnings.map((w, i) => `
#${i + 1} ${w.test_id} ${w.proxmox_expected ? 'PVE Expected' : ''} ${!w.proxmox_expected && w.proxmox_severity === "low" ? 'Low Risk' : ''} ${!w.proxmox_expected && !w.proxmox_severity && w.severity ? `${w.severity}` : ""}
${w.description}
${w.proxmox_context ? `
Proxmox: ${w.proxmox_context}
` : ""} ${w.solution ? `
Recommendation: ${w.solution}
` : ""}
`).join("")}
5. Suggestions (${report.suggestions.length}${(report.proxmox_expected_suggestions ?? 0) > 0 ? ` - ${actionableSuggestions} actionable` : ""})

Recommended improvements to strengthen your system's security posture.${(report.proxmox_expected_suggestions ?? 0) > 0 ? ` ${report.proxmox_expected_suggestions} items are expected behavior in Proxmox VE.` : ""}

${report.suggestions.length === 0 ? '
No suggestions. System is fully hardened.
' : report.suggestions.map((s, i) => `
#${i + 1} ${s.test_id} ${s.proxmox_expected ? 'PVE Expected' : ''} ${!s.proxmox_expected && s.proxmox_severity === "low" ? 'Low Priority' : ''}
${s.description}
${s.proxmox_context ? `
Proxmox: ${s.proxmox_context}
` : ""} ${s.solution ? `
Recommendation: ${s.solution}
` : ""} ${s.details ? `
${s.details}
` : ""}
`).join("")}
${(report.sections && report.sections.length > 0) ? `
6. Detailed Security Checks (${report.sections.length} categories)

Complete list of all security checks performed during the audit, organized by category.

${report.sections.map((section, sIdx) => `
${sIdx + 1} ${section.name} ${section.checks.length} checks
${section.checks.map(check => { const st = check.status.toUpperCase() const isWarn = ["WARNING", "UNSAFE", "WEAK", "DIFFERENT", "DISABLED"].includes(st) const isSugg = ["SUGGESTION", "PARTIALLY HARDENED", "MEDIUM", "NON DEFAULT"].includes(st) const isOk = ["OK", "FOUND", "DONE", "ENABLED", "ACTIVE", "YES", "HARDENED", "PROTECTED"].includes(st) const color = isWarn ? "#dc2626" : isSugg ? "#ca8a04" : isOk ? "#16a34a" : "#64748b" const cls = isWarn ? ' class="warn"' : isSugg ? ' class="sugg"' : "" return ` ` }).join("")}
CheckStatus
${check.name}${check.detail ? ` (${check.detail})` : ""} ${check.status}
`).join("")}
` : ""} ` } const loadSslStatus = async () => { try { setLoadingSsl(true) const data = await fetchApi("/api/ssl/status") if (data.success) { setSslEnabled(data.ssl_enabled || false) setSslSource(data.source || "none") setSslCertPath(data.cert_path || "") setSslKeyPath(data.key_path || "") setProxmoxCertAvailable(data.proxmox_available || false) setProxmoxCertInfo(data.cert_info || null) } } catch { // Silently fail } finally { setLoadingSsl(false) } } // Wait for the monitor service to come back on the new protocol, then redirect const waitForServiceAndRedirect = async (newProtocol: "https" | "http") => { const host = window.location.hostname const port = window.location.port || "8008" const newUrl = `${newProtocol}://${host}:${port}${window.location.pathname}` // Wait for service to restart (try up to 30 seconds) const maxAttempts = 15 for (let i = 0; i < maxAttempts; i++) { await new Promise(r => setTimeout(r, 2000)) try { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 3000) const resp = await fetch(`${newProtocol}://${host}:${port}/api/ssl/status`, { signal: controller.signal, // For self-signed certs, we need to handle rejection mode: "no-cors" }).catch(() => null) clearTimeout(timeout) // For HTTPS with self-signed certs, even a failed CORS request means the server is up if (resp || newProtocol === "https") { // Give it one more second to fully stabilize await new Promise(r => setTimeout(r, 1000)) window.location.href = newUrl return } } catch { // Server not ready yet, keep waiting } } // Fallback: redirect anyway after timeout window.location.href = newUrl } const handleEnableSsl = async (source: "proxmox" | "custom", certPath?: string, keyPath?: string) => { setConfiguringSsl(true) setError("") setSuccess("") try { const body: Record = { source, auto_restart: true } if (source === "custom" && certPath && keyPath) { body.cert_path = certPath body.key_path = keyPath } const data = await fetchApi("/api/ssl/configure", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }) if (data.success) { setSslEnabled(true) setSslSource(source) setShowCustomCertForm(false) setCustomCertPath("") setCustomKeyPath("") setConfiguringSsl(false) setSslRestarting(true) setSuccess("SSL enabled. Restarting service and switching to HTTPS...") await waitForServiceAndRedirect("https") } else { setError(data.message || "Failed to configure SSL") setConfiguringSsl(false) } } catch (err) { setError(err instanceof Error ? err.message : "Failed to configure SSL") setConfiguringSsl(false) } } const handleDisableSsl = async () => { if (!confirm("Are you sure you want to disable HTTPS? The monitor will switch to HTTP.")) { return } setConfiguringSsl(true) setError("") setSuccess("") try { const data = await fetchApi("/api/ssl/disable", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ auto_restart: true }), }) if (data.success) { setSslEnabled(false) setSslSource("none") setSslCertPath("") setSslKeyPath("") setConfiguringSsl(false) setSslRestarting(true) setSuccess("SSL disabled. Restarting service and switching to HTTP...") await waitForServiceAndRedirect("http") } else { setError(data.message || "Failed to disable SSL") setConfiguringSsl(false) } } catch (err) { setError(err instanceof Error ? err.message : "Failed to disable SSL") setConfiguringSsl(false) } } return (

Security

Manage authentication, encryption, and access control

{/* ── ProxMenux Monitor Security Group ── */}

ProxMenux Monitor

{/* Authentication Settings */}
Authentication
Protect your dashboard with username and password authentication
{error && (

{error}

)} {success && (

{success}

)}

Authentication Status

{authEnabled ? "Password protection is enabled" : "No password protection"}

{authEnabled ? "Enabled" : "Disabled"}
{!authEnabled && !showSetupForm && (

Enable authentication to protect your dashboard when accessing from non-private networks.

)} {!authEnabled && showSetupForm && (

Setup Authentication

setUsername(e.target.value)} className="pl-10" disabled={loading} />
setPassword(e.target.value)} className="pl-10" disabled={loading} />
setConfirmPassword(e.target.value)} className="pl-10" disabled={loading} />
)} {authEnabled && (
{!showChangePassword && ( )} {showChangePassword && (

Change Password

setCurrentPassword(e.target.value)} className="pl-10" disabled={loading} />
setNewPassword(e.target.value)} className="pl-10" disabled={loading} />
setConfirmNewPassword(e.target.value)} className="pl-10" disabled={loading} />
)} {!totpEnabled && (

Two-Factor Authentication (2FA)

Add an extra layer of security by requiring a code from your authenticator app in addition to your password.

)} {totpEnabled && (

2FA is enabled

{!show2FADisable && ( )} {show2FADisable && (

Disable Two-Factor Authentication

Enter your password to confirm

setDisable2FAPassword(e.target.value)} className="pl-10" disabled={loading} />
)}
)}
)}
{/* SSL/HTTPS Configuration */}
SSL / HTTPS
Serve ProxMenux Monitor over HTTPS using your Proxmox host certificate or a custom certificate
{loadingSsl ? (
) : ( <> {/* Current Status */}

{sslEnabled ? "HTTPS Enabled" : "HTTP (No SSL)"}

{sslEnabled ? `Using ${sslSource === "proxmox" ? "Proxmox host" : "custom"} certificate` : "Monitor is served over unencrypted HTTP"}

{sslEnabled ? "HTTPS" : "HTTP"}
{/* Active certificate info */} {sslEnabled && (
Active Certificate

Cert: {sslCertPath}

Key: {sslKeyPath}

)} {/* Proxmox certificate detection */} {!sslEnabled && proxmoxCertAvailable && (

Proxmox Host Certificate Detected

{proxmoxCertInfo && (
{proxmoxCertInfo.subject && (

Subject: {proxmoxCertInfo.subject}

)} {proxmoxCertInfo.issuer && (

Issuer: {proxmoxCertInfo.issuer}

)} {proxmoxCertInfo.expires && (

Expires: {proxmoxCertInfo.expires}

)} {proxmoxCertInfo.is_self_signed && (
Self-signed certificate (browsers will show a security warning)
)}
)}
)} {!sslEnabled && !proxmoxCertAvailable && (

No Proxmox host certificate detected. You can configure a custom certificate below.

)} {/* Custom certificate option */} {!sslEnabled && (
{!showCustomCertForm ? ( ) : (

Custom Certificate Paths

Enter the absolute paths to your SSL certificate and private key files on the Proxmox server.

setCustomCertPath(e.target.value)} disabled={configuringSsl} />
setCustomKeyPath(e.target.value)} disabled={configuringSsl} />
)}
)} {/* Restarting overlay or info note */} {sslRestarting ? (

Restarting monitor service...

The page will automatically redirect to the new address.

) : (

SSL changes will automatically restart the monitor service and redirect to the new address.

)} )} {/* API Access Tokens */} {authEnabled && (
API Access Tokens
Generate long-lived API tokens for external integrations like Homepage and Home Assistant
{error && (

{error}

)} {success && (

{success}

)}

About API Tokens

  • Tokens are valid for 1 year
  • Use them to access APIs from external services
  • {'Include in Authorization header: Bearer YOUR_TOKEN'}
  • See README.md for complete integration examples
{!showApiTokenSection && !apiToken && ( )} {showApiTokenSection && !apiToken && (

Generate API Token

Enter your credentials to generate a new long-lived API token

setTokenName(e.target.value)} className="pl-10" disabled={generatingToken} />
setTokenPassword(e.target.value)} className="pl-10" disabled={generatingToken} />
{totpEnabled && (
setTokenTotpCode(e.target.value)} className="pl-10" maxLength={6} disabled={generatingToken} />
)}
)} {apiToken && (

Your API Token

Important: Save this token now!

{"You won't be able to see it again. Store it securely."}

{tokenCopied && (

Copied to clipboard!

)}

How to use this token:

# Add to request headers:

{'Authorization: Bearer YOUR_TOKEN_HERE'}

See the README documentation for complete integration examples with Homepage and Home Assistant.

)} {/* Existing Tokens List */} {!loadingTokens && existingTokens.length > 0 && (

Active Tokens

{existingTokens.map((token) => (

{token.name}

{token.token_prefix} {token.created_at ? new Date(token.created_at).toLocaleDateString() : "Unknown"}
))}
)} {loadingTokens && (
Loading tokens...
)} {!loadingTokens && existingTokens.length === 0 && !showApiTokenSection && !apiToken && (
No API tokens created yet
)} )} {/* ── Proxmox VE Security Group ── */}

Proxmox VE

{/* Proxmox Firewall */}
Proxmox Firewall
{firewallData?.pve_firewall_installed && ( )}
Manage the Proxmox VE built-in firewall: enable/disable, configure rules, and protect your services
{firewallLoading ? (
) : !firewallData?.pve_firewall_installed ? (

Proxmox Firewall Not Detected

The pve-firewall service was not found on this system. It should be included with Proxmox VE by default.

) : ( <> {/* Firewall Status Overview */}
{/* Cluster Firewall */}

Cluster Firewall

{firewallData.cluster_fw_enabled ? "Active - Required for host rules to work" : "Disabled - Must be enabled first"}

{/* Host Firewall */}

Host Firewall

{firewallData.host_fw_enabled ? "Active - Rules are being enforced" : "Disabled"}

{!firewallData.cluster_fw_enabled && (

The Cluster Firewall must be enabled for any host-level firewall rules to take effect. Enable it first, then configure your host rules.

)} {/* Quick Presets */}

Quick Access Rules

{/* Monitor Port 8008 */}

ProxMenux Monitor

Port 8008/TCP

{/* Proxmox Web UI hint */}

Proxmox Web UI

Port 8006/TCP (always allowed)

Built-in
{!firewallData.monitor_port_open && (firewallData.cluster_fw_enabled || firewallData.host_fw_enabled) && (

The firewall is active but port 8008 is not allowed. ProxMenux Monitor may be inaccessible from other devices.

)}
{/* Rules Summary Dashboard */} {firewallData.rules.length > 0 && (() => { const acceptCount = firewallData.rules.filter(r => r.action === "ACCEPT").length const dropCount = firewallData.rules.filter(r => r.action === "DROP").length const rejectCount = firewallData.rules.filter(r => r.action === "REJECT").length const blockCount = dropCount + rejectCount const total = firewallData.rules.length const clusterCount = firewallData.rules.filter(r => r.source_file === "cluster").length const hostCount = firewallData.rules.filter(r => r.source_file === "host").length const inCount = firewallData.rules.filter(r => (r.direction || "IN") === "IN").length const outCount = firewallData.rules.filter(r => r.direction === "OUT").length // Collect unique protected ports const protectedPorts = new Set() firewallData.rules.forEach(r => { if (r.dport) r.dport.split(",").forEach(p => protectedPorts.add(p.trim())) }) return (

Rules Overview

{total}

Total Rules

{acceptCount}

Accept

{blockCount}

Block / Reject

{protectedPorts.size}

Ports Covered

{/* Visual bar */}
{acceptCount > 0 && (
)} {dropCount > 0 && (
)} {rejectCount > 0 && (
)}
Accept Drop Reject
{/* Scope breakdown */}
Cluster: {clusterCount} Host: {hostCount} | IN: {inCount} OUT: {outCount}
) })()} {/* Firewall Rules */}

Firewall Rules ({firewallData.rules_count})

{/* Add Rule Form */} {showAddRule && (

New Firewall Rule

{/* Service Presets */}

Quick Presets

{[ { label: "HTTP", port: "80", proto: "tcp", comment: "HTTP Web" }, { label: "HTTPS", port: "443", proto: "tcp", comment: "HTTPS Web" }, { label: "SSH", port: "22", proto: "tcp", comment: "SSH Remote Access" }, { label: "DNS", port: "53", proto: "udp", comment: "DNS" }, { label: "SMTP", port: "25", proto: "tcp", comment: "SMTP Mail" }, { label: "NFS", port: "2049", proto: "tcp", comment: "NFS" }, { label: "SMB", port: "445", proto: "tcp", comment: "SMB/CIFS" }, { label: "Ping", port: "", proto: "icmp", comment: "ICMP Ping" }, ].map((preset) => ( ))}
setNewRule({...newRule, dport: e.target.value})} className="h-9 text-sm" />

Single port, comma-separated, or range (8000:9000)

setNewRule({...newRule, source: e.target.value})} className="h-9 text-sm" />

IP, CIDR, or leave empty for any source

setNewRule({...newRule, iface: e.target.value})} className="h-9 text-sm" />
setNewRule({...newRule, comment: e.target.value})} className="h-9 text-sm" />
)} {/* Rules List */} {firewallData.rules.length > 0 ? (
{/* Table header */}
Action Proto Port Source Level
{firewallData.rules.map((rule, idx) => { const ruleKey = `${rule.source_file}-${rule.rule_index}` const isExpanded = expandedRuleKey === ruleKey const direction = rule.direction || "IN" const comment = rule.raw?.includes("#") ? rule.raw.split("#").slice(1).join("#").trim() : "" return (
{/* Main row */}
setExpandedRuleKey(isExpanded ? null : ruleKey)} > {/* Direction icon */}
{direction === "IN" ? ( ) : ( )}
{/* Action badge */} {rule.action || "?"} {/* Mobile: combined info */}
{rule.p || "*"} / {rule.dport || "*"} {comment && - {comment}}
{/* Desktop: direction label */} {direction} {/* Protocol */} {rule.p || "*"} {/* Port */} {rule.dport || "*"} {/* Source */} {rule.source || "any"} {/* Level badge */} {rule.source_file} {/* Expand/Delete */}
{/* Expanded details */} {isExpanded && (

Direction

{direction === "IN" ? : } {direction === "IN" ? "Incoming" : "Outgoing"}

Protocol

{rule.p || "any"}

Port

{rule.dport || "any"}

Source

{rule.source || "any"}

{rule.i && (

Interface

{rule.i}

)}

Scope

{rule.source_file === "cluster" ? : } {rule.source_file === "cluster" ? "Cluster" : "Host"}

{comment && (

Comment

{comment}

)}
{rule.raw}
)}
) })}
) : (

No firewall rules configured yet

Click "Add Rule" above to create your first rule

)}
)} {/* Fail2Ban */}
Fail2Ban
{fail2banInfo?.installed && fail2banInfo?.active && ( )}
Intrusion prevention system that bans IPs after repeated failed login attempts
{toolsLoading ? (
) : !fail2banInfo?.installed ? ( /* --- NOT INSTALLED --- */

Fail2Ban Not Installed

Protect SSH, Proxmox web interface, and ProxMenux Monitor from brute force attacks

Not Installed

What Fail2Ban will configure:

  • SSH protection (max 2 retries, 9h ban)
  • Proxmox web interface protection (port 8006, max 3 retries, 1h ban)
  • ProxMenux Monitor protection (port 8008 + reverse proxy, max 3 retries, 1h ban)
  • Global settings with nftables backend

All settings can be customized after installation. You can change retries, ban time, or set permanent bans.

) : ( /* --- INSTALLED --- */
{/* Status bar */}

{fail2banInfo.version}

{fail2banInfo.active ? "Service is running" : "Service is not running"}

{fail2banInfo.active ? "Active" : "Inactive"}
{fail2banInfo.active && f2bDetails && ( <> {/* Summary stats - inline */}
Jails: {f2bDetails.jails.length}
Banned IPs: a + j.currently_banned, 0) > 0 ? "text-red-500" : "text-green-500"}`}> {f2bDetails.jails.reduce((a, j) => a + j.currently_banned, 0)}
Total Bans: {f2bDetails.jails.reduce((a, j) => a + j.total_banned, 0)}
Failed Attempts: {f2bDetails.jails.reduce((a, j) => a + j.total_failed, 0)}
{/* Missing jails warning */} {(() => { const expectedJails = ["sshd", "proxmox", "proxmenux"] const currentNames = f2bDetails.jails.map(j => j.name.toLowerCase()) const missing = expectedJails.filter(j => !currentNames.includes(j)) if (missing.length === 0) return null const jailLabels: Record = { sshd: "SSH (sshd)", proxmox: "Proxmox UI (port 8006)", proxmenux: "ProxMenux Monitor (port 8008)", } return (

Missing protections detected

The following jails are not configured:{" "} {missing.map(j => jailLabels[j] || j).join(", ")}

) })()} {/* Tab switcher */}
{/* JAILS TAB */} {f2bActiveTab === "jails" && (
{f2bDetails.jails.map((jail) => (
{/* Jail header */}
0 ? "bg-red-500 animate-pulse" : "bg-green-500"}`} /> {jail.name} {jail.name === "sshd" ? "SSH Remote Access" : jail.name === "proxmox" ? "Proxmox UI :8006" : jail.name === "proxmenux" ? "ProxMenux Monitor :8008" : ""} {parseInt(jail.bantime, 10) === -1 && ( PERMANENT BAN )}
Retries: {jail.maxretry} Ban: {parseInt(jail.bantime, 10) === -1 ? "Permanent" : formatBanTime(jail.bantime)} Window: {formatBanTime(jail.findtime)}
{/* Jail config editor */} {f2bEditingJail === jail.name && (

Configure {jail.name}

setF2bJailConfig({...f2bJailConfig, maxretry: e.target.value})} className="h-9 text-sm" placeholder="e.g. 3" />

Failed attempts before ban

setF2bJailConfig({...f2bJailConfig, bantime: e.target.value, permanent: false})} className="h-9 text-sm" placeholder={f2bJailConfig.permanent ? "Permanent" : "e.g. 3600 = 1h"} disabled={f2bJailConfig.permanent} />
setF2bJailConfig({...f2bJailConfig, permanent: e.target.checked, bantime: ""})} className="rounded border-border" />
setF2bJailConfig({...f2bJailConfig, findtime: e.target.value})} className="h-9 text-sm" placeholder="e.g. 600 = 10m" />

Time window for counting retries

Common values: 600s = 10min, 3600s = 1h, 32400s = 9h, 86400s = 24h. Set ban to permanent if you want blocked IPs to stay blocked until you manually unban them.

)} {/* Mobile config summary (visible only on small screens) */}
Retries: {jail.maxretry} Ban: {parseInt(jail.bantime, 10) === -1 ? "Perm" : formatBanTime(jail.bantime)} Window: {formatBanTime(jail.findtime)}
{/* Jail stats - inline */}
Banned: 0 ? "text-red-500" : "text-green-500"}`}> {jail.currently_banned}
Total Bans: {jail.total_banned}
Failed Now: {jail.currently_failed}
Total Failed: {jail.total_failed}
{/* Banned IPs list */} {jail.banned_ips.length > 0 && (

Banned IPs ({jail.banned_ips.length})

{jail.banned_ips.map((entry) => (
{entry.ip} {entry.type === "local" ? "LAN" : entry.type === "external" ? "External" : "Unknown"}
))}
)} {jail.currently_banned === 0 && (

No IPs currently banned in this jail

)}
))} {f2bDetails.jails.length === 0 && (
No jails configured
)}
)} {/* ACTIVITY TAB */} {f2bActiveTab === "activity" && (
{f2bActivity.length === 0 ? (
No recent activity in the Fail2Ban log
) : ( f2bActivity.map((event, idx) => (
{event.action}
{event.ip} {event.jail} {event.timestamp}
)) )}
)} )} {fail2banInfo.active && !f2bDetails && f2bDetailsLoading && (
)}
)} {/* Lynis */}
Lynis Security Audit
System security auditing tool that performs comprehensive security scans
{toolsLoading ? (
) : !lynisInfo?.installed ? (

Lynis Not Installed

Comprehensive security auditing and hardening tool

Not Installed

Lynis features:

  • System hardening scoring (0-100)
  • Vulnerability detection and suggestions
  • Compliance checking (PCI-DSS, HIPAA, etc.)
  • Installed from latest GitHub source
) : (
{/* Status bar */}

Lynis {lynisInfo.version}

Security auditing tool installed

Installed
{/* Summary stats */}

Last Scan

{lynisInfo.last_scan ? lynisInfo.last_scan.replace("T", " ").substring(0, 16) : "Never"}

Hardening Index

{(() => { const rawScore = lynisReport?.hardening_index ?? lynisInfo.hardening_index const adjScore = lynisReport?.proxmox_adjusted_score const displayScore = adjScore ?? rawScore const scoreColorClass = displayScore === null || displayScore === undefined ? "text-muted-foreground" : displayScore >= 70 ? "text-green-500" : displayScore >= 50 ? "text-yellow-500" : "text-red-500" return (

{displayScore !== null && displayScore !== undefined ? displayScore : "N/A"}

{adjScore != null && rawScore != null && adjScore !== rawScore && (

Lynis: {rawScore} | PVE: {adjScore}

)}
) })()}

Warnings

{(() => { if (!lynisReport) return

-

const total = lynisReport.warnings.length const expected = lynisReport.proxmox_expected_warnings ?? 0 const real = total - expected return (

0 ? "text-red-500" : total > 0 ? "text-yellow-500" : "text-green-500"}`}> {real > 0 ? real : total}

{expected > 0 && (

+{expected} PVE expected

)}
) })()}

Suggestions

{(() => { if (!lynisReport) return

-

const total = lynisReport.suggestions.length const expected = lynisReport.proxmox_expected_suggestions ?? 0 const real = total - expected return (

0 ? "text-yellow-500" : "text-green-500"}`}> {real > 0 ? real : total}

{expected > 0 && (

+{expected} PVE expected

)}
) })()}
{/* Hardening bar */} {(() => { const rawScore = lynisReport?.hardening_index ?? lynisInfo.hardening_index const adjScore = lynisReport?.proxmox_adjusted_score if (rawScore === null || rawScore === undefined) return null const displayScore = adjScore ?? rawScore const hasAdjustment = adjScore != null && adjScore !== rawScore return (
Security Hardening Score {hasAdjustment && (Proxmox Adjusted)} = 70 ? "text-green-500" : displayScore >= 50 ? "text-yellow-500" : "text-red-500" }`}> {displayScore}/100
{hasAdjustment ? (
{/* Raw score bar (dimmed) */}
{/* Adjusted score bar */}
= 70 ? "bg-green-500" : displayScore >= 50 ? "bg-yellow-500" : "bg-red-500" }`} style={{ width: `${displayScore}%` }} />
) : (
= 70 ? "bg-green-500" : displayScore >= 50 ? "bg-yellow-500" : "bg-red-500" }`} style={{ width: `${displayScore}%` }} />
)}
Critical (0-49) Moderate (50-69) Good (70-100)
{hasAdjustment && (

Lynis raw score: {rawScore}/100 | {(lynisReport?.proxmox_expected_warnings ?? 0) + (lynisReport?.proxmox_expected_suggestions ?? 0)} findings are expected in Proxmox VE

)}
) })()} {/* Running indicator */} {lynisAuditRunning && (

Security audit in progress...

This may take 2-5 minutes. Lynis is scanning your system for vulnerabilities, misconfigurations, and hardening opportunities.

)} {/* Reports list */} {lynisReport && (

Audit Reports

{/* Report row - clickable to expand */}
{/* Expanded report details */} {lynisShowReport && (
{/* System info strip */}

Hostname

{lynisReport.hostname || "N/A"}

OS

{lynisReport.os_fullname || `${lynisReport.os_name} ${lynisReport.os_version}`.trim() || "N/A"}

Kernel

{lynisReport.kernel_version || "N/A"}

Tests

{lynisReport.tests_performed}

{/* Report tabs */}
{(["overview", "checks", "warnings", "suggestions"] as const).map((tab) => ( ))}
{/* Overview tab */} {lynisActiveTab === "overview" && (

Packages

{lynisReport.installed_packages || "N/A"}

Firewall

{lynisReport.firewall_active ? "Active" : "Inactive"}

Malware Scanner

{lynisReport.malware_scanner ? "Installed" : "Not Found"}

{/* Security checklist */}

Quick Status

{(() => { const adjScore = lynisReport.proxmox_adjusted_score ?? lynisReport.hardening_index ?? 0 const realWarnings = lynisReport.warnings.length - (lynisReport.proxmox_expected_warnings ?? 0) return [ { label: "Firewall", ok: lynisReport.firewall_active, passText: "Active", failText: "Inactive", }, { label: "Malware Scanner", ok: lynisReport.malware_scanner, passText: "Installed", failText: "Not Installed", isWarning: true, }, { label: "Warnings", ok: realWarnings <= 0, passText: lynisReport.warnings.length === 0 ? "None" : `${lynisReport.warnings.length} (all PVE expected)`, failText: `${realWarnings} actionable` + (lynisReport.proxmox_expected_warnings ? ` + ${lynisReport.proxmox_expected_warnings} PVE` : ""), isWarning: realWarnings > 0 && realWarnings <= 5, }, { label: "Hardening Score (PVE)", ok: adjScore >= 70, passText: `${adjScore}/100`, failText: `${adjScore}/100 (< 70)`, isWarning: adjScore >= 50, }, ].map((item) => { const color = item.ok ? "green" : item.isWarning ? "yellow" : "red" return (
{item.label} {item.ok ? item.passText : item.failText}
)}) })()}
)} {/* Checks tab */} {lynisActiveTab === "checks" && (
{(!lynisReport.sections || lynisReport.sections.length === 0) ? (
No check details available. Run an audit to generate detailed results.
) : (
{lynisReport.sections.map((section, sIdx) => (
{sIdx + 1} {section.name} {section.checks.length} checks
{section.checks.map((check, cIdx) => { const st = check.status.toUpperCase() const isOk = ["OK", "FOUND", "DONE", "ENABLED", "ACTIVE", "YES", "HARDENED", "PROTECTED", "NONE", "NOT FOUND", "NOT RUNNING", "NOT ACTIVE", "NOT ENABLED", "DEFAULT", "NO"].includes(st) const isWarn = ["WARNING", "UNSAFE", "WEAK", "DIFFERENT", "DISABLED"].includes(st) const isSugg = ["SUGGESTION", "PARTIALLY HARDENED", "MEDIUM", "NON DEFAULT"].includes(st) const dotColor = isWarn ? "bg-red-500" : isSugg ? "bg-yellow-500" : isOk ? "bg-green-500" : "bg-muted-foreground" const textColor = isWarn ? "text-red-500" : isSugg ? "text-yellow-500" : isOk ? "text-green-500" : "text-muted-foreground" return (
{check.name} {check.detail && {check.detail}} {check.status}
) })}
))}
)}
)} {/* Warnings tab */} {lynisActiveTab === "warnings" && (
{lynisReport.warnings.length === 0 ? (
No warnings found. Your system is well configured.
) : (
{lynisReport.warnings.map((w, idx) => (
{w.test_id} {w.proxmox_expected && ( PVE Expected )} {!w.proxmox_expected && w.proxmox_severity === "low" && ( Low Risk )} {!w.proxmox_expected && !w.proxmox_severity && w.severity && ( {w.severity} )}

{w.description}

{w.proxmox_context && (

Proxmox: {w.proxmox_context}

)} {w.solution && (

Solution: {w.solution}

)}
))}
)}
)} {/* Suggestions tab */} {lynisActiveTab === "suggestions" && (
{lynisReport.suggestions.length === 0 ? (
No suggestions. System is fully hardened.
) : (
{lynisReport.suggestions.map((s, idx) => (
{s.test_id} {s.proxmox_expected && ( PVE Expected )} {!s.proxmox_expected && s.proxmox_severity === "low" && ( Low Priority )}

{s.description}

{s.proxmox_context && (

Proxmox: {s.proxmox_context}

)} {s.solution && (

Solution: {s.solution}

)} {s.details && (

{s.details}

)}
))}
)}
)}
)}
)} {/* Run audit button - at the bottom */}
)} {/* Script Terminal Modals */} { setShowFail2banInstaller(false) loadSecurityTools() }} scriptPath="/usr/local/share/proxmenux/scripts/security/fail2ban_installer.sh" scriptName="fail2ban_installer" params={{ EXECUTION_MODE: "web" }} title="Fail2Ban Installation" description="Installing and configuring Fail2Ban for SSH and Proxmox protection..." /> { setShowLynisInstaller(false) loadSecurityTools() }} scriptPath="/usr/local/share/proxmenux/scripts/security/lynis_installer.sh" scriptName="lynis_installer" params={{ EXECUTION_MODE: "web" }} title="Lynis Installation" description="Installing Lynis security auditing tool from GitHub..." /> setShow2FASetup(false)} onSuccess={() => { setSuccess("2FA enabled successfully!") checkAuthStatus() }} />
) }