mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-18 16:36:27 +00:00
Update security
This commit is contained in:
@@ -9,7 +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,
|
||||
FileText, Printer, Play, BarChart3, TriangleAlert, ChevronDown,
|
||||
} from "lucide-react"
|
||||
import { getApiUrl, fetchApi } from "../lib/api-config"
|
||||
import { TwoFactorSetup } from "./two-factor-setup"
|
||||
@@ -103,20 +103,27 @@ export function Security() {
|
||||
// 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 LynisCheck {
|
||||
name: string; status: string; detail?: string
|
||||
}
|
||||
interface LynisSection {
|
||||
name: string; checks: LynisCheck[]
|
||||
}
|
||||
interface LynisReport {
|
||||
datetime_start: string; datetime_end: string; lynis_version: string
|
||||
os_name: string; os_version: string; hostname: string
|
||||
os_name: string; os_version: string; os_fullname: 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
|
||||
sections: LynisSection[]
|
||||
}
|
||||
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")
|
||||
const [lynisActiveTab, setLynisActiveTab] = useState<"overview" | "warnings" | "suggestions" | "checks">("overview")
|
||||
|
||||
// Fail2Ban detailed state
|
||||
interface BannedIp {
|
||||
@@ -984,7 +991,7 @@ export function Security() {
|
||||
<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>.
|
||||
running <strong>${report.os_fullname || `${report.os_name} ${report.os_version}`.trim() || "Unknown OS"}</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.
|
||||
@@ -1003,7 +1010,7 @@ export function Security() {
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-label">Operating System</div>
|
||||
<div class="info-value">${report.os_name} ${report.os_version}</div>
|
||||
<div class="info-value">${report.os_fullname || `${report.os_name} ${report.os_version}`.trim() || "N/A"}</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-label">Kernel</div>
|
||||
@@ -1014,12 +1021,12 @@ export function Security() {
|
||||
<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 class="info-label">Report Date</div>
|
||||
<div class="info-value">${report.datetime_start ? report.datetime_start.replace("T", " ").substring(0, 16) : "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 class="info-label">Tests Performed</div>
|
||||
<div class="info-value">${report.tests_performed}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1029,15 +1036,15 @@ 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"}</div>
|
||||
<div class="status-label">Hardening Score</div>
|
||||
<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>
|
||||
<div class="status-card">
|
||||
<div class="status-value" style="color:#dc2626;">${report.warnings.length}</div>
|
||||
<div class="status-value" style="color:${report.warnings.length > 0 ? "#dc2626" : "#16a34a"};">${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-value" style="color:${report.suggestions.length > 0 ? "#ca8a04" : "#16a34a"};">${report.suggestions.length}</div>
|
||||
<div class="status-label">Suggestions</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
@@ -1045,7 +1052,7 @@ export function Security() {
|
||||
<div class="status-label">Tests Performed</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-grid" style="grid-template-columns: repeat(4, 1fr);">
|
||||
<div class="info-grid" style="grid-template-columns: repeat(3, 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>
|
||||
@@ -1055,13 +1062,9 @@ export function Security() {
|
||||
<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-label">Installed 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>
|
||||
|
||||
@@ -1101,6 +1104,43 @@ export function Security() {
|
||||
</div>`).join("")}
|
||||
</div>
|
||||
|
||||
<!-- Detailed Checks -->
|
||||
${(report.sections && report.sections.length > 0) ? `
|
||||
<div class="section page-break">
|
||||
<div class="section-title">6. Detailed Security Checks (${report.sections.length} categories)</div>
|
||||
<p style="font-size:11px;color:#64748b;margin-bottom:16px;">Complete list of all security checks performed during the audit, organized by category.</p>
|
||||
${report.sections.map((section, sIdx) => `
|
||||
<div style="margin-bottom:16px;page-break-inside:avoid;">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;padding:6px 10px;background:#f1f5f9;border-radius:4px;">
|
||||
<span style="font-size:10px;font-weight:700;color:#0891b2;background:#ecfeff;padding:2px 6px;border-radius:3px;">${sIdx + 1}</span>
|
||||
<span style="font-size:12px;font-weight:700;color:#0f172a;">${section.name}</span>
|
||||
<span style="font-size:10px;color:#94a3b8;margin-left:auto;">${section.checks.length} checks</span>
|
||||
</div>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:11px;">
|
||||
<thead>
|
||||
<tr style="background:#f8fafc;">
|
||||
<th style="text-align:left;padding:4px 8px;font-size:10px;color:#64748b;font-weight:600;border-bottom:1px solid #e2e8f0;">Check</th>
|
||||
<th style="text-align:right;padding:4px 8px;font-size:10px;color:#64748b;font-weight:600;border-bottom:1px solid #e2e8f0;width:120px;">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${section.checks.map(check => {
|
||||
const st = check.status.toUpperCase()
|
||||
const isWarn = ["WARNING", "UNSAFE", "WEAK", "DIFFERENT", "DISABLED"].includes(st)
|
||||
const isSugg = ["SUGGESTION", "PARTIALLY HARDENED", "MEDIUM", "NON DEFAULT"].includes(st)
|
||||
const isOk = ["OK", "FOUND", "DONE", "ENABLED", "ACTIVE", "YES", "HARDENED", "PROTECTED"].includes(st)
|
||||
const color = isWarn ? "#dc2626" : isSugg ? "#ca8a04" : isOk ? "#16a34a" : "#64748b"
|
||||
const bg = isWarn ? "#fef2f2" : isSugg ? "#fefce8" : "transparent"
|
||||
return `<tr style="background:${bg};border-bottom:1px solid #f1f5f9;">
|
||||
<td style="padding:3px 8px;color:#1e293b;">${check.name}${check.detail ? ` <span style="color:#94a3b8;font-size:10px;">(${check.detail})</span>` : ""}</td>
|
||||
<td style="padding:3px 8px;text-align:right;font-weight:700;color:${color};font-size:10px;">${check.status}</td>
|
||||
</tr>`
|
||||
}).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`).join("")}
|
||||
</div>` : ""}
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="report-footer">
|
||||
<div>Generated by ProxMenux Monitor using Lynis ${report.lynis_version || ""}</div>
|
||||
@@ -2900,89 +2940,60 @@ export function Security() {
|
||||
<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 ${
|
||||
lynisInfo.hardening_index === null ? "text-muted-foreground" :
|
||||
lynisInfo.hardening_index >= 70 ? "text-green-500" :
|
||||
lynisInfo.hardening_index >= 50 ? "text-yellow-500" :
|
||||
(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"
|
||||
}`}>
|
||||
{lynisInfo.hardening_index !== null ? lynisInfo.hardening_index : "N/A"}
|
||||
{(lynisReport?.hardening_index ?? lynisInfo.hardening_index) !== null
|
||||
? (lynisReport?.hardening_index ?? 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"}
|
||||
{lynisReport ? lynisReport.warnings.length : "-"}
|
||||
</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"}
|
||||
{lynisReport ? lynisReport.suggestions.length : "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
{(() => {
|
||||
const score = lynisReport?.hardening_index ?? lynisInfo.hardening_index
|
||||
if (score === null || score === undefined) return null
|
||||
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={`font-bold ${
|
||||
score >= 70 ? "text-green-500" : score >= 50 ? "text-yellow-500" : "text-red-500"
|
||||
}`}>
|
||||
{score}/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>
|
||||
<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>
|
||||
<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 && (
|
||||
@@ -2997,192 +3008,296 @@ export function Security() {
|
||||
</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"
|
||||
{/* Reports list */}
|
||||
{lynisReport && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Audit Reports</p>
|
||||
|
||||
{/* Report row - clickable to expand */}
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => setLynisShowReport(!lynisShowReport)}
|
||||
className="w-full flex items-center justify-between p-3 bg-muted/20 hover:bg-muted/40 transition-colors text-left"
|
||||
>
|
||||
<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"}
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="h-4 w-4 text-cyan-500 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
Security Audit - {lynisReport.datetime_start
|
||||
? lynisReport.datetime_start.replace("T", " ").substring(0, 16)
|
||||
: lynisInfo.last_scan?.replace("T", " ").substring(0, 16) || "Unknown date"}
|
||||
</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 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
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
const printWindow = window.open("", "_blank")
|
||||
if (printWindow) {
|
||||
printWindow.document.write(generatePrintableReport(lynisReport))
|
||||
printWindow.document.close()
|
||||
}
|
||||
}}
|
||||
className="h-7 px-2 text-xs text-cyan-500 hover:text-cyan-400 hover:bg-cyan-500/10"
|
||||
title="Print / Save as PDF"
|
||||
>
|
||||
<Printer className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (confirm("Delete this audit report? The report file will be removed from the server.")) {
|
||||
fetchApi("/api/security/lynis/report", { method: "DELETE" })
|
||||
.then(() => {
|
||||
setLynisReport(null)
|
||||
setLynisShowReport(false)
|
||||
setSuccess("Report deleted")
|
||||
loadSecurityTools()
|
||||
})
|
||||
.catch(() => setError("Failed to delete report"))
|
||||
}
|
||||
}}
|
||||
className="h-7 px-2 text-xs text-red-500 hover:text-red-400 hover:bg-red-500/10"
|
||||
title="Delete report"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<ChevronDown className={`h-4 w-4 text-muted-foreground transition-transform ${lynisShowReport ? "rotate-180" : ""}`} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 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>
|
||||
{/* Expanded report details */}
|
||||
{lynisShowReport && (
|
||||
<div className="border-t border-border">
|
||||
{/* 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_fullname || `${lynisReport.os_name} ${lynisReport.os_version}`.trim() || "N/A"}</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", "checks", "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 === "checks" && <Search 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 === "checks" ? `Checks (${lynisReport.sections?.length || 0})`
|
||||
: 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>
|
||||
)}
|
||||
|
||||
{/* Checks tab */}
|
||||
{lynisActiveTab === "checks" && (
|
||||
<div className="max-h-[500px] overflow-y-auto">
|
||||
{(!lynisReport.sections || lynisReport.sections.length === 0) ? (
|
||||
<div className="p-6 text-center text-sm text-muted-foreground">
|
||||
No check details available. Run an audit to generate detailed results.
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
{lynisReport.sections.map((section, sIdx) => (
|
||||
<div key={sIdx}>
|
||||
<div className="px-3 py-2 bg-muted/30 flex items-center gap-2">
|
||||
<span className="text-[10px] font-bold text-cyan-500 bg-cyan-500/10 px-1.5 py-0.5 rounded">{sIdx + 1}</span>
|
||||
<span className="text-xs font-semibold">{section.name}</span>
|
||||
<span className="text-[10px] text-muted-foreground ml-auto">{section.checks.length} checks</span>
|
||||
</div>
|
||||
<div className="divide-y divide-border/50">
|
||||
{section.checks.map((check, cIdx) => {
|
||||
const st = check.status.toUpperCase()
|
||||
const isOk = ["OK", "FOUND", "DONE", "ENABLED", "ACTIVE", "YES", "HARDENED", "PROTECTED", "NONE", "NOT FOUND", "NOT RUNNING", "NOT ACTIVE", "NOT ENABLED", "DEFAULT", "NO"].includes(st)
|
||||
const isWarn = ["WARNING", "UNSAFE", "WEAK", "DIFFERENT", "DISABLED"].includes(st)
|
||||
const isSugg = ["SUGGESTION", "PARTIALLY HARDENED", "MEDIUM", "NON DEFAULT"].includes(st)
|
||||
const dotColor = isWarn ? "bg-red-500" : isSugg ? "bg-yellow-500" : isOk ? "bg-green-500" : "bg-muted-foreground"
|
||||
const textColor = isWarn ? "text-red-500" : isSugg ? "text-yellow-500" : isOk ? "text-green-500" : "text-muted-foreground"
|
||||
return (
|
||||
<div key={cIdx} className="flex items-center gap-2 px-3 py-1.5 hover:bg-muted/10">
|
||||
<div className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${dotColor}`} />
|
||||
<span className="text-[11px] flex-1 min-w-0 truncate">{check.name}</span>
|
||||
{check.detail && <span className="text-[10px] text-muted-foreground/70 truncate max-w-[150px]">{check.detail}</span>}
|
||||
<span className={`text-[10px] font-bold flex-shrink-0 ${textColor}`}>{check.status}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* Run audit button - at the bottom */}
|
||||
<Button
|
||||
onClick={handleRunLynisAudit}
|
||||
disabled={lynisAuditRunning}
|
||||
className="w-full 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>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -255,6 +255,26 @@ def lynis_report():
|
||||
return jsonify({"success": False, "message": str(e)}), 500
|
||||
|
||||
|
||||
@security_bp.route('/api/security/lynis/report', methods=['DELETE'])
|
||||
def lynis_report_delete():
|
||||
"""Delete Lynis audit report files"""
|
||||
if not security_manager:
|
||||
return jsonify({"success": False, "message": "Security manager not available"}), 500
|
||||
try:
|
||||
import os
|
||||
deleted = []
|
||||
for f in ["/var/log/lynis-report.dat", "/var/log/lynis.log", "/var/log/lynis-output.log"]:
|
||||
if os.path.isfile(f):
|
||||
os.remove(f)
|
||||
deleted.append(f)
|
||||
if deleted:
|
||||
return jsonify({"success": True, "message": f"Deleted: {', '.join(deleted)}"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "No report files found to delete"})
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "message": str(e)}), 500
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Security Tools Detection
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
@@ -1042,21 +1042,27 @@ def _detect_lynis():
|
||||
if rc == 0:
|
||||
info["version"] = out.strip()
|
||||
|
||||
# Check for last scan report
|
||||
report_file = "/var/log/lynis-report.dat"
|
||||
if os.path.isfile(report_file):
|
||||
try:
|
||||
with open(report_file, 'r') as f:
|
||||
for line in f:
|
||||
if line.startswith("report_datetime_start="):
|
||||
info["last_scan"] = line.split("=", 1)[1].strip()
|
||||
elif line.startswith("hardening_index="):
|
||||
try:
|
||||
info["hardening_index"] = int(line.split("=", 1)[1].strip())
|
||||
except ValueError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
# Check for last scan report - use full parser for accurate data
|
||||
report = parse_lynis_report()
|
||||
if report:
|
||||
info["last_scan"] = report.get("datetime_start", None)
|
||||
info["hardening_index"] = report.get("hardening_index", None)
|
||||
else:
|
||||
# Fallback: quick read of report.dat
|
||||
report_file = "/var/log/lynis-report.dat"
|
||||
if os.path.isfile(report_file):
|
||||
try:
|
||||
with open(report_file, 'r') as f:
|
||||
for line in f:
|
||||
if line.startswith("report_datetime_start="):
|
||||
info["last_scan"] = line.split("=", 1)[1].strip()
|
||||
elif line.startswith("hardening_index="):
|
||||
try:
|
||||
info["hardening_index"] = int(line.split("=", 1)[1].strip())
|
||||
except ValueError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return info
|
||||
|
||||
@@ -1098,6 +1104,13 @@ def run_lynis_audit():
|
||||
[lynis_cmd, "audit", "system", "--no-colors", "--quick"],
|
||||
timeout=600
|
||||
)
|
||||
# Save stdout output for section parsing
|
||||
if out:
|
||||
try:
|
||||
with open("/var/log/lynis-output.log", "w") as fout:
|
||||
fout.write(out)
|
||||
except Exception:
|
||||
pass
|
||||
if rc == 0:
|
||||
_lynis_audit_progress = "completed"
|
||||
else:
|
||||
@@ -1123,6 +1136,7 @@ def get_lynis_audit_status():
|
||||
def parse_lynis_report():
|
||||
"""
|
||||
Parse /var/log/lynis-report.dat into structured report data.
|
||||
Also enriches with data from lynis.log when report.dat is sparse.
|
||||
Returns a dict with all audit findings.
|
||||
"""
|
||||
report_file = "/var/log/lynis-report.dat"
|
||||
@@ -1135,6 +1149,7 @@ def parse_lynis_report():
|
||||
"lynis_version": "",
|
||||
"os_name": "",
|
||||
"os_version": "",
|
||||
"os_fullname": "",
|
||||
"hostname": "",
|
||||
"hardening_index": None,
|
||||
"tests_performed": 0,
|
||||
@@ -1147,11 +1162,16 @@ def parse_lynis_report():
|
||||
"malware_scanner": False,
|
||||
}
|
||||
|
||||
# Collect all raw key-value pairs first for flexible matching
|
||||
raw_data = {}
|
||||
warnings_raw = []
|
||||
suggestions_raw = []
|
||||
|
||||
try:
|
||||
with open(report_file, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
if not line or line.startswith("#") or line.startswith("["):
|
||||
continue
|
||||
|
||||
if "=" not in line:
|
||||
@@ -1161,73 +1181,212 @@ def parse_lynis_report():
|
||||
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
|
||||
if key == "warning[]":
|
||||
warnings_raw.append(value)
|
||||
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
|
||||
|
||||
suggestions_raw.append(value)
|
||||
else:
|
||||
# Last value wins (some keys appear multiple times)
|
||||
raw_data[key] = value
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# Map known fields (Lynis uses varied naming across versions)
|
||||
report["datetime_start"] = raw_data.get("report_datetime_start", "")
|
||||
report["datetime_end"] = raw_data.get("report_datetime_end", "")
|
||||
report["lynis_version"] = raw_data.get("lynis_version", "")
|
||||
report["hostname"] = raw_data.get("hostname", "")
|
||||
|
||||
# OS name - try multiple fields
|
||||
report["os_name"] = (raw_data.get("os_name", "") or
|
||||
raw_data.get("os", "") or
|
||||
raw_data.get("os_fullname", ""))
|
||||
report["os_version"] = (raw_data.get("os_version", "") or
|
||||
raw_data.get("os_version_id", ""))
|
||||
report["os_fullname"] = raw_data.get("os_fullname", "")
|
||||
|
||||
# Kernel - try multiple field names
|
||||
report["kernel_version"] = (raw_data.get("os_kernel_version_full", "") or
|
||||
raw_data.get("os_kernel_version", "") or
|
||||
raw_data.get("linux_kernel_version", "") or
|
||||
raw_data.get("linux_version", "") or
|
||||
raw_data.get("os_kernelversion_full", "") or
|
||||
raw_data.get("os_kernelversion", ""))
|
||||
|
||||
# Hardening index
|
||||
for k in ["hardening_index", "hpindex", "hp_index"]:
|
||||
if k in raw_data:
|
||||
try:
|
||||
report["hardening_index"] = int(raw_data[k])
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Tests performed
|
||||
for k in ["tests_performed", "ctests_performed", "total_tests"]:
|
||||
if k in raw_data:
|
||||
try:
|
||||
val = int(raw_data[k])
|
||||
if val > report["tests_performed"]:
|
||||
report["tests_performed"] = val
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Installed packages
|
||||
for k in ["installed_packages", "installed_packages_array"]:
|
||||
if k in raw_data:
|
||||
try:
|
||||
report["installed_packages"] = int(raw_data[k])
|
||||
except ValueError:
|
||||
# Might be a string like "package1,package2" - count them
|
||||
pkgs = raw_data[k]
|
||||
if pkgs:
|
||||
report["installed_packages"] = len(pkgs.split(","))
|
||||
|
||||
# Firewall
|
||||
for k in ["firewall_active", "firewall_installed"]:
|
||||
if k in raw_data and raw_data[k] in ("1", "true", "yes"):
|
||||
report["firewall_active"] = True
|
||||
break
|
||||
|
||||
# Malware scanner
|
||||
for k in ["malware_scanner_installed", "malware_scanner"]:
|
||||
if k in raw_data and raw_data[k] in ("1", "true", "yes"):
|
||||
report["malware_scanner"] = True
|
||||
break
|
||||
|
||||
# Parse warnings
|
||||
for w in warnings_raw:
|
||||
parts = w.split("|")
|
||||
if len(parts) >= 2:
|
||||
report["warnings"].append({
|
||||
"test_id": parts[0].strip() if len(parts) > 0 else "",
|
||||
"severity": parts[1].strip() if len(parts) > 1 else "",
|
||||
"description": parts[2].strip() if len(parts) > 2 else parts[1].strip(),
|
||||
"solution": parts[3].strip() if len(parts) > 3 else "",
|
||||
})
|
||||
|
||||
# Parse suggestions
|
||||
for s in suggestions_raw:
|
||||
parts = s.split("|")
|
||||
if len(parts) >= 2:
|
||||
report["suggestions"].append({
|
||||
"test_id": parts[0].strip() if len(parts) > 0 else "",
|
||||
"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 "",
|
||||
})
|
||||
|
||||
# Parse lynis-output.log (stdout) for section checks, fallback to lynis.log
|
||||
report["sections"] = []
|
||||
# Prefer the stdout output which has clean formatted sections
|
||||
output_file = "/var/log/lynis-output.log"
|
||||
log_file = output_file if os.path.isfile(output_file) else "/var/log/lynis.log"
|
||||
if os.path.isfile(log_file):
|
||||
try:
|
||||
import re
|
||||
with open(log_file, 'r') as f:
|
||||
log_lines = f.readlines()
|
||||
|
||||
current_section = None
|
||||
current_checks = []
|
||||
|
||||
for line in log_lines:
|
||||
line = line.rstrip('\n')
|
||||
stripped = line.strip()
|
||||
|
||||
# Detect section headers: "[+] Boot and services"
|
||||
section_match = re.match(r'^\[\+\]\s+(.+)', stripped)
|
||||
if section_match:
|
||||
# Save previous section
|
||||
if current_section and current_checks:
|
||||
report["sections"].append({
|
||||
"name": current_section,
|
||||
"checks": current_checks,
|
||||
})
|
||||
current_section = section_match.group(1).strip()
|
||||
current_checks = []
|
||||
continue
|
||||
|
||||
# Skip separator lines, empty, banner lines
|
||||
if stripped.startswith('---') or not stripped:
|
||||
continue
|
||||
if stripped.startswith('===') or stripped.startswith('#'):
|
||||
current_section = None # Stop parsing after results summary
|
||||
continue
|
||||
|
||||
# Detect any line with [ STATUS ] pattern (covers -, File:, Directory:, etc.)
|
||||
check_match = re.match(
|
||||
r'^[\s]*[-*]?\s*(.+?)\s{2,}\[\s*(.+?)\s*\]\s*$', stripped
|
||||
)
|
||||
if check_match and current_section:
|
||||
check_name = check_match.group(1).strip()
|
||||
check_status = check_match.group(2).strip()
|
||||
# Skip noise lines
|
||||
if check_name and not check_name.startswith('..') and len(check_name) > 2:
|
||||
current_checks.append({
|
||||
"name": check_name,
|
||||
"status": check_status,
|
||||
})
|
||||
continue
|
||||
|
||||
# Detect sub-results: " Result: found 35 running services"
|
||||
result_match = re.match(r'^[\s]+Result:\s+(.+)', stripped)
|
||||
if result_match and current_section and current_checks:
|
||||
current_checks[-1]["detail"] = result_match.group(1).strip()
|
||||
continue
|
||||
|
||||
# Fallback data extraction
|
||||
if not report["hardening_index"] and "Hardening index" in stripped:
|
||||
m = re.search(r'Hardening index\s*:\s*\[?(\d+)\]?', stripped)
|
||||
if m:
|
||||
report["hardening_index"] = int(m.group(1))
|
||||
if report["tests_performed"] == 0 and "Tests performed" in stripped:
|
||||
m = re.search(r'Tests performed\s*:\s*(\d+)', stripped)
|
||||
if m:
|
||||
report["tests_performed"] = int(m.group(1))
|
||||
if not report["kernel_version"] and "Kernel version" in stripped:
|
||||
m = re.search(r'Kernel version\s*:\s*(.+)', stripped)
|
||||
if m:
|
||||
report["kernel_version"] = m.group(1).strip()
|
||||
if not report["hostname"] and "Hostname" in stripped and ":" in stripped:
|
||||
m = re.search(r'Hostname\s*:\s*(.+)', stripped)
|
||||
if m:
|
||||
val = m.group(1).strip()
|
||||
if val and val != "N/A":
|
||||
report["hostname"] = val
|
||||
|
||||
# Save last section
|
||||
if current_section and current_checks:
|
||||
report["sections"].append({
|
||||
"name": current_section,
|
||||
"checks": current_checks,
|
||||
})
|
||||
|
||||
# Filter out sections with no meaningful checks
|
||||
report["sections"] = [
|
||||
s for s in report["sections"]
|
||||
if len(s["checks"]) > 0
|
||||
]
|
||||
|
||||
except Exception:
|
||||
report["sections"] = []
|
||||
|
||||
# Fallback: get kernel from uname if still empty
|
||||
if not report["kernel_version"]:
|
||||
try:
|
||||
rc, out, _ = _run_cmd(["uname", "-r"])
|
||||
if rc == 0 and out.strip():
|
||||
report["kernel_version"] = out.strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: get hostname from system
|
||||
if not report["hostname"]:
|
||||
try:
|
||||
import socket
|
||||
report["hostname"] = socket.gethostname()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return report
|
||||
|
||||
Reference in New Issue
Block a user