mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-18 16:36:27 +00:00
Update lynis
This commit is contained in:
@@ -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<string, { score?: number }>
|
||||
installed_packages: number; kernel_version: string
|
||||
firewall_active: boolean; malware_scanner: boolean
|
||||
}
|
||||
const [lynisAuditRunning, setLynisAuditRunning] = useState(false)
|
||||
const [lynisReport, setLynisReport] = useState<LynisReport | null>(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 `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Security Audit Report - ${report.hostname || "ProxMenux"}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: #1a1a2e; background: #fff; font-size: 13px; line-height: 1.5; }
|
||||
|
||||
/* Page setup for print */
|
||||
@page { margin: 15mm 15mm 20mm 15mm; size: A4; }
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.page-break { page-break-before: always; }
|
||||
body { font-size: 11px; }
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.report-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 20px 0; border-bottom: 3px solid #0f172a; margin-bottom: 24px;
|
||||
}
|
||||
.report-header-left { display: flex; align-items: center; gap: 16px; }
|
||||
.report-header-left img { height: 48px; width: auto; }
|
||||
.report-header-left h1 { font-size: 22px; font-weight: 700; color: #0f172a; }
|
||||
.report-header-left p { font-size: 11px; color: #64748b; }
|
||||
.report-header-right { text-align: right; font-size: 11px; color: #64748b; }
|
||||
.report-header-right .report-id { font-family: monospace; font-size: 10px; color: #94a3b8; }
|
||||
|
||||
/* Sections */
|
||||
.section { margin-bottom: 24px; page-break-inside: avoid; }
|
||||
.section-title {
|
||||
font-size: 14px; font-weight: 700; color: #0f172a; text-transform: uppercase;
|
||||
letter-spacing: 0.05em; padding-bottom: 6px; border-bottom: 2px solid #e2e8f0; margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Score box */
|
||||
.score-section { display: flex; align-items: center; gap: 24px; padding: 20px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; margin-bottom: 24px; }
|
||||
.score-circle {
|
||||
width: 100px; height: 100px; border-radius: 50%; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; border: 4px solid; flex-shrink: 0;
|
||||
}
|
||||
.score-number { font-size: 32px; font-weight: 800; line-height: 1; }
|
||||
.score-label { font-size: 9px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; }
|
||||
.score-details { flex: 1; }
|
||||
.score-details h3 { font-size: 16px; margin-bottom: 4px; }
|
||||
.score-details p { font-size: 12px; color: #64748b; }
|
||||
|
||||
/* Info grid */
|
||||
.info-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 8px; }
|
||||
.info-card { padding: 10px 12px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; }
|
||||
.info-label { font-size: 10px; font-weight: 600; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 2px; }
|
||||
.info-value { font-size: 13px; font-weight: 600; color: #0f172a; }
|
||||
|
||||
/* Status grid */
|
||||
.status-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 8px; }
|
||||
.status-card { padding: 12px; text-align: center; border-radius: 6px; border: 1px solid #e2e8f0; }
|
||||
.status-value { font-size: 20px; font-weight: 800; }
|
||||
.status-label { font-size: 10px; color: #64748b; text-transform: uppercase; margin-top: 2px; }
|
||||
|
||||
/* Findings */
|
||||
.finding { padding: 10px 12px; margin-bottom: 6px; border-left: 4px solid; border-radius: 0 4px 4px 0; background: #fafafa; }
|
||||
.finding-warning { border-color: #dc2626; background: #fef2f2; }
|
||||
.finding-suggestion { border-color: #ca8a04; background: #fefce8; }
|
||||
.finding-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
||||
.finding-id { font-family: 'Courier New', monospace; font-size: 10px; background: #e2e8f0; padding: 1px 6px; border-radius: 3px; font-weight: 600; }
|
||||
.finding-severity { font-size: 10px; font-weight: 700; text-transform: uppercase; color: #dc2626; }
|
||||
.finding-desc { font-size: 12px; color: #1e293b; }
|
||||
.finding-solution { font-size: 11px; color: #64748b; margin-top: 3px; }
|
||||
.finding-solution strong { color: #475569; }
|
||||
.finding-details { font-size: 10px; font-family: 'Courier New', monospace; color: #94a3b8; margin-top: 2px; }
|
||||
|
||||
/* Table of contents summary */
|
||||
.summary-bar { display: flex; gap: 16px; margin-bottom: 16px; }
|
||||
.summary-item { display: flex; align-items: center; gap: 6px; font-size: 12px; }
|
||||
.summary-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
|
||||
/* Footer */
|
||||
.report-footer {
|
||||
margin-top: 32px; padding-top: 12px; border-top: 1px solid #e2e8f0;
|
||||
display: flex; justify-content: space-between; font-size: 10px; color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Print button */
|
||||
.print-bar {
|
||||
position: fixed; top: 0; left: 0; right: 0; background: #0f172a; color: #fff;
|
||||
padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; z-index: 100;
|
||||
}
|
||||
.print-bar button {
|
||||
background: #06b6d4; color: #fff; border: none; padding: 8px 20px; border-radius: 6px;
|
||||
font-size: 13px; font-weight: 600; cursor: pointer;
|
||||
}
|
||||
.print-bar button:hover { background: #0891b2; }
|
||||
@media print { .print-bar { display: none; } body { padding-top: 0; } }
|
||||
@media screen { body { padding-top: 56px; max-width: 800px; margin: 0 auto; padding-left: 24px; padding-right: 24px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="print-bar no-print">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<strong>ProxMenux Security Audit Report</strong>
|
||||
<span style="font-size:11px;opacity:0.7;">Use Print / Save as PDF to download</span>
|
||||
</div>
|
||||
<button onclick="window.print()">Print / Save as PDF</button>
|
||||
</div>
|
||||
|
||||
<!-- Report Header -->
|
||||
<div class="report-header">
|
||||
<div class="report-header-left">
|
||||
<img src="${logoUrl}" alt="ProxMenux" onerror="this.style.display='none'" />
|
||||
<div>
|
||||
<h1>Security Audit Report</h1>
|
||||
<p>ProxMenux Monitor - Lynis System Audit</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="report-header-right">
|
||||
<div><strong>Report Date:</strong> ${now}</div>
|
||||
<div><strong>Auditor:</strong> Lynis ${report.lynis_version || ""}</div>
|
||||
<div class="report-id">ID: PMXA-${Date.now().toString(36).toUpperCase()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Executive Summary -->
|
||||
<div class="section">
|
||||
<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-label">${scoreLabel}</div>
|
||||
</div>
|
||||
<div class="score-details">
|
||||
<h3>System Hardening Assessment</h3>
|
||||
<p>
|
||||
This automated security audit was performed on host <strong>${report.hostname || "Unknown"}</strong>
|
||||
running <strong>${report.os_name} ${report.os_version}</strong>.
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Information -->
|
||||
<div class="section">
|
||||
<div class="section-title">2. System Information</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-card">
|
||||
<div class="info-label">Hostname</div>
|
||||
<div class="info-value">${report.hostname || "N/A"}</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-label">Operating System</div>
|
||||
<div class="info-value">${report.os_name} ${report.os_version}</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-label">Kernel</div>
|
||||
<div class="info-value">${report.kernel_version || "N/A"}</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-label">Lynis Version</div>
|
||||
<div class="info-value">${report.lynis_version || "N/A"}</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-label">Scan Started</div>
|
||||
<div class="info-value">${report.datetime_start || "N/A"}</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-label">Scan Ended</div>
|
||||
<div class="info-value">${report.datetime_end || "N/A"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Status -->
|
||||
<div class="section">
|
||||
<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"}</div>
|
||||
<div class="status-label">Hardening Score</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-value" style="color:#dc2626;">${report.warnings.length}</div>
|
||||
<div class="status-label">Warnings</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-value" style="color:#ca8a04;">${report.suggestions.length}</div>
|
||||
<div class="status-label">Suggestions</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-value">${report.tests_performed}</div>
|
||||
<div class="status-label">Tests Performed</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-grid" style="grid-template-columns: repeat(4, 1fr);">
|
||||
<div class="info-card" style="text-align:center;">
|
||||
<div class="info-label">Firewall</div>
|
||||
<div class="info-value" style="color:${report.firewall_active ? "#16a34a" : "#dc2626"};">${report.firewall_active ? "Active" : "Inactive"}</div>
|
||||
</div>
|
||||
<div class="info-card" style="text-align:center;">
|
||||
<div class="info-label">Malware Scanner</div>
|
||||
<div class="info-value" style="color:${report.malware_scanner ? "#16a34a" : "#ca8a04"};">${report.malware_scanner ? "Installed" : "Not Found"}</div>
|
||||
</div>
|
||||
<div class="info-card" style="text-align:center;">
|
||||
<div class="info-label">Packages</div>
|
||||
<div class="info-value">${report.installed_packages || "N/A"}</div>
|
||||
</div>
|
||||
<div class="info-card" style="text-align:center;">
|
||||
<div class="info-label">Score Rating</div>
|
||||
<div class="info-value" style="color:${scoreColor};">${scoreLabel}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warnings -->
|
||||
<div class="section page-break">
|
||||
<div class="section-title">4. Warnings (${report.warnings.length})</div>
|
||||
<p style="font-size:11px;color:#64748b;margin-bottom:12px;">Issues that require immediate attention and may represent security vulnerabilities.</p>
|
||||
${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-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>` : ""}
|
||||
</div>
|
||||
<div class="finding-desc">${w.description}</div>
|
||||
${w.solution ? `<div class="finding-solution"><strong>Recommendation:</strong> ${w.solution}</div>` : ""}
|
||||
</div>`).join("")}
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
${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-header">
|
||||
<span style="font-size:10px;color:#94a3b8;font-weight:700;">#${i + 1}</span>
|
||||
<span class="finding-id">${s.test_id}</span>
|
||||
</div>
|
||||
<div class="finding-desc">${s.description}</div>
|
||||
${s.solution ? `<div class="finding-solution"><strong>Recommendation:</strong> ${s.solution}</div>` : ""}
|
||||
${s.details ? `<div class="finding-details">${s.details}</div>` : ""}
|
||||
</div>`).join("")}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="report-footer">
|
||||
<div>Generated by ProxMenux Monitor using Lynis ${report.lynis_version || ""}</div>
|
||||
<div>Report Date: ${now}</div>
|
||||
<div style="font-style:italic;">Confidential - For authorized personnel only</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
const loadSslStatus = async () => {
|
||||
try {
|
||||
setLoadingSsl(true)
|
||||
@@ -2520,7 +2873,7 @@ export function Security() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* Status */}
|
||||
{/* Status bar */}
|
||||
<div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
@@ -2536,33 +2889,300 @@ export function Security() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last Scan Info */}
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="p-3 bg-muted/30 rounded-lg border border-border">
|
||||
{/* Summary stats */}
|
||||
<div className="grid gap-3 grid-cols-2 sm:grid-cols-4">
|
||||
<div className="p-3 bg-muted/30 rounded-lg border border-border text-center">
|
||||
<p className="text-xs text-muted-foreground mb-1">Last Scan</p>
|
||||
<p className="text-sm font-medium">
|
||||
{lynisInfo.last_scan || "No scan performed yet"}
|
||||
{lynisInfo.last_scan ? lynisInfo.last_scan.replace("T", " ").substring(0, 16) : "Never"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-muted/30 rounded-lg border border-border">
|
||||
<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-lg font-bold ${
|
||||
<p className={`text-xl font-bold ${
|
||||
lynisInfo.hardening_index === null ? "text-muted-foreground" :
|
||||
lynisInfo.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"}
|
||||
</p>
|
||||
</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 : "N/A"}
|
||||
</p>
|
||||
</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 : "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-blue-500">
|
||||
Run audits from the Proxmox terminal with: <code className="text-xs bg-blue-500/10 px-1.5 py-0.5 rounded">lynis audit system</code>
|
||||
{/* Hardening bar */}
|
||||
{lynisInfo.hardening_index !== null && (
|
||||
<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={`font-bold ${
|
||||
lynisInfo.hardening_index >= 70 ? "text-green-500" :
|
||||
lynisInfo.hardening_index >= 50 ? "text-yellow-500" :
|
||||
"text-red-500"
|
||||
}`}>
|
||||
{lynisInfo.hardening_index}/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 ${
|
||||
lynisInfo.hardening_index >= 70 ? "bg-green-500" :
|
||||
lynisInfo.hardening_index >= 50 ? "bg-yellow-500" :
|
||||
"bg-red-500"
|
||||
}`}
|
||||
style={{ width: `${lynisInfo.hardening_index}%` }}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleRunLynisAudit}
|
||||
disabled={lynisAuditRunning}
|
||||
className="flex-1 bg-cyan-600 hover:bg-cyan-700 text-white"
|
||||
>
|
||||
{lynisAuditRunning ? (
|
||||
<>
|
||||
<div className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full mr-2" />
|
||||
Running Audit...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Run Security Audit
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{lynisReport && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setLynisShowReport(!lynisShowReport)}
|
||||
className="bg-transparent border-cyan-500/30 text-cyan-500 hover:bg-cyan-500/10"
|
||||
>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
{lynisShowReport ? "Hide Report" : "View Report"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Running indicator */}
|
||||
{lynisAuditRunning && (
|
||||
<div className="bg-cyan-500/10 border border-cyan-500/20 rounded-lg p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin h-5 w-5 border-2 border-cyan-500 border-t-transparent rounded-full" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-cyan-500">Security audit in progress...</p>
|
||||
<p className="text-xs text-cyan-400/70">This may take 2-5 minutes. Lynis is scanning your system for vulnerabilities, misconfigurations, and hardening opportunities.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Report viewer */}
|
||||
{lynisShowReport && lynisReport && (
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
{/* Report header */}
|
||||
<div className="bg-muted/40 p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-cyan-500" />
|
||||
<span className="font-semibold text-sm">Audit Report</span>
|
||||
{lynisReport.datetime_start && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
- {lynisReport.datetime_start.replace("T", " ").substring(0, 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
// Open print-friendly report in new window
|
||||
const printWindow = window.open("", "_blank")
|
||||
if (printWindow) {
|
||||
printWindow.document.write(generatePrintableReport(lynisReport))
|
||||
printWindow.document.close()
|
||||
}
|
||||
}}
|
||||
className="h-7 px-2.5 text-xs text-cyan-500 hover:text-cyan-400 hover:bg-cyan-500/10"
|
||||
>
|
||||
<Printer className="h-3 w-3 mr-1" />
|
||||
Print / PDF
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* System info strip */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-px bg-border">
|
||||
<div className="p-2.5 bg-card text-center">
|
||||
<p className="text-[10px] text-muted-foreground uppercase">Hostname</p>
|
||||
<p className="text-xs font-medium truncate">{lynisReport.hostname || "N/A"}</p>
|
||||
</div>
|
||||
<div className="p-2.5 bg-card text-center">
|
||||
<p className="text-[10px] text-muted-foreground uppercase">OS</p>
|
||||
<p className="text-xs font-medium truncate">{lynisReport.os_name} {lynisReport.os_version}</p>
|
||||
</div>
|
||||
<div className="p-2.5 bg-card text-center">
|
||||
<p className="text-[10px] text-muted-foreground uppercase">Kernel</p>
|
||||
<p className="text-xs font-medium truncate">{lynisReport.kernel_version || "N/A"}</p>
|
||||
</div>
|
||||
<div className="p-2.5 bg-card text-center">
|
||||
<p className="text-[10px] text-muted-foreground uppercase">Tests</p>
|
||||
<p className="text-xs font-medium">{lynisReport.tests_performed}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Report tabs */}
|
||||
<div className="flex gap-0 border-t border-border">
|
||||
{(["overview", "warnings", "suggestions"] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setLynisActiveTab(tab)}
|
||||
className={`flex-1 px-3 py-2 text-xs font-medium transition-all flex items-center justify-center gap-1.5 border-r last:border-r-0 border-border ${
|
||||
lynisActiveTab === tab
|
||||
? "bg-cyan-500 text-white"
|
||||
: "bg-muted/20 text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
}`}
|
||||
>
|
||||
{tab === "overview" && <BarChart3 className="h-3 w-3" />}
|
||||
{tab === "warnings" && <TriangleAlert className="h-3 w-3" />}
|
||||
{tab === "suggestions" && <Info className="h-3 w-3" />}
|
||||
{tab === "overview" ? "Overview" : tab === "warnings" ? `Warnings (${lynisReport.warnings.length})` : `Suggestions (${lynisReport.suggestions.length})`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Overview tab */}
|
||||
{lynisActiveTab === "overview" && (
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
<div className="p-3 rounded-lg border border-border bg-muted/20 text-center">
|
||||
<p className="text-[10px] text-muted-foreground uppercase mb-1">Packages</p>
|
||||
<p className="text-lg font-bold">{lynisReport.installed_packages || "N/A"}</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-border bg-muted/20 text-center">
|
||||
<p className="text-[10px] text-muted-foreground uppercase mb-1">Firewall</p>
|
||||
<p className={`text-lg font-bold ${lynisReport.firewall_active ? "text-green-500" : "text-red-500"}`}>
|
||||
{lynisReport.firewall_active ? "Active" : "Inactive"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-border bg-muted/20 text-center">
|
||||
<p className="text-[10px] text-muted-foreground uppercase mb-1">Malware Scanner</p>
|
||||
<p className={`text-lg font-bold ${lynisReport.malware_scanner ? "text-green-500" : "text-yellow-500"}`}>
|
||||
{lynisReport.malware_scanner ? "Installed" : "Not Found"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security checklist */}
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Quick Status</p>
|
||||
{[
|
||||
{ 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) => (
|
||||
<div key={item.label} className="flex items-center gap-2 px-3 py-1.5 rounded bg-muted/20">
|
||||
<div className={`w-2 h-2 rounded-full ${item.ok ? "bg-green-500" : "bg-red-500"}`} />
|
||||
<span className="text-xs">{item.label}</span>
|
||||
<span className={`ml-auto text-[10px] font-bold ${item.ok ? "text-green-500" : "text-red-500"}`}>
|
||||
{item.ok ? "PASS" : "FAIL"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warnings tab */}
|
||||
{lynisActiveTab === "warnings" && (
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{lynisReport.warnings.length === 0 ? (
|
||||
<div className="p-6 text-center text-sm text-muted-foreground">
|
||||
No warnings found. Your system is well configured.
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
{lynisReport.warnings.map((w, idx) => (
|
||||
<div key={idx} className="p-3 hover:bg-muted/20 transition-colors">
|
||||
<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="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 && (
|
||||
<span className="text-[10px] text-red-400">{w.severity}</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-foreground">{w.description}</p>
|
||||
{w.solution && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Solution: {w.solution}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Suggestions tab */}
|
||||
{lynisActiveTab === "suggestions" && (
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{lynisReport.suggestions.length === 0 ? (
|
||||
<div className="p-6 text-center text-sm text-muted-foreground">
|
||||
No suggestions. System is fully hardened.
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
{lynisReport.suggestions.map((s, idx) => (
|
||||
<div key={idx} className="p-3 hover:bg-muted/20 transition-colors">
|
||||
<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="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>
|
||||
<p className="text-sm text-foreground">{s.description}</p>
|
||||
{s.solution && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Solution: {s.solution}
|
||||
</p>
|
||||
)}
|
||||
{s.details && (
|
||||
<p className="text-[10px] text-muted-foreground/70 mt-0.5 font-mono">{s.details}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -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
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user