diff --git a/AppImage/components/notification-settings.tsx b/AppImage/components/notification-settings.tsx index 4afbaae3..fde9c8e1 100644 --- a/AppImage/components/notification-settings.tsx +++ b/AppImage/components/notification-settings.tsx @@ -998,7 +998,7 @@ export function NotificationSettings() { - {/* ── Service Status ── */} + {/* ─�� Service Status ── */} {status && (
diff --git a/AppImage/components/security.tsx b/AppImage/components/security.tsx index 07334ca6..601e09be 100644 --- a/AppImage/components/security.tsx +++ b/AppImage/components/security.tsx @@ -109,6 +109,10 @@ export function Security() { } | null>(null) const [showFail2banInstaller, setShowFail2banInstaller] = useState(false) const [showLynisInstaller, setShowLynisInstaller] = useState(false) + const [uninstallingFail2ban, setUninstallingFail2ban] = useState(false) + const [uninstallingLynis, setUninstallingLynis] = useState(false) + const [showFail2banUninstallConfirm, setShowFail2banUninstallConfirm] = useState(false) + const [showLynisUninstallConfirm, setShowLynisUninstallConfirm] = 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 } @@ -251,6 +255,52 @@ export function Security() { } } + const handleUninstallFail2ban = async () => { + setUninstallingFail2ban(true) + setError("") + setSuccess("") + setShowFail2banUninstallConfirm(false) + try { + const data = await fetchApi("/api/security/fail2ban/uninstall", { + method: "POST", + }) + if (data.success) { + setSuccess(data.message || "Fail2Ban has been uninstalled") + loadSecurityTools() + setF2bDetails(null) + } else { + setError(data.message || "Failed to uninstall Fail2Ban") + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to uninstall Fail2Ban") + } finally { + setUninstallingFail2ban(false) + } + } + + const handleUninstallLynis = async () => { + setUninstallingLynis(true) + setError("") + setSuccess("") + setShowLynisUninstallConfirm(false) + try { + const data = await fetchApi("/api/security/lynis/uninstall", { + method: "POST", + }) + if (data.success) { + setSuccess(data.message || "Lynis has been uninstalled") + loadSecurityTools() + setLynisReport(null) + } else { + setError(data.message || "Failed to uninstall Lynis") + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to uninstall Lynis") + } finally { + setUninstallingLynis(false) + } + } + const loadFail2banDetails = async () => { try { setF2bDetailsLoading(true) @@ -2956,16 +3006,34 @@ ${(report.sections && report.sections.length > 0) ? ` Fail2Ban
- {fail2banInfo?.installed && fail2banInfo?.active && ( - + {fail2banInfo?.installed && ( +
+ {fail2banInfo?.active && ( + + )} + +
)}
@@ -3417,9 +3485,27 @@ ${(report.sections && report.sections.length > 0) ? ` {/* Lynis */} -
- - Lynis Security Audit +
+
+ + Lynis Security Audit +
+ {lynisInfo?.installed && ( + + )}
System security auditing tool that performs comprehensive security scans @@ -4019,6 +4105,107 @@ ${(report.sections && report.sections.length > 0) ? ` description="Installing Lynis security auditing tool from GitHub..." /> + {/* Uninstall Confirmation Dialogs */} + {showFail2banUninstallConfirm && ( +
+
+
+
+ +
+
+

Uninstall Fail2Ban?

+

This action cannot be undone

+
+
+

+ This will completely remove Fail2Ban and all its configuration, including: +

+
    +
  • SSH protection jail
  • +
  • Proxmox web interface protection
  • +
  • ProxMenux Monitor protection
  • +
  • All custom jail configurations
  • +
  • Auth logger services
  • +
+
+ + +
+
+
+ )} + + {showLynisUninstallConfirm && ( +
+
+
+
+ +
+
+

Uninstall Lynis?

+

This action cannot be undone

+
+
+

+ This will completely remove Lynis and all audit data, including: +

+
    +
  • Lynis installation (/opt/lynis)
  • +
  • Wrapper script (/usr/local/bin/lynis)
  • +
  • All audit reports and logs
  • +
+
+ + +
+
+
+ )} + setShow2FASetup(false)} diff --git a/AppImage/scripts/flask_notification_routes.py b/AppImage/scripts/flask_notification_routes.py index 065f3435..7c3294b7 100644 --- a/AppImage/scripts/flask_notification_routes.py +++ b/AppImage/scripts/flask_notification_routes.py @@ -13,7 +13,7 @@ from flask import Blueprint, jsonify, request from notification_manager import notification_manager -# ─── Webhook Hardening Helpers ──────────────────────────────────��� +# ─── Webhook Hardening Helpers ─────────────────────────────────── class WebhookRateLimiter: """Simple sliding-window rate limiter for the webhook endpoint.""" diff --git a/AppImage/scripts/flask_security_routes.py b/AppImage/scripts/flask_security_routes.py index 9870c72b..e1195d2c 100644 --- a/AppImage/scripts/flask_security_routes.py +++ b/AppImage/scripts/flask_security_routes.py @@ -308,6 +308,34 @@ def lynis_report_delete(): return jsonify({"success": False, "message": str(e)}), 500 +# ------------------------------------------------------------------- +# Security Tools Uninstall +# ------------------------------------------------------------------- + +@security_bp.route('/api/security/fail2ban/uninstall', methods=['POST']) +def fail2ban_uninstall(): + """Uninstall Fail2Ban and clean up configuration""" + if not security_manager: + return jsonify({"success": False, "message": "Security manager not available"}), 500 + try: + success, message = security_manager.uninstall_fail2ban() + return jsonify({"success": success, "message": message}) + except Exception as e: + return jsonify({"success": False, "message": str(e)}), 500 + + +@security_bp.route('/api/security/lynis/uninstall', methods=['POST']) +def lynis_uninstall(): + """Uninstall Lynis and clean up files""" + if not security_manager: + return jsonify({"success": False, "message": "Security manager not available"}), 500 + try: + success, message = security_manager.uninstall_lynis() + return jsonify({"success": success, "message": message}) + except Exception as e: + return jsonify({"success": False, "message": str(e)}), 500 + + # ------------------------------------------------------------------- # Security Tools Detection # ------------------------------------------------------------------- diff --git a/AppImage/scripts/security_manager.py b/AppImage/scripts/security_manager.py index 8958a284..47d4b657 100644 --- a/AppImage/scripts/security_manager.py +++ b/AppImage/scripts/security_manager.py @@ -1984,3 +1984,149 @@ def parse_lynis_report(): report["proxmox_context_applied"] = True return report + + +# ------------------------------------------------------------------- +# Uninstall Functions +# ------------------------------------------------------------------- + +def uninstall_fail2ban(): + """ + Uninstall Fail2Ban and clean up all configuration. + Returns (success, message). + """ + try: + # Stop fail2ban service + _run_cmd(["systemctl", "stop", "fail2ban"], timeout=30) + _run_cmd(["systemctl", "disable", "fail2ban"], timeout=10) + + # Stop and remove auth logger services + _run_cmd(["systemctl", "stop", "proxmox-auth-logger.service"], timeout=10) + _run_cmd(["systemctl", "disable", "proxmox-auth-logger.service"], timeout=10) + _run_cmd(["systemctl", "stop", "ssh-auth-logger.service"], timeout=10) + _run_cmd(["systemctl", "disable", "ssh-auth-logger.service"], timeout=10) + + # Remove systemd service files + for svc_file in [ + "/etc/systemd/system/proxmox-auth-logger.service", + "/etc/systemd/system/ssh-auth-logger.service", + ]: + if os.path.exists(svc_file): + os.remove(svc_file) + + _run_cmd(["systemctl", "daemon-reload"], timeout=10) + + # Remove log files created by auth loggers + for log_file in ["/var/log/proxmox-auth.log", "/var/log/ssh-auth.log"]: + if os.path.exists(log_file): + os.remove(log_file) + + # Purge fail2ban package + _run_cmd(["apt-get", "purge", "-y", "fail2ban"], timeout=120) + + # Remove configuration files + for cfg_file in [ + "/etc/fail2ban/jail.d/proxmox.conf", + "/etc/fail2ban/jail.d/proxmenux.conf", + "/etc/fail2ban/filter.d/proxmox.conf", + "/etc/fail2ban/filter.d/proxmenux.conf", + "/etc/fail2ban/jail.local", + ]: + if os.path.exists(cfg_file): + os.remove(cfg_file) + + # Restore SSH MaxAuthTries if backup exists + base_dir = "/usr/local/share/proxmenux" + backup_file = os.path.join(base_dir, "sshd_maxauthtries_backup") + sshd_config = "/etc/ssh/sshd_config" + if os.path.exists(backup_file) and os.path.exists(sshd_config): + try: + with open(backup_file, 'r') as f: + original_val = f.read().strip() + if original_val: + with open(sshd_config, 'r') as f: + content = f.read() + import re + content = re.sub( + r'^MaxAuthTries.*$', + f'MaxAuthTries {original_val}', + content, + flags=re.MULTILINE + ) + with open(sshd_config, 'w') as f: + f.write(content) + _run_cmd(["systemctl", "reload", "sshd"], timeout=10) + os.remove(backup_file) + except Exception: + pass + + # Remove journald drop-in + journald_dropin = "/etc/systemd/journald.conf.d/proxmenux-loglevel.conf" + if os.path.exists(journald_dropin): + os.remove(journald_dropin) + _run_cmd(["systemctl", "restart", "systemd-journald"], timeout=30) + + # Update component status + components_file = os.path.join(base_dir, "components_status.json") + if os.path.exists(components_file): + try: + import json + with open(components_file, 'r') as f: + components = json.load(f) + if "fail2ban" in components: + components["fail2ban"]["status"] = "removed" + components["fail2ban"]["version"] = "" + with open(components_file, 'w') as f: + json.dump(components, f, indent=2) + except Exception: + pass + + return True, "Fail2Ban has been uninstalled successfully" + except Exception as e: + return False, f"Error uninstalling Fail2Ban: {str(e)}" + + +def uninstall_lynis(): + """ + Uninstall Lynis and clean up all files. + Returns (success, message). + """ + try: + import shutil + + # Remove installation directory + if os.path.exists("/opt/lynis"): + shutil.rmtree("/opt/lynis") + + # Remove wrapper script + if os.path.exists("/usr/local/bin/lynis"): + os.remove("/usr/local/bin/lynis") + + # Remove report files + for report_file in [ + "/var/log/lynis-report.dat", + "/var/log/lynis.log", + "/var/log/lynis-output.log", + ]: + if os.path.exists(report_file): + os.remove(report_file) + + # Update component status + base_dir = "/usr/local/share/proxmenux" + components_file = os.path.join(base_dir, "components_status.json") + if os.path.exists(components_file): + try: + import json + with open(components_file, 'r') as f: + components = json.load(f) + if "lynis" in components: + components["lynis"]["status"] = "removed" + components["lynis"]["version"] = "" + with open(components_file, 'w') as f: + json.dump(components, f, indent=2) + except Exception: + pass + + return True, "Lynis has been uninstalled successfully" + except Exception as e: + return False, f"Error uninstalling Lynis: {str(e)}"