Update Lynis

This commit is contained in:
MacRimi
2026-02-08 20:00:23 +01:00
parent 42626f3bce
commit cc0f401855
2 changed files with 371 additions and 73 deletions

View File

@@ -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<LynisReport | null>(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() {
<div class="section-title">1. Executive Summary</div>
<div class="score-section">
<div class="score-circle" style="border-color: ${scoreColor}; color: ${scoreColor};">
<div class="score-number">${report.hardening_index ?? "N/A"}</div>
<div class="score-number">${displayScore ?? "N/A"}</div>
<div class="score-label">${scoreLabel}</div>
</div>
<div class="score-details">
<h3>System Hardening Assessment</h3>
<h3>System Hardening Assessment${hasAdjustment ? " (Proxmox Adjusted)" : ""}</h3>
<p>
This automated security audit was performed on host <strong>${report.hostname || "Unknown"}</strong>
running <strong>${report.os_fullname || `${report.os_name} ${report.os_version}`.trim() || "Unknown OS"}</strong>.
running <strong>${report.os_fullname || `${report.os_name} ${report.os_version}`.trim() || "Unknown OS"}</strong> (Proxmox VE).
A total of <strong>${report.tests_performed}</strong> tests were executed,
resulting in <strong style="color:#dc2626;">${report.warnings.length} warning(s)</strong>
and <strong style="color:#ca8a04;">${report.suggestions.length} suggestion(s)</strong> for improvement.
@@ -1036,16 +1044,16 @@ export function Security() {
<div class="section-title">3. Security Posture Overview</div>
<div class="status-grid">
<div class="status-card">
<div class="status-value" style="color:${scoreColor};">${report.hardening_index ?? "N/A"}<span style="font-size:12px;color:#64748b;">/100</span></div>
<div class="status-label">Hardening Score (${scoreLabel})</div>
<div class="status-value" style="color:${scoreColor};">${displayScore ?? "N/A"}<span style="font-size:12px;color:#64748b;">/100</span></div>
<div class="status-label">Hardening Score - PVE Adjusted (${scoreLabel})${hasAdjustment ? `<br><span style="font-size:10px;color:#64748b;">Lynis raw: ${rawScore}/100</span>` : ""}</div>
</div>
<div class="status-card">
<div class="status-value" style="color:${report.warnings.length > 0 ? "#dc2626" : "#16a34a"};">${report.warnings.length}</div>
<div class="status-label">Warnings</div>
<div class="status-value" style="color:${(report.warnings.length - (report.proxmox_expected_warnings ?? 0)) > 0 ? "#dc2626" : "#16a34a"};">${report.warnings.length - (report.proxmox_expected_warnings ?? 0)}</div>
<div class="status-label">Actionable Warnings${(report.proxmox_expected_warnings ?? 0) > 0 ? `<br><span style="font-size:10px;color:#22d3ee;">+${report.proxmox_expected_warnings} PVE expected</span>` : ""}</div>
</div>
<div class="status-card">
<div class="status-value" style="color:${report.suggestions.length > 0 ? "#ca8a04" : "#16a34a"};">${report.suggestions.length}</div>
<div class="status-label">Suggestions</div>
<div class="status-value" style="color:${(report.suggestions.length - (report.proxmox_expected_suggestions ?? 0)) > 0 ? "#ca8a04" : "#16a34a"};">${report.suggestions.length - (report.proxmox_expected_suggestions ?? 0)}</div>
<div class="status-label">Actionable Suggestions${(report.proxmox_expected_suggestions ?? 0) > 0 ? `<br><span style="font-size:10px;color:#22d3ee;">+${report.proxmox_expected_suggestions} PVE expected</span>` : ""}</div>
</div>
<div class="status-card">
<div class="status-value">${report.tests_performed}</div>
@@ -1075,13 +1083,16 @@ export function Security() {
${report.warnings.length === 0 ?
'<div style="padding:16px;text-align:center;color:#16a34a;background:#f0fdf4;border-radius:6px;border:1px solid #bbf7d0;">No warnings detected. System appears to be well-configured.</div>' :
report.warnings.map((w, i) => `
<div class="finding finding-warning">
<div class="finding finding-warning" style="${w.proxmox_expected ? 'opacity:0.7;border-left-color:#22d3ee;' : ''}">
<div class="finding-header">
<span style="font-size:10px;color:#94a3b8;font-weight:700;">#${i + 1}</span>
<span class="finding-id">${w.test_id}</span>
${w.severity ? `<span class="finding-severity">${w.severity}</span>` : ""}
<span class="finding-id" ${w.proxmox_expected ? 'style="background:#083344;color:#22d3ee;"' : ''}>${w.test_id}</span>
${w.proxmox_expected ? '<span style="font-size:9px;padding:2px 6px;border-radius:4px;background:#083344;color:#22d3ee;">PVE Expected</span>' : ''}
${!w.proxmox_expected && w.proxmox_severity === "low" ? '<span style="font-size:9px;padding:2px 6px;border-radius:4px;background:#fefce8;color:#ca8a04;">Low Risk</span>' : ''}
${!w.proxmox_expected && !w.proxmox_severity && w.severity ? `<span class="finding-severity">${w.severity}</span>` : ""}
</div>
<div class="finding-desc">${w.description}</div>
${w.proxmox_context ? `<div style="font-size:10px;color:#22d3ee;margin-top:4px;"><strong>Proxmox:</strong> ${w.proxmox_context}</div>` : ""}
${w.solution ? `<div class="finding-solution"><strong>Recommendation:</strong> ${w.solution}</div>` : ""}
</div>`).join("")}
</div>
@@ -1089,16 +1100,19 @@ export function Security() {
<!-- Suggestions -->
<div class="section page-break">
<div class="section-title">5. Suggestions (${report.suggestions.length})</div>
<p style="font-size:11px;color:#64748b;margin-bottom:12px;">Recommended improvements to strengthen your system's security posture.</p>
<p style="font-size:11px;color:#64748b;margin-bottom:12px;">Recommended improvements to strengthen your system's security posture.${(report.proxmox_expected_suggestions ?? 0) > 0 ? ` <span style="color:#22d3ee;">${report.proxmox_expected_suggestions} items are expected behavior in Proxmox VE.</span>` : ""}</p>
${report.suggestions.length === 0 ?
'<div style="padding:16px;text-align:center;color:#16a34a;background:#f0fdf4;border-radius:6px;border:1px solid #bbf7d0;">No suggestions. System is fully hardened.</div>' :
report.suggestions.map((s, i) => `
<div class="finding finding-suggestion">
<div class="finding finding-suggestion" style="${s.proxmox_expected ? 'opacity:0.7;border-left-color:#22d3ee;' : ''}">
<div class="finding-header">
<span style="font-size:10px;color:#94a3b8;font-weight:700;">#${i + 1}</span>
<span class="finding-id">${s.test_id}</span>
<span class="finding-id" ${s.proxmox_expected ? 'style="background:#083344;color:#22d3ee;"' : ''}>${s.test_id}</span>
${s.proxmox_expected ? '<span style="font-size:9px;padding:2px 6px;border-radius:4px;background:#083344;color:#22d3ee;">PVE Expected</span>' : ''}
${!s.proxmox_expected && s.proxmox_severity === "low" ? '<span style="font-size:9px;padding:2px 6px;border-radius:4px;background:#f8fafc;color:#64748b;">Low Priority</span>' : ''}
</div>
<div class="finding-desc">${s.description}</div>
${s.proxmox_context ? `<div style="font-size:10px;color:#22d3ee;margin-top:4px;"><strong>Proxmox:</strong> ${s.proxmox_context}</div>` : ""}
${s.solution ? `<div class="finding-solution"><strong>Recommendation:</strong> ${s.solution}</div>` : ""}
${s.details ? `<div class="finding-details">${s.details}</div>` : ""}
</div>`).join("")}
@@ -2939,58 +2953,125 @@ ${(report.sections && report.sections.length > 0) ? `
</div>
<div className="p-3 bg-muted/30 rounded-lg border border-border text-center">
<p className="text-xs text-muted-foreground mb-1">Hardening Index</p>
<p className={`text-xl font-bold ${
(lynisReport?.hardening_index ?? lynisInfo.hardening_index) === null ? "text-muted-foreground" :
(lynisReport?.hardening_index ?? lynisInfo.hardening_index ?? 0) >= 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"}
</p>
{(() => {
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 (
<div>
<p className={`text-xl font-bold ${scoreColorClass}`}>
{displayScore !== null && displayScore !== undefined ? displayScore : "N/A"}
</p>
{adjScore != null && rawScore != null && adjScore !== rawScore && (
<p className="text-[10px] text-muted-foreground mt-0.5">
Lynis: {rawScore} | PVE: {adjScore}
</p>
)}
</div>
)
})()}
</div>
<div className="p-3 bg-muted/30 rounded-lg border border-border text-center">
<p className="text-xs text-muted-foreground mb-1">Warnings</p>
<p className={`text-xl font-bold ${lynisReport && lynisReport.warnings.length > 0 ? "text-red-500" : "text-green-500"}`}>
{lynisReport ? lynisReport.warnings.length : "-"}
</p>
{(() => {
if (!lynisReport) return <p className="text-xl font-bold text-muted-foreground">-</p>
const total = lynisReport.warnings.length
const expected = lynisReport.proxmox_expected_warnings ?? 0
const real = total - expected
return (
<div>
<p className={`text-xl font-bold ${real > 0 ? "text-red-500" : total > 0 ? "text-yellow-500" : "text-green-500"}`}>
{real > 0 ? real : total}
</p>
{expected > 0 && (
<p className="text-[10px] text-muted-foreground mt-0.5">
+{expected} PVE expected
</p>
)}
</div>
)
})()}
</div>
<div className="p-3 bg-muted/30 rounded-lg border border-border text-center">
<p className="text-xs text-muted-foreground mb-1">Suggestions</p>
<p className={`text-xl font-bold ${lynisReport && lynisReport.suggestions.length > 0 ? "text-yellow-500" : "text-green-500"}`}>
{lynisReport ? lynisReport.suggestions.length : "-"}
</p>
{(() => {
if (!lynisReport) return <p className="text-xl font-bold text-muted-foreground">-</p>
const total = lynisReport.suggestions.length
const expected = lynisReport.proxmox_expected_suggestions ?? 0
const real = total - expected
return (
<div>
<p className={`text-xl font-bold ${real > 0 ? "text-yellow-500" : "text-green-500"}`}>
{real > 0 ? real : total}
</p>
{expected > 0 && (
<p className="text-[10px] text-muted-foreground mt-0.5">
+{expected} PVE expected
</p>
)}
</div>
)
})()}
</div>
</div>
{/* 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 (
<div className="space-y-1.5">
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">Security Hardening Score</span>
<span className="text-muted-foreground">
Security Hardening Score {hasAdjustment && <span className="text-cyan-400/70">(Proxmox Adjusted)</span>}
</span>
<span className={`font-bold ${
score >= 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
</span>
</div>
<div className="w-full h-3 bg-muted/50 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-1000 ${
score >= 70 ? "bg-green-500" : score >= 50 ? "bg-yellow-500" : "bg-red-500"
}`}
style={{ width: `${score}%` }}
/>
</div>
{hasAdjustment ? (
<div className="relative w-full h-3 bg-muted/50 rounded-full overflow-hidden">
{/* Raw score bar (dimmed) */}
<div
className="absolute inset-y-0 left-0 rounded-full bg-yellow-500/30"
style={{ width: `${rawScore}%` }}
/>
{/* Adjusted score bar */}
<div
className={`absolute inset-y-0 left-0 rounded-full transition-all duration-1000 ${
displayScore >= 70 ? "bg-green-500" : displayScore >= 50 ? "bg-yellow-500" : "bg-red-500"
}`}
style={{ width: `${displayScore}%` }}
/>
</div>
) : (
<div className="w-full h-3 bg-muted/50 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-1000 ${
displayScore >= 70 ? "bg-green-500" : displayScore >= 50 ? "bg-yellow-500" : "bg-red-500"
}`}
style={{ width: `${displayScore}%` }}
/>
</div>
)}
<div className="flex justify-between text-[10px] text-muted-foreground">
<span>Critical (0-49)</span>
<span>Moderate (50-69)</span>
<span>Good (70-100)</span>
</div>
{hasAdjustment && (
<p className="text-[10px] text-cyan-400/70 text-center">
Lynis raw score: {rawScore}/100 | {(lynisReport?.proxmox_expected_warnings ?? 0) + (lynisReport?.proxmox_expected_suggestions ?? 0)} findings are expected in Proxmox VE
</p>
)}
</div>
)
})()}
@@ -3028,7 +3109,7 @@ ${(report.sections && report.sections.length > 0) ? `
: lynisInfo.last_scan?.replace("T", " ").substring(0, 16) || "Unknown date"}
</p>
<p className="text-[11px] text-muted-foreground">
{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
</p>
</div>
</div>
@@ -3146,7 +3227,10 @@ ${(report.sections && report.sections.length > 0) ? `
{/* Security checklist */}
<div className="space-y-1.5">
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Quick Status</p>
{[
{(() => {
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}
</span>
</div>
)})}
)})
})()}
</div>
</div>
)}
@@ -3240,17 +3325,33 @@ ${(report.sections && report.sections.length > 0) ? `
) : (
<div className="divide-y divide-border">
{lynisReport.warnings.map((w, idx) => (
<div key={idx} className="p-3 hover:bg-muted/20 transition-colors">
<div key={idx} className={`p-3 hover:bg-muted/20 transition-colors ${w.proxmox_expected ? "opacity-60" : ""}`}>
<div className="flex items-start gap-2">
<div className="w-2 h-2 rounded-full bg-red-500 flex-shrink-0 mt-1.5" />
<div className={`w-2 h-2 rounded-full flex-shrink-0 mt-1.5 ${
w.proxmox_expected ? "bg-cyan-500" :
w.proxmox_severity === "low" ? "bg-yellow-500" : "bg-red-500"
}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<code className="text-[10px] px-1.5 py-0.5 rounded bg-red-500/10 text-red-500 font-mono">{w.test_id}</code>
{w.severity && (
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
<code className={`text-[10px] px-1.5 py-0.5 rounded font-mono ${
w.proxmox_expected ? "bg-cyan-500/10 text-cyan-400" : "bg-red-500/10 text-red-500"
}`}>{w.test_id}</code>
{w.proxmox_expected && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-cyan-500/10 text-cyan-400">PVE Expected</span>
)}
{!w.proxmox_expected && w.proxmox_severity === "low" && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-yellow-500/10 text-yellow-500">Low Risk</span>
)}
{!w.proxmox_expected && !w.proxmox_severity && w.severity && (
<span className="text-[10px] text-red-400">{w.severity}</span>
)}
</div>
<p className="text-sm text-foreground">{w.description}</p>
{w.proxmox_context && (
<p className="text-xs text-cyan-400/70 mt-1 flex items-start gap-1">
<span className="shrink-0">Proxmox:</span> {w.proxmox_context}
</p>
)}
{w.solution && (
<p className="text-xs text-muted-foreground mt-1">
Solution: {w.solution}
@@ -3275,14 +3376,30 @@ ${(report.sections && report.sections.length > 0) ? `
) : (
<div className="divide-y divide-border">
{lynisReport.suggestions.map((s, idx) => (
<div key={idx} className="p-3 hover:bg-muted/20 transition-colors">
<div key={idx} className={`p-3 hover:bg-muted/20 transition-colors ${s.proxmox_expected ? "opacity-60" : ""}`}>
<div className="flex items-start gap-2">
<div className="w-2 h-2 rounded-full bg-yellow-500 flex-shrink-0 mt-1.5" />
<div className={`w-2 h-2 rounded-full flex-shrink-0 mt-1.5 ${
s.proxmox_expected ? "bg-cyan-500" :
s.proxmox_severity === "low" ? "bg-muted-foreground" : "bg-yellow-500"
}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<code className="text-[10px] px-1.5 py-0.5 rounded bg-yellow-500/10 text-yellow-500 font-mono">{s.test_id}</code>
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
<code className={`text-[10px] px-1.5 py-0.5 rounded font-mono ${
s.proxmox_expected ? "bg-cyan-500/10 text-cyan-400" : "bg-yellow-500/10 text-yellow-500"
}`}>{s.test_id}</code>
{s.proxmox_expected && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-cyan-500/10 text-cyan-400">PVE Expected</span>
)}
{!s.proxmox_expected && s.proxmox_severity === "low" && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground">Low Priority</span>
)}
</div>
<p className="text-sm text-foreground">{s.description}</p>
{s.proxmox_context && (
<p className="text-xs text-cyan-400/70 mt-1 flex items-start gap-1">
<span className="shrink-0">Proxmox:</span> {s.proxmox_context}
</p>
)}
{s.solution && (
<p className="text-xs text-muted-foreground mt-1">
Solution: {s.solution}

View File

@@ -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