diff --git a/AppImage/components/security.tsx b/AppImage/components/security.tsx index cc19d646..de0d0e5e 100644 --- a/AppImage/components/security.tsx +++ b/AppImage/components/security.tsx @@ -101,8 +101,8 @@ export function Security() { 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 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 } @@ -118,6 +118,10 @@ export function Security() { 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) @@ -845,13 +849,17 @@ export function Security() { } const generatePrintableReport = (report: LynisReport) => { - const scoreColor = report.hardening_index === null ? "#888" - : report.hardening_index >= 70 ? "#16a34a" - : report.hardening_index >= 50 ? "#ca8a04" + 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 = report.hardening_index === null ? "N/A" - : report.hardening_index >= 70 ? "GOOD" - : report.hardening_index >= 50 ? "MODERATE" + 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` @@ -984,14 +992,14 @@ export function Security() {
1. Executive Summary
-
${report.hardening_index ?? "N/A"}
+
${displayScore ?? "N/A"}
${scoreLabel}
-

System Hardening Assessment

+

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

This automated security audit was performed on host ${report.hostname || "Unknown"} - running ${report.os_fullname || `${report.os_name} ${report.os_version}`.trim() || "Unknown OS"}. + running ${report.os_fullname || `${report.os_name} ${report.os_version}`.trim() || "Unknown OS"} (Proxmox VE). A total of ${report.tests_performed} tests were executed, resulting in ${report.warnings.length} warning(s) and ${report.suggestions.length} suggestion(s) for improvement. @@ -1036,16 +1044,16 @@ export function Security() {

3. Security Posture Overview
-
${report.hardening_index ?? "N/A"}/100
-
Hardening Score (${scoreLabel})
+
${displayScore ?? "N/A"}/100
+
Hardening Score - PVE Adjusted (${scoreLabel})${hasAdjustment ? `
Lynis raw: ${rawScore}/100` : ""}
-
${report.warnings.length}
-
Warnings
+
${report.warnings.length - (report.proxmox_expected_warnings ?? 0)}
+
Actionable Warnings${(report.proxmox_expected_warnings ?? 0) > 0 ? `
+${report.proxmox_expected_warnings} PVE expected` : ""}
-
${report.suggestions.length}
-
Suggestions
+
${report.suggestions.length - (report.proxmox_expected_suggestions ?? 0)}
+
Actionable Suggestions${(report.proxmox_expected_suggestions ?? 0) > 0 ? `
+${report.proxmox_expected_suggestions} PVE expected` : ""}
${report.tests_performed}
@@ -1075,13 +1083,16 @@ export function Security() { ${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.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("")}
@@ -1089,16 +1100,19 @@ export function Security() {
5. Suggestions (${report.suggestions.length})
-

Recommended improvements to strengthen your system's security posture.

+

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.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("")} @@ -2939,58 +2953,125 @@ ${(report.sections && report.sections.length > 0) ? `

Hardening Index

-

= 70 ? "text-green-500" : - (lynisReport?.hardening_index ?? lynisInfo.hardening_index ?? 0) >= 50 ? "text-yellow-500" : - "text-red-500" - }`}> - {(lynisReport?.hardening_index ?? lynisInfo.hardening_index) !== null - ? (lynisReport?.hardening_index ?? lynisInfo.hardening_index) - : "N/A"} -

+ {(() => { + 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

-

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

+ {(() => { + 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

-

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

+ {(() => { + 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 score = lynisReport?.hardening_index ?? lynisInfo.hardening_index - if (score === null || score === undefined) return null + 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 + + Security Hardening Score {hasAdjustment && (Proxmox Adjusted)} + = 70 ? "text-green-500" : score >= 50 ? "text-yellow-500" : "text-red-500" + displayScore >= 70 ? "text-green-500" : displayScore >= 50 ? "text-yellow-500" : "text-red-500" }`}> - {score}/100 + {displayScore}/100
-
-
= 70 ? "bg-green-500" : score >= 50 ? "bg-yellow-500" : "bg-red-500" - }`} - style={{ width: `${score}%` }} - /> -
+ {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 +

+ )}
) })()} @@ -3028,7 +3109,7 @@ ${(report.sections && report.sections.length > 0) ? ` : lynisInfo.last_scan?.replace("T", " ").substring(0, 16) || "Unknown date"}

- {lynisReport.hostname || "System"} - {lynisReport.tests_performed} tests - Score: {lynisReport.hardening_index ?? "N/A"}/100 - {lynisReport.warnings.length} warnings - {lynisReport.suggestions.length} suggestions + {lynisReport.hostname || "System"} - {lynisReport.tests_performed} tests - PVE Score: {lynisReport.proxmox_adjusted_score ?? lynisReport.hardening_index ?? "N/A"}/100 - {lynisReport.warnings.length - (lynisReport.proxmox_expected_warnings ?? 0)} warnings - {lynisReport.suggestions.length - (lynisReport.proxmox_expected_suggestions ?? 0)} suggestions

@@ -3146,7 +3227,10 @@ ${(report.sections && report.sections.length > 0) ? ` {/* 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, @@ -3162,17 +3246,17 @@ ${(report.sections && report.sections.length > 0) ? ` }, { label: "Warnings", - ok: lynisReport.warnings.length === 0, - passText: "None", - failText: `${lynisReport.warnings.length} found`, - isWarning: lynisReport.warnings.length > 0 && lynisReport.warnings.length <= 10, + 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", - ok: (lynisReport.hardening_index || 0) >= 70, - passText: `${lynisReport.hardening_index || 0}/100`, - failText: `${lynisReport.hardening_index || 0}/100 (< 70)`, - isWarning: (lynisReport.hardening_index || 0) >= 50, + 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" @@ -3184,7 +3268,8 @@ ${(report.sections && report.sections.length > 0) ? ` {item.ok ? item.passText : item.failText}
- )})} + )}) + })()}
)} @@ -3240,17 +3325,33 @@ ${(report.sections && report.sections.length > 0) ? ` ) : (
{lynisReport.warnings.map((w, idx) => ( -
+
-
+
-
- {w.test_id} - {w.severity && ( +
+ {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} @@ -3275,14 +3376,30 @@ ${(report.sections && report.sections.length > 0) ? ` ) : (

{lynisReport.suggestions.map((s, idx) => ( -
+
-
+
-
- {s.test_id} +
+ {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} diff --git a/AppImage/scripts/security_manager.py b/AppImage/scripts/security_manager.py index ec67fb52..b22cafbb 100644 --- a/AppImage/scripts/security_manager.py +++ b/AppImage/scripts/security_manager.py @@ -1673,4 +1673,185 @@ def parse_lynis_report(): except Exception: pass + # ── Proxmox Context: classify warnings/suggestions ──────────────── + # Proxmox VE is a hypervisor with specific requirements that cause + # Lynis to flag items that are normal/expected in this environment. + # We classify each finding and calculate an adjusted score. + + PVE_WARNING_CONTEXT = { + "FIRE-4512": { + "reason": "Proxmox uses pve-firewall which manages iptables/nftables rules dynamically. Direct iptables rules are not used.", + "expected": True, + }, + "NETW-3015": { + "reason": "Network bridges (vmbr*) operate in promiscuous mode by design to forward traffic between VMs/containers.", + "expected": True, + }, + "MAIL-8818": { + "reason": "Postfix is used by Proxmox for system notifications. The SMTP banner can be customized but is low risk on an internal server.", + "expected": False, + "severity_override": "low", + }, + "NETW-2705": { + "reason": "Single DNS server is common in home/lab environments. Add a secondary DNS in /etc/resolv.conf if possible.", + "expected": False, + "severity_override": "low", + }, + "PKGS-7392": { + "reason": "Package updates should be applied regularly. Check if the 'vulnerable' packages are Proxmox-specific packages pending a PVE update.", + "expected": False, + }, + } + + PVE_SUGGESTION_CONTEXT = { + "BOOT-5122": { + "reason": "GRUB password is recommended for physical servers but less critical for headless/remote Proxmox nodes.", + "expected": False, + "severity_override": "low", + }, + "BOOT-5264": { + "reason": "Many Proxmox core services (pve-*, corosync, spiceproxy, etc.) run without systemd hardening. This is by design as they need broad system access.", + "expected": True, + }, + "KRNL-5788": { + "reason": "Proxmox uses its own kernel (pve-kernel) which may not place vmlinuz in the standard location.", + "expected": True, + }, + "AUTH-9282": { + "reason": "Proxmox system accounts (www-data, backup, etc.) don't use password expiry. Only applies to interactive user accounts.", + "expected": True, + }, + "AUTH-9284": { + "reason": "Locked system accounts are normal in Proxmox (daemon, nobody, etc.).", + "expected": True, + }, + "USB-1000": { + "reason": "USB passthrough to VMs may require USB drivers. Disable only if USB passthrough is not needed.", + "expected": False, + "severity_override": "low", + }, + "STRG-1846": { + "reason": "FireWire is typically not used in modern servers. Safe to disable.", + "expected": False, + "severity_override": "low", + }, + "NETW-3200": { + "reason": "Protocols dccp, sctp, rds, tipc are typically not needed. Can be disabled via modprobe blacklist.", + "expected": False, + }, + "SSH-7408": { + "reason": "SSH hardening is recommended but PermitRootLogin is required for Proxmox API/CLI management. Other SSH settings can be tuned.", + "expected": False, + }, + "FILE-6310": { + "reason": "Separate partitions for /home and /var are best practice but Proxmox typically uses a simple partition layout with LVM-thin for VM storage.", + "expected": True, + }, + "KRNL-6000": { + "reason": "Some sysctl values differ because Proxmox needs IP forwarding, bridge-nf-call, and relaxed kernel settings for VM/container networking.", + "expected": True, + }, + "HRDN-7230": { + "reason": "A malware scanner (rkhunter, ClamAV) is recommended but optional for a dedicated hypervisor.", + "expected": False, + "severity_override": "low", + }, + "HRDN-7222": { + "reason": "Restricting compiler access is good practice on production servers. Less critical on a hypervisor where only root has access.", + "expected": False, + "severity_override": "low", + }, + "FINT-4350": { + "reason": "File integrity monitoring (AIDE, Tripwire) is recommended for production but optional for home/lab environments.", + "expected": False, + "severity_override": "low", + }, + "ACCT-9622": { + "reason": "Process accounting is useful for forensics but not required for a hypervisor.", + "expected": False, + "severity_override": "low", + }, + "ACCT-9626": { + "reason": "Sysstat is useful for performance monitoring but not a security requirement.", + "expected": False, + "severity_override": "low", + }, + "ACCT-9628": { + "reason": "Auditd provides detailed audit logging. Recommended for production, optional for home/lab.", + "expected": False, + "severity_override": "low", + }, + "TOOL-5002": { + "reason": "Automation tools (Ansible, Puppet) are useful for multi-node clusters but not required for single-node setups.", + "expected": True, + }, + "LOGG-2154": { + "reason": "External logging is recommended for production but optional for single-node environments.", + "expected": False, + "severity_override": "low", + }, + "BANN-7126": { + "reason": "Legal banners in /etc/issue are recommended for compliance but not a security risk.", + "expected": False, + "severity_override": "low", + }, + "BANN-7130": { + "reason": "Legal banners in /etc/issue.net are recommended for compliance but not a security risk.", + "expected": False, + "severity_override": "low", + }, + } + + # Apply Proxmox context to warnings + pve_expected_warnings = 0 + for w in report["warnings"]: + tid = w.get("test_id", "") + ctx = PVE_WARNING_CONTEXT.get(tid) + if ctx: + w["proxmox_context"] = ctx["reason"] + w["proxmox_expected"] = ctx.get("expected", False) + if ctx.get("severity_override"): + w["proxmox_severity"] = ctx["severity_override"] + if ctx.get("expected", False): + pve_expected_warnings += 1 + else: + w["proxmox_context"] = "" + w["proxmox_expected"] = False + + # Apply Proxmox context to suggestions + pve_expected_suggestions = 0 + for s in report["suggestions"]: + tid = s.get("test_id", "") + ctx = PVE_SUGGESTION_CONTEXT.get(tid) + if ctx: + s["proxmox_context"] = ctx["reason"] + s["proxmox_expected"] = ctx.get("expected", False) + if ctx.get("severity_override"): + s["proxmox_severity"] = ctx["severity_override"] + if ctx.get("expected", False): + pve_expected_suggestions += 1 + else: + s["proxmox_context"] = "" + s["proxmox_expected"] = False + + # Calculate Proxmox-adjusted score + # Lynis score is based on total tests and findings. + # We boost the score proportionally to the expected items. + raw_score = report["hardening_index"] or 0 + total_findings = len(report["warnings"]) + len(report["suggestions"]) + expected_findings = pve_expected_warnings + pve_expected_suggestions + if total_findings > 0 and raw_score > 0: + # Each finding roughly reduces the score. Expected findings should + # not penalize. We estimate the boost proportionally. + penalty_per_finding = (100 - raw_score) / max(total_findings, 1) + boost = int(penalty_per_finding * expected_findings * 0.8) + adjusted_score = min(100, raw_score + boost) + else: + adjusted_score = raw_score + + report["proxmox_adjusted_score"] = adjusted_score + report["proxmox_expected_warnings"] = pve_expected_warnings + report["proxmox_expected_suggestions"] = pve_expected_suggestions + report["proxmox_context_applied"] = True + return report