diff --git a/AppImage/components/security.tsx b/AppImage/components/security.tsx index 6aaad67f..79232e19 100644 --- a/AppImage/components/security.tsx +++ b/AppImage/components/security.tsx @@ -9,6 +9,7 @@ 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, } from "lucide-react" import { getApiUrl, fetchApi } from "../lib/api-config" import { TwoFactorSetup } from "./two-factor-setup" @@ -99,6 +100,24 @@ export function Security() { const [showFail2banInstaller, setShowFail2banInstaller] = useState(false) const [showLynisInstaller, setShowLynisInstaller] = useState(false) + // Lynis audit state + interface LynisWarning { test_id: string; severity: string; description: string; solution: string } + interface LynisSuggestion { test_id: string; description: string; solution: string; details: string } + interface LynisReport { + datetime_start: string; datetime_end: string; lynis_version: string + os_name: string; os_version: 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 + } + 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">("overview") + // Fail2Ban detailed state interface BannedIp { ip: string @@ -266,6 +285,65 @@ export function Security() { } } + // --- 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 @@ -759,6 +837,281 @@ export function Security() { } } + const generatePrintableReport = (report: LynisReport) => { + const scoreColor = report.hardening_index === null ? "#888" + : report.hardening_index >= 70 ? "#16a34a" + : report.hardening_index >= 50 ? "#ca8a04" + : "#dc2626" + const scoreLabel = report.hardening_index === null ? "N/A" + : report.hardening_index >= 70 ? "GOOD" + : report.hardening_index >= 50 ? "MODERATE" + : "CRITICAL" + const now = new Date().toLocaleString() + const logoUrl = `${window.location.origin}/images/proxmenux-logo.png` + + return ` + + + +Security Audit Report - ${report.hostname || "ProxMenux"} + + + + + + + +
+
+ ProxMenux +
+

Security Audit Report

+

ProxMenux Monitor - Lynis System Audit

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

System Hardening Assessment

+

+ This automated security audit was performed on host ${report.hostname || "Unknown"} + running ${report.os_name} ${report.os_version}. + A total of ${report.tests_performed} tests were executed, + resulting in ${report.warnings.length} warning(s) + and ${report.suggestions.length} suggestion(s) for improvement. +

+
+
+
+ + +
+
2. System Information
+
+
+
Hostname
+
${report.hostname || "N/A"}
+
+
+
Operating System
+
${report.os_name} ${report.os_version}
+
+
+
Kernel
+
${report.kernel_version || "N/A"}
+
+
+
Lynis Version
+
${report.lynis_version || "N/A"}
+
+
+
Scan Started
+
${report.datetime_start || "N/A"}
+
+
+
Scan Ended
+
${report.datetime_end || "N/A"}
+
+
+
+ + +
+
3. Security Posture Overview
+
+
+
${report.hardening_index ?? "N/A"}
+
Hardening Score
+
+
+
${report.warnings.length}
+
Warnings
+
+
+
${report.suggestions.length}
+
Suggestions
+
+
+
${report.tests_performed}
+
Tests Performed
+
+
+
+
+
Firewall
+
${report.firewall_active ? "Active" : "Inactive"}
+
+
+
Malware Scanner
+
${report.malware_scanner ? "Installed" : "Not Found"}
+
+
+
Packages
+
${report.installed_packages || "N/A"}
+
+
+
Score Rating
+
${scoreLabel}
+
+
+
+ + +
+
4. Warnings (${report.warnings.length})
+

Issues that require immediate 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.severity ? `${w.severity}` : ""} +
+
${w.description}
+ ${w.solution ? `
Recommendation: ${w.solution}
` : ""} +
`).join("")} +
+ + +
+
5. Suggestions (${report.suggestions.length})
+

Recommended improvements to strengthen your system's security posture.

+ ${report.suggestions.length === 0 ? + '
No suggestions. System is fully hardened.
' : + report.suggestions.map((s, i) => ` +
+
+ #${i + 1} + ${s.test_id} +
+
${s.description}
+ ${s.solution ? `
Recommendation: ${s.solution}
` : ""} + ${s.details ? `
${s.details}
` : ""} +
`).join("")} +
+ + + + + +` + } + const loadSslStatus = async () => { try { setLoadingSsl(true) @@ -2520,7 +2873,7 @@ export function Security() { ) : (
- {/* Status */} + {/* Status bar */}
@@ -2536,33 +2889,300 @@ export function Security() {
- {/* Last Scan Info */} -
-
+ {/* Summary stats */} +
+

Last Scan

- {lynisInfo.last_scan || "No scan performed yet"} + {lynisInfo.last_scan ? lynisInfo.last_scan.replace("T", " ").substring(0, 16) : "Never"}

-
+

Hardening Index

-

= 70 ? "text-green-500" : lynisInfo.hardening_index >= 50 ? "text-yellow-500" : "text-red-500" }`}> - {lynisInfo.hardening_index !== null ? `${lynisInfo.hardening_index}/100` : "N/A"} + {lynisInfo.hardening_index !== null ? lynisInfo.hardening_index : "N/A"} +

+
+
+

Warnings

+

0 ? "text-red-500" : "text-green-500"}`}> + {lynisReport ? lynisReport.warnings.length : "N/A"} +

+
+
+

Suggestions

+

0 ? "text-yellow-500" : "text-green-500"}`}> + {lynisReport ? lynisReport.suggestions.length : "N/A"}

-
- -

- Run audits from the Proxmox terminal with: lynis audit system -

+ {/* Hardening bar */} + {lynisInfo.hardening_index !== null && ( +
+
+ Security Hardening Score + = 70 ? "text-green-500" : + lynisInfo.hardening_index >= 50 ? "text-yellow-500" : + "text-red-500" + }`}> + {lynisInfo.hardening_index}/100 + +
+
+
= 70 ? "bg-green-500" : + lynisInfo.hardening_index >= 50 ? "bg-yellow-500" : + "bg-red-500" + }`} + style={{ width: `${lynisInfo.hardening_index}%` }} + /> +
+
+ Critical (0-49) + Moderate (50-69) + Good (70-100) +
+
+ )} + + {/* Action buttons */} +
+ + {lynisReport && ( + + )}
+ + {/* Running indicator */} + {lynisAuditRunning && ( +
+
+
+
+

Security audit in progress...

+

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

+
+
+
+ )} + + {/* Report viewer */} + {lynisShowReport && lynisReport && ( +
+ {/* Report header */} +
+
+ + Audit Report + {lynisReport.datetime_start && ( + + - {lynisReport.datetime_start.replace("T", " ").substring(0, 16)} + + )} +
+ +
+ + {/* System info strip */} +
+
+

Hostname

+

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

+
+
+

OS

+

{lynisReport.os_name} {lynisReport.os_version}

+
+
+

Kernel

+

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

+
+
+

Tests

+

{lynisReport.tests_performed}

+
+
+ + {/* Report tabs */} +
+ {(["overview", "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

+ {[ + { label: "Firewall", ok: lynisReport.firewall_active }, + { label: "Malware Scanner", ok: lynisReport.malware_scanner }, + { label: "No Critical Warnings", ok: lynisReport.warnings.length === 0 }, + { label: "Hardening Score >= 70", ok: (lynisReport.hardening_index || 0) >= 70 }, + ].map((item) => ( +
+
+ {item.label} + + {item.ok ? "PASS" : "FAIL"} + +
+ ))} +
+
+ )} + + {/* 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.severity && ( + {w.severity} + )} +
+

{w.description}

+ {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.description}

+ {s.solution && ( +

+ Solution: {s.solution} +

+ )} + {s.details && ( +

{s.details}

+ )} +
+
+
+ ))} +
+ )} +
+ )} +
+ )}
)} diff --git a/AppImage/scripts/flask_security_routes.py b/AppImage/scripts/flask_security_routes.py index b0e6c805..0dcff8f1 100644 --- a/AppImage/scripts/flask_security_routes.py +++ b/AppImage/scripts/flask_security_routes.py @@ -212,6 +212,49 @@ def fail2ban_activity(): return jsonify({"success": False, "message": str(e)}), 500 +# ------------------------------------------------------------------- +# Lynis Audit +# ------------------------------------------------------------------- + +@security_bp.route('/api/security/lynis/run', methods=['POST']) +def lynis_run_audit(): + """Start a Lynis audit (runs in background)""" + if not security_manager: + return jsonify({"success": False, "message": "Security manager not available"}), 500 + try: + success, message = security_manager.run_lynis_audit() + return jsonify({"success": success, "message": message}) + except Exception as e: + return jsonify({"success": False, "message": str(e)}), 500 + + +@security_bp.route('/api/security/lynis/status', methods=['GET']) +def lynis_audit_status(): + """Get Lynis audit running status""" + if not security_manager: + return jsonify({"success": False, "message": "Security manager not available"}), 500 + try: + status = security_manager.get_lynis_audit_status() + return jsonify({"success": True, **status}) + except Exception as e: + return jsonify({"success": False, "message": str(e)}), 500 + + +@security_bp.route('/api/security/lynis/report', methods=['GET']) +def lynis_report(): + """Get parsed Lynis audit report""" + if not security_manager: + return jsonify({"success": False, "message": "Security manager not available"}), 500 + try: + report = security_manager.parse_lynis_report() + if report: + return jsonify({"success": True, "report": report}) + else: + return jsonify({"success": False, "message": "No report available. Run an audit first."}) + 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 5c271740..1c13d29b 100644 --- a/AppImage/scripts/security_manager.py +++ b/AppImage/scripts/security_manager.py @@ -1017,6 +1017,14 @@ def _detect_fail2ban(): return info +def _find_lynis_cmd(): + """Find the lynis binary path""" + for path in ["/usr/local/bin/lynis", "/opt/lynis/lynis", "/usr/bin/lynis"]: + if os.path.isfile(path) and os.access(path, os.X_OK): + return path + return None + + def _detect_lynis(): """Detect Lynis installation and status""" info = { @@ -1026,12 +1034,7 @@ def _detect_lynis(): "hardening_index": None, } - # Check both locations - lynis_cmd = None - for path in ["/usr/local/bin/lynis", "/opt/lynis/lynis", "/usr/bin/lynis"]: - if os.path.isfile(path) and os.access(path, os.X_OK): - lynis_cmd = path - break + lynis_cmd = _find_lynis_cmd() if lynis_cmd: info["installed"] = True @@ -1056,3 +1059,175 @@ def _detect_lynis(): pass return info + + +# Track running audit +_lynis_audit_running = False +_lynis_audit_progress = "" + + +def run_lynis_audit(): + """ + Run lynis audit system in the background. + Returns (success, message). + """ + global _lynis_audit_running, _lynis_audit_progress + + if _lynis_audit_running: + return False, "An audit is already running" + + lynis_cmd = _find_lynis_cmd() + if not lynis_cmd: + return False, "Lynis is not installed" + + _lynis_audit_running = True + _lynis_audit_progress = "starting" + + import threading + + def _run_audit(): + global _lynis_audit_running, _lynis_audit_progress + try: + _lynis_audit_progress = "running" + # Remove old report so lynis creates a fresh one + report_file = "/var/log/lynis-report.dat" + if os.path.isfile(report_file): + os.remove(report_file) + + rc, out, err = _run_cmd( + [lynis_cmd, "audit", "system", "--no-colors", "--quick"], + timeout=600 + ) + if rc == 0: + _lynis_audit_progress = "completed" + else: + _lynis_audit_progress = f"error: {err[:200] if err else 'unknown error'}" + except Exception as e: + _lynis_audit_progress = f"error: {str(e)}" + finally: + _lynis_audit_running = False + + t = threading.Thread(target=_run_audit, daemon=True) + t.start() + return True, "Audit started" + + +def get_lynis_audit_status(): + """Get current audit status""" + return { + "running": _lynis_audit_running, + "progress": _lynis_audit_progress, + } + + +def parse_lynis_report(): + """ + Parse /var/log/lynis-report.dat into structured report data. + Returns a dict with all audit findings. + """ + report_file = "/var/log/lynis-report.dat" + if not os.path.isfile(report_file): + return None + + report = { + "datetime_start": "", + "datetime_end": "", + "lynis_version": "", + "os_name": "", + "os_version": "", + "hostname": "", + "hardening_index": None, + "tests_performed": 0, + "warnings": [], + "suggestions": [], + "categories": {}, + "installed_packages": 0, + "kernel_version": "", + "firewall_active": False, + "malware_scanner": False, + } + + try: + with open(report_file, 'r') as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + + if "=" not in line: + continue + + key, _, value = line.partition("=") + key = key.strip() + value = value.strip() + + if key == "report_datetime_start": + report["datetime_start"] = value + elif key == "report_datetime_end": + report["datetime_end"] = value + elif key == "lynis_version": + report["lynis_version"] = value + elif key == "os_name": + report["os_name"] = value + elif key == "os_version": + report["os_version"] = value + elif key == "hostname": + report["hostname"] = value + elif key == "hardening_index": + try: + report["hardening_index"] = int(value) + except ValueError: + pass + elif key == "tests_performed": + try: + report["tests_performed"] = int(value) + except ValueError: + pass + elif key == "installed_packages": + try: + report["installed_packages"] = int(value) + except ValueError: + pass + elif key == "linux_kernel_version": + report["kernel_version"] = value + elif key == "firewall_active": + report["firewall_active"] = value == "1" + elif key == "malware_scanner_installed": + report["malware_scanner"] = value == "1" + + # Warnings: warning[]=TEST_ID|severity|description|solution + elif key == "warning[]": + parts = value.split("|") + if len(parts) >= 3: + report["warnings"].append({ + "test_id": parts[0].strip(), + "severity": parts[1].strip() if len(parts) > 1 else "", + "description": parts[2].strip() if len(parts) > 2 else "", + "solution": parts[3].strip() if len(parts) > 3 else "", + }) + + # Suggestions: suggestion[]=TEST_ID|description|solution|details + elif key == "suggestion[]": + parts = value.split("|") + if len(parts) >= 2: + report["suggestions"].append({ + "test_id": parts[0].strip(), + "description": parts[1].strip() if len(parts) > 1 else "", + "solution": parts[2].strip() if len(parts) > 2 else "", + "details": parts[3].strip() if len(parts) > 3 else "", + }) + + # Category results + elif key.endswith("_score"): + cat_name = key.replace("_score", "") + try: + if cat_name not in report["categories"]: + report["categories"][cat_name] = {} + report["categories"][cat_name]["score"] = int(value) + except ValueError: + pass + + except Exception: + return None + + return report