mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-18 16:36:27 +00:00
Update Lynis
This commit is contained in:
@@ -101,8 +101,8 @@ export function Security() {
|
|||||||
const [showLynisInstaller, setShowLynisInstaller] = useState(false)
|
const [showLynisInstaller, setShowLynisInstaller] = useState(false)
|
||||||
|
|
||||||
// Lynis audit state
|
// Lynis audit state
|
||||||
interface LynisWarning { test_id: string; severity: string; description: string; solution: 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 }
|
interface LynisSuggestion { test_id: string; description: string; solution: string; details: string; proxmox_context?: string; proxmox_expected?: boolean; proxmox_severity?: string }
|
||||||
interface LynisCheck {
|
interface LynisCheck {
|
||||||
name: string; status: string; detail?: string
|
name: string; status: string; detail?: string
|
||||||
}
|
}
|
||||||
@@ -118,6 +118,10 @@ export function Security() {
|
|||||||
installed_packages: number; kernel_version: string
|
installed_packages: number; kernel_version: string
|
||||||
firewall_active: boolean; malware_scanner: boolean
|
firewall_active: boolean; malware_scanner: boolean
|
||||||
sections: LynisSection[]
|
sections: LynisSection[]
|
||||||
|
proxmox_adjusted_score?: number
|
||||||
|
proxmox_expected_warnings?: number
|
||||||
|
proxmox_expected_suggestions?: number
|
||||||
|
proxmox_context_applied?: boolean
|
||||||
}
|
}
|
||||||
const [lynisAuditRunning, setLynisAuditRunning] = useState(false)
|
const [lynisAuditRunning, setLynisAuditRunning] = useState(false)
|
||||||
const [lynisReport, setLynisReport] = useState<LynisReport | null>(null)
|
const [lynisReport, setLynisReport] = useState<LynisReport | null>(null)
|
||||||
@@ -845,13 +849,17 @@ export function Security() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const generatePrintableReport = (report: LynisReport) => {
|
const generatePrintableReport = (report: LynisReport) => {
|
||||||
const scoreColor = report.hardening_index === null ? "#888"
|
const adjScore = report.proxmox_adjusted_score ?? report.hardening_index
|
||||||
: report.hardening_index >= 70 ? "#16a34a"
|
const rawScore = report.hardening_index
|
||||||
: report.hardening_index >= 50 ? "#ca8a04"
|
const displayScore = adjScore ?? rawScore
|
||||||
|
const hasAdjustment = adjScore != null && rawScore != null && adjScore !== rawScore
|
||||||
|
const scoreColor = displayScore === null ? "#888"
|
||||||
|
: displayScore >= 70 ? "#16a34a"
|
||||||
|
: displayScore >= 50 ? "#ca8a04"
|
||||||
: "#dc2626"
|
: "#dc2626"
|
||||||
const scoreLabel = report.hardening_index === null ? "N/A"
|
const scoreLabel = displayScore === null ? "N/A"
|
||||||
: report.hardening_index >= 70 ? "GOOD"
|
: displayScore >= 70 ? "GOOD"
|
||||||
: report.hardening_index >= 50 ? "MODERATE"
|
: displayScore >= 50 ? "MODERATE"
|
||||||
: "CRITICAL"
|
: "CRITICAL"
|
||||||
const now = new Date().toLocaleString()
|
const now = new Date().toLocaleString()
|
||||||
const logoUrl = `${window.location.origin}/images/proxmenux-logo.png`
|
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="section-title">1. Executive Summary</div>
|
||||||
<div class="score-section">
|
<div class="score-section">
|
||||||
<div class="score-circle" style="border-color: ${scoreColor}; color: ${scoreColor};">
|
<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 class="score-label">${scoreLabel}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="score-details">
|
<div class="score-details">
|
||||||
<h3>System Hardening Assessment</h3>
|
<h3>System Hardening Assessment${hasAdjustment ? " (Proxmox Adjusted)" : ""}</h3>
|
||||||
<p>
|
<p>
|
||||||
This automated security audit was performed on host <strong>${report.hostname || "Unknown"}</strong>
|
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,
|
A total of <strong>${report.tests_performed}</strong> tests were executed,
|
||||||
resulting in <strong style="color:#dc2626;">${report.warnings.length} warning(s)</strong>
|
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.
|
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="section-title">3. Security Posture Overview</div>
|
||||||
<div class="status-grid">
|
<div class="status-grid">
|
||||||
<div class="status-card">
|
<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-value" style="color:${scoreColor};">${displayScore ?? "N/A"}<span style="font-size:12px;color:#64748b;">/100</span></div>
|
||||||
<div class="status-label">Hardening Score (${scoreLabel})</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>
|
||||||
<div class="status-card">
|
<div class="status-card">
|
||||||
<div class="status-value" style="color:${report.warnings.length > 0 ? "#dc2626" : "#16a34a"};">${report.warnings.length}</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">Warnings</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>
|
||||||
<div class="status-card">
|
<div class="status-card">
|
||||||
<div class="status-value" style="color:${report.suggestions.length > 0 ? "#ca8a04" : "#16a34a"};">${report.suggestions.length}</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">Suggestions</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>
|
||||||
<div class="status-card">
|
<div class="status-card">
|
||||||
<div class="status-value">${report.tests_performed}</div>
|
<div class="status-value">${report.tests_performed}</div>
|
||||||
@@ -1075,13 +1083,16 @@ export function Security() {
|
|||||||
${report.warnings.length === 0 ?
|
${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>' :
|
'<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) => `
|
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">
|
<div class="finding-header">
|
||||||
<span style="font-size:10px;color:#94a3b8;font-weight:700;">#${i + 1}</span>
|
<span style="font-size:10px;color:#94a3b8;font-weight:700;">#${i + 1}</span>
|
||||||
<span class="finding-id">${w.test_id}</span>
|
<span class="finding-id" ${w.proxmox_expected ? 'style="background:#083344;color:#22d3ee;"' : ''}>${w.test_id}</span>
|
||||||
${w.severity ? `<span class="finding-severity">${w.severity}</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>
|
||||||
<div class="finding-desc">${w.description}</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>` : ""}
|
${w.solution ? `<div class="finding-solution"><strong>Recommendation:</strong> ${w.solution}</div>` : ""}
|
||||||
</div>`).join("")}
|
</div>`).join("")}
|
||||||
</div>
|
</div>
|
||||||
@@ -1089,16 +1100,19 @@ export function Security() {
|
|||||||
<!-- Suggestions -->
|
<!-- Suggestions -->
|
||||||
<div class="section page-break">
|
<div class="section page-break">
|
||||||
<div class="section-title">5. Suggestions (${report.suggestions.length})</div>
|
<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 ?
|
${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>' :
|
'<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) => `
|
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">
|
<div class="finding-header">
|
||||||
<span style="font-size:10px;color:#94a3b8;font-weight:700;">#${i + 1}</span>
|
<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>
|
||||||
<div class="finding-desc">${s.description}</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.solution ? `<div class="finding-solution"><strong>Recommendation:</strong> ${s.solution}</div>` : ""}
|
||||||
${s.details ? `<div class="finding-details">${s.details}</div>` : ""}
|
${s.details ? `<div class="finding-details">${s.details}</div>` : ""}
|
||||||
</div>`).join("")}
|
</div>`).join("")}
|
||||||
@@ -2939,58 +2953,125 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-muted/30 rounded-lg border border-border text-center">
|
<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-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" :
|
const rawScore = lynisReport?.hardening_index ?? lynisInfo.hardening_index
|
||||||
(lynisReport?.hardening_index ?? lynisInfo.hardening_index ?? 0) >= 70 ? "text-green-500" :
|
const adjScore = lynisReport?.proxmox_adjusted_score
|
||||||
(lynisReport?.hardening_index ?? lynisInfo.hardening_index ?? 0) >= 50 ? "text-yellow-500" :
|
const displayScore = adjScore ?? rawScore
|
||||||
"text-red-500"
|
const scoreColorClass = displayScore === null || displayScore === undefined ? "text-muted-foreground" :
|
||||||
}`}>
|
displayScore >= 70 ? "text-green-500" :
|
||||||
{(lynisReport?.hardening_index ?? lynisInfo.hardening_index) !== null
|
displayScore >= 50 ? "text-yellow-500" : "text-red-500"
|
||||||
? (lynisReport?.hardening_index ?? lynisInfo.hardening_index)
|
return (
|
||||||
: "N/A"}
|
<div>
|
||||||
</p>
|
<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>
|
||||||
<div className="p-3 bg-muted/30 rounded-lg border border-border text-center">
|
<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-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 : "-"}
|
if (!lynisReport) return <p className="text-xl font-bold text-muted-foreground">-</p>
|
||||||
</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>
|
||||||
<div className="p-3 bg-muted/30 rounded-lg border border-border text-center">
|
<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-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 : "-"}
|
if (!lynisReport) return <p className="text-xl font-bold text-muted-foreground">-</p>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hardening bar */}
|
{/* Hardening bar */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const score = lynisReport?.hardening_index ?? lynisInfo.hardening_index
|
const rawScore = lynisReport?.hardening_index ?? lynisInfo.hardening_index
|
||||||
if (score === null || score === undefined) return null
|
const adjScore = lynisReport?.proxmox_adjusted_score
|
||||||
|
if (rawScore === null || rawScore === undefined) return null
|
||||||
|
const displayScore = adjScore ?? rawScore
|
||||||
|
const hasAdjustment = adjScore != null && adjScore !== rawScore
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center justify-between text-xs">
|
<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 ${
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-3 bg-muted/50 rounded-full overflow-hidden">
|
{hasAdjustment ? (
|
||||||
<div
|
<div className="relative w-full h-3 bg-muted/50 rounded-full overflow-hidden">
|
||||||
className={`h-full rounded-full transition-all duration-1000 ${
|
{/* Raw score bar (dimmed) */}
|
||||||
score >= 70 ? "bg-green-500" : score >= 50 ? "bg-yellow-500" : "bg-red-500"
|
<div
|
||||||
}`}
|
className="absolute inset-y-0 left-0 rounded-full bg-yellow-500/30"
|
||||||
style={{ width: `${score}%` }}
|
style={{ width: `${rawScore}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
{/* 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">
|
<div className="flex justify-between text-[10px] text-muted-foreground">
|
||||||
<span>Critical (0-49)</span>
|
<span>Critical (0-49)</span>
|
||||||
<span>Moderate (50-69)</span>
|
<span>Moderate (50-69)</span>
|
||||||
<span>Good (70-100)</span>
|
<span>Good (70-100)</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
@@ -3028,7 +3109,7 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
: lynisInfo.last_scan?.replace("T", " ").substring(0, 16) || "Unknown date"}
|
: lynisInfo.last_scan?.replace("T", " ").substring(0, 16) || "Unknown date"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3146,7 +3227,10 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
{/* Security checklist */}
|
{/* Security checklist */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Quick Status</p>
|
<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",
|
label: "Firewall",
|
||||||
ok: lynisReport.firewall_active,
|
ok: lynisReport.firewall_active,
|
||||||
@@ -3162,17 +3246,17 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Warnings",
|
label: "Warnings",
|
||||||
ok: lynisReport.warnings.length === 0,
|
ok: realWarnings <= 0,
|
||||||
passText: "None",
|
passText: lynisReport.warnings.length === 0 ? "None" : `${lynisReport.warnings.length} (all PVE expected)`,
|
||||||
failText: `${lynisReport.warnings.length} found`,
|
failText: `${realWarnings} actionable` + (lynisReport.proxmox_expected_warnings ? ` + ${lynisReport.proxmox_expected_warnings} PVE` : ""),
|
||||||
isWarning: lynisReport.warnings.length > 0 && lynisReport.warnings.length <= 10,
|
isWarning: realWarnings > 0 && realWarnings <= 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Hardening Score",
|
label: "Hardening Score (PVE)",
|
||||||
ok: (lynisReport.hardening_index || 0) >= 70,
|
ok: adjScore >= 70,
|
||||||
passText: `${lynisReport.hardening_index || 0}/100`,
|
passText: `${adjScore}/100`,
|
||||||
failText: `${lynisReport.hardening_index || 0}/100 (< 70)`,
|
failText: `${adjScore}/100 (< 70)`,
|
||||||
isWarning: (lynisReport.hardening_index || 0) >= 50,
|
isWarning: adjScore >= 50,
|
||||||
},
|
},
|
||||||
].map((item) => {
|
].map((item) => {
|
||||||
const color = item.ok ? "green" : item.isWarning ? "yellow" : "red"
|
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}
|
{item.ok ? item.passText : item.failText}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)})}
|
)})
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -3240,17 +3325,33 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
{lynisReport.warnings.map((w, idx) => (
|
{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="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-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-0.5">
|
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
|
||||||
<code className="text-[10px] px-1.5 py-0.5 rounded bg-red-500/10 text-red-500 font-mono">{w.test_id}</code>
|
<code className={`text-[10px] px-1.5 py-0.5 rounded font-mono ${
|
||||||
{w.severity && (
|
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>
|
<span className="text-[10px] text-red-400">{w.severity}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-foreground">{w.description}</p>
|
<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 && (
|
{w.solution && (
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Solution: {w.solution}
|
Solution: {w.solution}
|
||||||
@@ -3275,14 +3376,30 @@ ${(report.sections && report.sections.length > 0) ? `
|
|||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
{lynisReport.suggestions.map((s, idx) => (
|
{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="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-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-0.5">
|
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
|
||||||
<code className="text-[10px] px-1.5 py-0.5 rounded bg-yellow-500/10 text-yellow-500 font-mono">{s.test_id}</code>
|
<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>
|
</div>
|
||||||
<p className="text-sm text-foreground">{s.description}</p>
|
<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 && (
|
{s.solution && (
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Solution: {s.solution}
|
Solution: {s.solution}
|
||||||
|
|||||||
@@ -1673,4 +1673,185 @@ def parse_lynis_report():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
return report
|
||||||
|
|||||||
Reference in New Issue
Block a user