@extends('layouts.app') @section('content')
{{-- ===== 頁首:標題 + 主機摘要 ===== --}}

{{ $host->name }}

{{ $reportData['os_release']['PRETTY_NAME'] ?? '' }} @if (!empty($reportData['ip']))・IP:{{ $reportData['ip'] }}@endif @if (!empty($reportData['uptime']))・{{ $reportData['uptime'] }}@endif

報告時間:{{ $reportDate }} @if ($host->agent_last_report_at) @endif
Word PDF 編輯
@csrf @method('DELETE')
返回
@if (!empty($reportData['error']))
{{ $reportData['error'] }}
@endif {{-- ===== 完成度進度條 ===== --}} @if (empty($reportData['error'])) @php $progressColor = match (true) { $completionRate >= 90 => 'bg-success', $completionRate >= 70 => 'bg-info', $completionRate >= 50 => 'bg-warning', default => 'bg-danger', }; @endphp
@endif {{-- ==================================================== 資安合規總覽 ===================================================== --}} @php $rootDisk = null; foreach (($reportData['disk_info'] ?? []) as $disk) { if (($disk['mounted_on'] ?? '') === '/') { $rootDisk = $disk; break; } } $memPercent = 0; if (!empty($reportData['memory']['total_gb']) && $reportData['memory']['total_gb'] > 0) { $memPercent = round($reportData['memory']['used_gb'] / $reportData['memory']['total_gb'] * 100); } $diskPercent = $rootDisk ? intval($rootDisk['use'] ?? '0') : 0; // 告警閾值(主機個別設定 or 全域預設) $alertCpu = $host->cpu_alert_threshold ?? (int)\App\Models\SystemSetting::get('host_alert_cpu', 85); $alertMem = $host->mem_alert_threshold ?? (int)\App\Models\SystemSetting::get('host_alert_mem', 85); $alertDisk = $host->disk_alert_threshold ?? (int)\App\Models\SystemSetting::get('host_alert_disk', 80); $ssh = $reportData['ssh_config'] ?? []; $pwd = $reportData['password_policy'] ?? []; $secChecks = [ ['name' => '防火牆', 'pass' => ($reportData['firewall_status'] ?? '') === 'active', 'icon' => 'bi-bricks'], ['name' => 'SSH Root 禁止', 'pass' => in_array($ssh['permit_root_login'] ?? '', ['no', 'prohibit-password', 'without-password', 'forced-commands-only']), 'icon' => 'bi-terminal'], ['name' => 'fail2ban', 'pass' => ($reportData['fail2ban_status'] ?? '') === 'active', 'icon' => 'bi-shield-lock'], ['name' => '掃毒軟體', 'pass' => !empty($reportData['antivirus_installed']), 'icon' => 'bi-bug'], ['name' => 'NTP 校時', 'pass' => ($reportData['ntp_status'] ?? '') === 'active', 'icon' => 'bi-clock'], ['name' => '密碼複雜度', 'pass' => !empty($pwd['pam_pwquality']), 'icon' => 'bi-key'], ['name' => '安全更新', 'pass' => ($reportData['security_updates'] ?? 0) == 0, 'icon' => 'bi-download'], ['name' => '存取控制', 'pass' => in_array(strtolower($reportData['mac_status'] ?? ''), ['enabled', 'enforcing']), 'icon' => 'bi-lock'], ]; $passCount = count(array_filter($secChecks, fn($c) => $c['pass'])); @endphp
資安合規檢查 {{ $passCount }}/{{ count($secChecks) }} 通過
@foreach ($secChecks as $chk)
{{ $chk['name'] }}
{{ $chk['pass'] ? 'PASS' : 'FAIL' }}
@endforeach
{{-- ==================================================== 1-20 詳細檢查項目 ===================================================== --}}
{{-- 1. 基本系統資訊 --}}
1. 基本系統資訊
作業系統{{ $reportData['os_release']['PRETTY_NAME'] ?? ($reportData['os'] ?? 'N/A') }}
核心版本{{ $reportData['kernel_version'] ?? 'N/A' }}
主機名稱{{ $reportData['hostname'] ?? 'N/A' }}
IP 位址{{ $reportData['ip'] ?? 'N/A' }}
時區{{ $reportData['timezone'] ?? 'N/A' }}
運行時間{{ $reportData['uptime'] ?? 'N/A' }}
{{-- 2. 硬體資訊 --}}
2. 硬體資訊
@php $alertThreshold = $alertDisk; @endphp @forelse(collect($reportData['disk_info'] ?? [])->reject(fn($d) => str_starts_with($d['filesystem'] ?? '', '/dev/loop') || str_starts_with($d['mounted_on'] ?? '', '/snap/'))->values() as $disk) @php $pct = intval($disk['percent'] ?? $disk['use'] ?? 0); @endphp @empty @endforelse
CPU 型號{{ $reportData['cpu_model'] ?? 'N/A' }}
CPU 數量{{ $reportData['cpu_count'] ?? 'N/A' }} 核心
記憶體 @if(isset($reportData['memory'])) {{ $reportData['memory']['total_gb'] ?? '?' }} GB(已用 {{ $reportData['memory']['used_gb'] ?? '?' }} GB / 可用 {{ $reportData['memory']['free_gb'] ?? '?' }} GB) @if($memPercent >= $alertMem) 超過告警閾值 {{ $alertMem }}% @endif
@else N/A @endif
{{ $disk['mounted_on'] ?? '?' }} @if($pct >= $alertThreshold) @endif
已用 {{ $disk['used'] ?? '?' }} / 總計 {{ $disk['size'] ?? '?' }} {{ $pct }}%
{{ $disk['filesystem'] ?? '' }} 剩餘 {{ $disk['avail'] ?? '?' }}
磁碟N/A
@php $overThreshold = collect($reportData['disk_info'] ?? [])->reject(fn($d) => str_starts_with($d['filesystem'] ?? '', '/dev/loop') || str_starts_with($d['mounted_on'] ?? '', '/snap/'))->filter(fn($d) => intval($d['percent'] ?? $d['use'] ?? 0) >= $alertThreshold)->count(); @endphp @if($overThreshold > 0)
{{ $overThreshold }} 個磁碟分割區超過告警閾值({{ $alertThreshold }}%)
@endif
{{-- 3. 網路與服務資訊 --}}
3. 網路與服務資訊
@php $apacheRaw = $reportData['apache_version'] ?? ''; $nginxRaw = $reportData['nginx_version'] ?? ''; $apacheVer = preg_match('/Apache\/([\d\.]+)/', $apacheRaw, $m) ? $m[1] : $apacheRaw; $nginxVer = $nginxRaw; if ($apacheVer) { $webLabel = 'Apache'; $webVer = $apacheVer; } elseif ($nginxVer) { $webLabel = 'Nginx'; $webVer = $nginxVer; } else { $webLabel = 'Web Server'; $webVer = 'N/A'; } $phpVer = $reportData['php_version'] ?? ''; $dotnetVer = $reportData['dotnet_version'] ?? ''; if ($phpVer) { $langLabel = 'PHP'; $langVer = $phpVer; } elseif ($dotnetVer) { $langLabel = '.NET'; $langVer = $dotnetVer; } else { $langLabel = 'PHP / .NET'; $langVer = 'N/A'; } $versions = [ $webLabel => $webVer, $langLabel => $langVer, 'MySQL' => preg_match('/([\d\.]+)/', $reportData['mysql_version'] ?? '', $m) ? $m[1] : 'N/A', 'OpenSSL' => $reportData['openssl_version'] ?? 'N/A', 'OpenSSH' => $reportData['openssh_version'] ?? 'N/A', 'Python' => $reportData['python_version'] ?? 'N/A', 'GCC' => $reportData['gcc_version'] ?? 'N/A', ]; @endphp
@foreach ($versions as $name => $ver) @endforeach
{{ $name }}{{ $ver }}
NTP 校時 @if (($reportData['ntp_status'] ?? '') === 'active') 已啟用 @else {{ $reportData['ntp_status'] ?? 'N/A' }} @endif @if (!empty($reportData['ntp_servers'])) {{ implode(', ', $reportData['ntp_servers']) }} @endif
{{-- 4. 帳號管理(僅特權帳號摘要) --}}
4. 帳號管理

特權帳號(sudo): @foreach (($reportData['privileged_accounts'] ?? []) as $acct) {{ $acct }} @endforeach @if (empty($reportData['privileged_accounts'])) N/A @endif

詳細帳號安全檢查請見第 15 項
{{-- 5. 掃毒資訊 --}}
5. 掃毒資訊

掃毒軟體: @if (!empty($reportData['antivirus_installed'])) 已安裝 @else 未安裝 @endif

版本: {{ $reportData['antivirus_version'] ?? 'N/A' }}

@if (!empty($reportData['antivirus_result']) && $reportData['antivirus_result'] !== '無掃毒結果')
掃描結果(點擊展開)
{{ $reportData['antivirus_result'] }}
@endif
{{-- 6. 防火牆設定 --}}
6. 防火牆設定
類型:{{ strtoupper($reportData['firewall_type'] ?? 'N/A') }} @if (($reportData['firewall_status'] ?? '') === 'active') 已啟用 @else {{ $reportData['firewall_status'] ?? 'N/A' }} @endif
@if (!empty($reportData['firewall_rules']))
防火牆規則(點擊展開)
{{ $reportData['firewall_rules'] }}
@endif
{{-- 7. SSH 安全設定 --}}
7. SSH 安全設定
Port{{ $ssh['port'] ?? '22' }}
Root 登入 @php $rl = $ssh['permit_root_login'] ?? ''; @endphp @if ($rl === 'no') 已禁止(no) @elseif (in_array($rl, ['prohibit-password', 'without-password'])) 僅金鑰({{ $rl }}) @elseif ($rl === 'forced-commands-only') 僅指定指令(forced-commands-only) @else {{ $rl ?: 'unknown' }} @endif
密碼認證 @if (($ssh['password_auth'] ?? '') === 'no') 已關閉(僅金鑰) @else {{ $ssh['password_auth'] ?? 'unknown' }} @endif
公鑰認證{{ $ssh['pubkey_auth'] ?? 'unknown' }}
最大嘗試次數{{ $ssh['max_auth_tries'] ?? 'unknown' }}
{{-- 8. 密碼政策 --}}
8. 密碼政策
最長有效天數{{ $pwd['max_days'] ?? 'N/A' }} 天
最短更改間隔{{ $pwd['min_days'] ?? 'N/A' }} 天
密碼最短長度{{ $pwd['min_length'] ?? ($pwd['pam_minlen'] ?? 'N/A') }}
到期警告天數{{ $pwd['warn_age'] ?? 'N/A' }} 天
密碼複雜度 @if (!empty($pwd['pam_pwquality'])) 已啟用(pam_pwquality) @else 未設定 @endif
{{-- 9. fail2ban 入侵防護 --}}
9. fail2ban 入侵防護

狀態: @if (($reportData['fail2ban_status'] ?? '') === 'active') 已啟用 @else {{ $reportData['fail2ban_status'] ?? 'N/A' }} @endif

@if (!empty($reportData['fail2ban_jails']))
啟用的 Jail
{{ $reportData['fail2ban_jails'] }}
@endif
{{-- 10. 系統更新狀態 --}}
10. 系統更新狀態
上次更新{{ $reportData['last_update'] ?? 'N/A' }}
待更新套件 @if (($reportData['pending_updates'] ?? 0) == 0) 已是最新 @else {{ $reportData['pending_updates'] }} 個待更新 @endif
安全更新 @if (($reportData['security_updates'] ?? 0) == 0) 無待處理 @else {{ $reportData['security_updates'] }} 個待處理 @endif
{{-- 11. Web Server 安全設定 --}} @if (!empty($reportData['apache_security'])) @php $as = $reportData['apache_security']; @endphp
11. Apache 安全設定
ServerTokens @if (strtolower($as['server_tokens'] ?? '') === 'prod')Prod@else{{ $as['server_tokens'] ?? 'unknown' }}@endif
ServerSignature @if (strtolower($as['server_signature'] ?? '') === 'off')Off@else{{ $as['server_signature'] ?? 'unknown' }}@endif
TraceEnable @if (strtolower($as['trace_enable'] ?? '') === 'off')Off@else{{ $as['trace_enable'] ?? 'unknown' }}@endif
@elseif (!empty($reportData['nginx_security'])) @php $ns = $reportData['nginx_security']; @endphp
11. Nginx 安全設定
server_tokens @if (strtolower($ns['server_tokens'] ?? '') === 'off')off@else{{ $ns['server_tokens'] ?? 'unknown' }}@endif
gzip @if (strtolower($ns['gzip'] ?? '') === 'on')on@else{{ $ns['gzip'] ?? 'unknown' }}@endif
autoindex @if (strtolower($ns['autoindex'] ?? '') === 'off')off@else{{ $ns['autoindex'] ?? 'unknown' }}@endif
@endif {{-- 11b. 網站安全標頭 + SSL 憑證 + 錯誤日誌 --}} @php $wsSec = $reportData['web_service'] ?? null; $wsSecName = $wsSec['name'] ?? ''; @endphp @if (!empty($wsSecName) && ($wsSec['status'] ?? '') === 'active') @php $hdr = $wsSec['http_headers'] ?? []; $sslDays = isset($wsSec['ssl_cert_days_left']) ? intval($wsSec['ssl_cert_days_left']) : -1; $sslDate = $wsSec['ssl_cert_expiry'] ?? ''; $errCount = intval($wsSec['error_count'] ?? 0); $errSamp = $wsSec['error_sample'] ?? ''; $hdrRows = [ 'Content-Security-Policy' => $hdr['csp'] ?? '', 'Strict-Transport-Security' => $hdr['hsts'] ?? '', 'X-Frame-Options' => $hdr['x_frame_options'] ?? '', 'X-Content-Type-Options' => $hdr['x_content_type_options'] ?? '', 'X-XSS-Protection' => $hdr['x_xss_protection'] ?? '', 'Referrer-Policy' => $hdr['referrer_policy'] ?? '', 'Permissions-Policy' => $hdr['permissions_policy'] ?? '', 'Server' => $hdr['server'] ?? '', ]; @endphp
11b. 網站資安檢測({{ $wsSecName }})
{{-- HTTP Security Headers --}}
HTTP 資安標頭
@foreach ($hdrRows as $hName => $hVal) @php $isServer = ($hName === 'Server'); $hasVersionLeak = $isServer && $hVal && preg_match('/\d+\.\d+/', $hVal); if ($isServer) { $badge = $hVal ? ($hasVersionLeak ? 'bg-warning text-dark' : 'bg-success') : 'bg-secondary'; $label = $hVal ?: '未偵測'; } else { $badge = $hVal ? 'bg-success' : 'bg-danger'; $label = $hVal ?: '未設定'; } @endphp @endforeach
{{ $hName }} @if (strlen($hVal) > 0 && !$isServer) 已設定 {{ Str::limit($hVal, 60) }} @elseif ($isServer) {{ $label }} @if ($hasVersionLeak) 版本資訊外洩 @endif @else 未設定 @endif
{{-- SSL + Error Log --}}
SSL 憑證狀態
@if ($sslDays >= 0) @endif
到期日 @if ($sslDays >= 0) {{ $sslDate }} @else 未偵測到 HTTPS @endif
剩餘天數 @if ($sslDays <= 7) {{ $sslDays }} 天 @elseif ($sslDays <= 30) {{ $sslDays }} 天 @else {{ $sslDays }} 天 @endif
HTTP 回應碼 @php $code = intval($wsSec['http_code'] ?? 0); @endphp @if ($code >= 200 && $code < 400) {{ $code }} @elseif ($code >= 400) {{ $code }} @else 無回應 @endif @if (intval($wsSec['response_ms'] ?? 0) > 0) {{ $wsSec['response_ms'] }}ms @endif
錯誤日誌(最近 200 行)
@if ($errSamp) @endif
Error 筆數 @if ($errCount >= 50) {{ $errCount }} 筆 @elseif ($errCount > 0) {{ $errCount }} 筆 @else 無異常 @endif
範例 @foreach (explode('§', $errSamp) as $errLine) @if ($errLine)
{{ Str::limit($errLine, 120) }}
@endif @endforeach
@endif {{-- 12. 存取控制機制 --}}
12. 存取控制機制
機制類型{{ $reportData['mac_type'] ?? 'N/A' }}
狀態 @if (in_array(strtolower($reportData['mac_status'] ?? ''), ['enabled', 'enforcing'])) {{ $reportData['mac_status'] }} @else {{ $reportData['mac_status'] ?? 'N/A' }} @endif
{{-- 13. Log 管理設定 --}}
13. Log 管理設定
Logrotate (180天) @if (!empty($reportData['log_rotate_180days'])) 已設定 @else 未設定 @endif {{ $reportData['log_rotate_detail'] ?? '' }}
Apache Logrotate @if (!empty($reportData['apache_logrotate_180days'])) 已設定 @else 未設定 @endif {{ $reportData['apache_logrotate_detail'] ?? '' }}
{{-- 14. 螢幕保護設定 --}}
14. 螢幕保護設定
@php $rawTimeout = $reportData['screensaver_timeout'] ?? ''; $humanTimeout = $rawTimeout; if (is_string($rawTimeout) && preg_match('/uint32\s+(\d+)/', $rawTimeout, $matches)) { $seconds = intval($matches[1]); $humanTimeout = $seconds . ' 秒(約 ' . round($seconds / 60, 1) . ' 分鐘)'; } @endphp
螢幕保護啟用{{ !empty($reportData['screensaver_configured']) ? '已設定' : '未設定' }}
等待時間{{ $humanTimeout }}
{{-- end .row --}} {{-- 15. 帳號安全檢查(chage / sudo / 群組)--}} @if(!empty($reportData['accounts_detail']))
15. 帳號安全檢查 {{ count($reportData['accounts_detail']) }} 個帳號(UID≥1000 + root)
@foreach($reportData['accounts_detail'] as $acctDetail) @php $c = $acctDetail['chage'] ?? []; @endphp @endforeach
帳號 Sudo Shell 所屬群組 最後更改密碼 密碼到期 帳號到期 最大間隔 最小間隔 警告天數
{{ $acctDetail['user'] }} @if(!empty($acctDetail['is_sudo'])) sudo @else - @endif {{ basename($c['shell'] ?? '') ?: '-' }} {{ $acctDetail['groups'] ?? '-' }} {{ $c['last_change'] ?? 'N/A' }} {!! ($c['pw_expires'] ?? 'never') === 'never' ? 'never' : $c['pw_expires'] !!} {!! ($c['acct_expires'] ?? 'never') === 'never' ? 'never' : $c['acct_expires'] !!} {!! ($c['max_days'] ?? '99999') === '99999' ? '99999' : $c['max_days'] !!} {{ $c['min_days'] ?? 'N/A' }} {{ $c['warn_days'] ?? 'N/A' }}
@endif {{-- 16. 最近登入紀錄 --}} @if (!empty($reportData['last_logins']))
16. 最近登入紀錄(最近 20 筆) @if (!empty($reportData['failed_login_count'])) 失敗登入: @endif
{{ str_replace('§', "\n", $reportData['last_logins'] ?? '') }}
@endif {{-- 16. 對外開放 Port 清單 --}} @if (!empty($reportData['listening_ports_parsed']))
17. 對外開放 Port 清單 {{ count($reportData['listening_ports_parsed']) }} 個 Port
@foreach ($reportData['listening_ports_parsed'] as $p) @endforeach
協定Port服務監聽位址
{{ $p['protocol'] ?? '' }} {{ $p['port'] ?? '' }} {{ $p['service'] ?? $p['process'] ?? '-' }} {{ $p['address'] ?? '' }}
@endif {{-- 18. 系統排程(root / www-data) --}} @if(!empty($reportData['cron_jobs']))
19. 系統排程(root / www-data) {{ count($reportData['cron_jobs']) }} 筆
@foreach($reportData['cron_jobs'] as $cj) @endforeach
來源排程內容
{{ $cj['source'] }} {{ $cj['job'] }}
@endif {{-- 20. 子站 SSL 憑證資訊 --}} @if (!empty($reportData['apache_sites_certificates']['exists']))
20. 子站 SSL 憑證資訊
@foreach ($reportData['apache_sites_certificates']['details'] ?? [] as $site) @php $certExpiry = $site['certificate_expiry'] ?? ''; $certDays = -1; if ($certExpiry && $certExpiry !== 'N/A') { try { $exp = \Carbon\Carbon::parse($certExpiry)->startOfDay(); $certDays = (int) now()->startOfDay()->diffInDays($exp, false); } catch (\Throwable $e) {} } @endphp @endforeach
站台名稱憑證檔案到期日剩餘天數
{{ $site['name'] ?? 'N/A' }} {{ $site['certificate_file'] ?? 'N/A' }} {{ $certExpiry ?: 'N/A' }} @if ($certDays >= 0) @if ($certDays <= 7) {{ $certDays }} 天 @elseif ($certDays <= 30) {{ $certDays }} 天 @else {{ $certDays }} 天 @endif @elseif ($certDays < 0 && $certExpiry && $certExpiry !== 'N/A') 已過期 @else @endif
@endif {{-- 21. SSL/TLS 安全檢測 --}} @php $ws21 = $reportData['web_service'] ?? []; $hdr21 = $ws21['http_headers'] ?? []; $tls21 = $ws21['ssl_tls'] ?? []; $sslDays21 = isset($ws21['ssl_cert_days_left']) ? intval($ws21['ssl_cert_days_left']) : null; $wsActive21 = ($ws21['status'] ?? '') === 'active'; // TLS grade calculation $tlsGrade = null; if (!empty($tls21)) { $g = 'A'; if ($sslDays21 !== null && $sslDays21 <= 0) { $g = 'F'; } elseif (($tls21['weak_cipher'] ?? false)) { $g = 'C'; } elseif (($tls21['sslv3'] ?? false)) { $g = 'C'; } elseif (($tls21['tls10'] ?? false) || ($tls21['tls11'] ?? false)) { $g = 'B'; } elseif ( ($tls21['tls13'] ?? false) && ($tls21['forward_secrecy'] ?? false) && !empty($hdr21['hsts']) ) { $g = 'A+'; } $tlsGrade = $g; } $gradeColor = match($tlsGrade) { 'A+' => '#00c853', 'A' => '#43a047', 'B' => '#f9a825', 'C' => '#ef6c00', 'F' => '#c62828', default => '#9e9e9e', }; // HTTP header checks @endphp
21. SSL/TLS 安全分析 (Agent 本機檢測)
@if (!$wsActive21)
此主機未偵測到執行中的 Web 服務,略過 SSL 與標頭檢測。
@elseif (empty($tls21))
Agent 尚未回報 SSL/TLS 資料,請確認 Agent 已更新並重新執行。
@else
{{-- TLS 等級 --}} @if($tlsGrade)
TLS 等級
{{ $tlsGrade }}
@if($tlsGrade === 'A+') 最佳 @elseif($tlsGrade === 'A') 良好 @elseif($tlsGrade === 'B') 尚可(有舊協定) @elseif($tlsGrade === 'C') 不安全 @elseif($tlsGrade === 'F') 危險 @endif
@endif {{-- 協定支援 + 憑證細節 --}}
協定版本
@php $protos = [ 'SSLv3' => ['key'=>'sslv3', 'good'=>false], 'TLS 1.0' => ['key'=>'tls10', 'good'=>false], 'TLS 1.1' => ['key'=>'tls11', 'good'=>false], 'TLS 1.2' => ['key'=>'tls12', 'good'=>true], 'TLS 1.3' => ['key'=>'tls13', 'good'=>true], ]; @endphp
@foreach($protos as $pname => $pcfg) @php $supported = $tls21[$pcfg['key']] ?? false; $isGood = $pcfg['good']; $danger = $supported && !$isGood; $success = $supported && $isGood; @endphp
{{ $pname }} @if($supported) 支援 @else 不支援 @endif
@endforeach
@if(!empty($tls21))
@php $features = [ ['label'=>'前向保密 (FS)', 'val'=>$tls21['forward_secrecy']??false, 'good'=>true], ['label'=>'弱加密套件', 'val'=>$tls21['weak_cipher']??false, 'good'=>false], ]; @endphp @foreach($features as $feat) @php $fOk = $feat['good'] ? $feat['val'] : !$feat['val']; @endphp
{{ $feat['label'] }} {{ $feat['val'] ? '是' : '否' }}
@endforeach
@endif @if(!empty($tls21['cert_bits']) || !empty($tls21['cert_sig']) || !empty($tls21['cert_issuer']))
憑證技術細節
@if(!empty($tls21['cert_bits'])) @endif @if(!empty($tls21['cert_sig'])) @endif @if(!empty($tls21['cert_issuer'])) @endif
金鑰長度 {{ $tls21['cert_bits'] }} bit @if(intval($tls21['cert_bits']) < 2048) @elseif(intval($tls21['cert_bits']) >= 4096) @endif
簽章演算法 {{ $tls21['cert_sig'] }} @if(str_contains(strtolower($tls21['cert_sig']), 'sha1')) 已棄用 @endif
簽發機構 {{ $tls21['cert_issuer'] }}
@endif
{{-- 改善建議 --}} @php $suggestions = []; if (!($tls21['tls13'] ?? false)) $suggestions[] = [ 'level' => 'warning', 'icon' => 'bi-arrow-up-circle', 'title' => '啟用 TLS 1.3', 'body' => 'TLS 1.3 提供更快的握手速度與更強的安全性,且是取得 A+ 等級的必要條件。
Apache:SSLProtocol -all +TLSv1.2 +TLSv1.3
Nginx:ssl_protocols TLSv1.2 TLSv1.3;', ]; if (!($tls21['forward_secrecy'] ?? false)) $suggestions[] = [ 'level' => 'danger', 'icon' => 'bi-shield-exclamation', 'title' => '啟用前向保密(Forward Secrecy)', 'body' => '缺少前向保密表示若私鑰洩漏,過去所有加密流量均可被解密。需使用 ECDHE 或 DHE 加密套件。
Apache:SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
同時加上 SSLSessionTickets off', ]; if ($tls21['weak_cipher'] ?? false) $suggestions[] = [ 'level' => 'danger', 'icon' => 'bi-exclamation-octagon', 'title' => '停用弱加密套件', 'body' => '目前伺服器支援已知不安全的加密套件(如 RC4、3DES、NULL、EXPORT 等)。應限制為 ECDHE + AES-GCM 或 ChaCha20-Poly1305 的現代套件。', ]; if ($tls21['sslv3'] ?? false) $suggestions[] = [ 'level' => 'danger', 'icon' => 'bi-x-octagon', 'title' => '停用 SSLv3', 'body' => 'SSLv3 存在嚴重安全漏洞(POODLE 攻擊),必須明確停用。
Apache:SSLProtocol -all +TLSv1.2 +TLSv1.3', ]; if (($tls21['tls10'] ?? false) || ($tls21['tls11'] ?? false)) $suggestions[] = [ 'level' => 'warning', 'icon' => 'bi-exclamation-triangle', 'title' => '停用 TLS 1.0 / TLS 1.1', 'body' => 'TLS 1.0 與 TLS 1.1 已於 2021 年由 RFC 8996 正式棄用,應停用以避免 BEAST、POODLE 等攻擊。
Apache:SSLProtocol -all +TLSv1.2 +TLSv1.3', ]; if (empty($hdr21['hsts'])) $suggestions[] = [ 'level' => 'info', 'icon' => 'bi-lock', 'title' => '加入 HSTS 標頭', 'body' => 'HSTS(HTTP Strict Transport Security)強制瀏覽器只使用 HTTPS 連線,防止 SSL Stripping 攻擊,也是取得 A+ 等級的必要條件。
Apache:Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"', ]; if ($sslDays21 !== null && $sslDays21 <= 30 && $sslDays21 > 0) $suggestions[] = [ 'level' => 'warning', 'icon' => 'bi-calendar-x', 'title' => 'SSL 憑證即將到期', 'body' => "憑證將於 {$sslDays21} 天後到期,請儘快更新以避免服務中斷。建議設定自動續期(如 certbot renew)。", ]; if ($sslDays21 !== null && $sslDays21 <= 0) $suggestions[] = [ 'level' => 'danger', 'icon' => 'bi-calendar-x-fill', 'title' => 'SSL 憑證已到期', 'body' => '憑證已過期,瀏覽器將顯示安全警告,使用者無法正常連線。請立即更新憑證。', ]; @endphp @if(count($suggestions) > 0)
改善建議
@foreach($suggestions as $sg) @php $sgColor = match($sg['level']) { 'danger' => ['border'=>'#dc3545','bg'=>'#fff5f5','icon'=>'text-danger','badge'=>'bg-danger'], 'warning' => ['border'=>'#fd7e14','bg'=>'#fff8f0','icon'=>'text-warning','badge'=>'bg-warning text-dark'], default => ['border'=>'#0dcaf0','bg'=>'#f0fbfe','icon'=>'text-info', 'badge'=>'bg-info text-dark'], }; @endphp
{{ $sg['title'] }}
{!! $sg['body'] !!}
@endforeach
@endif @endif
{{-- 22. 掃毒結果 --}}
22. 掃毒結果(ClamAV) @if($host->av_last_scan_at) 最後掃描:{{ $host->av_last_scan_at->format('Y-m-d H:i') }} @endif
@if(!$host->av_last_scan)

尚未有掃毒紀錄,Agent 將於每週六 02:00 自動執行掃描。

@else @php $av = $host->av_last_scan; $avStatus = $av['status'] ?? 'unknown'; $avBadge = match($avStatus) { 'clean' => ['bg-success', '乾淨'], 'infected' => ['bg-danger', '發現威脅'], 'error' => ['bg-secondary','掃描錯誤'], default => ['bg-secondary', $avStatus], }; $avDur = $av['duration_seconds'] ?? null; $avDurStr = $avDur !== null ? ($avDur >= 3600 ? sprintf('%d 時 %d 分 %d 秒', intdiv($avDur,3600), intdiv($avDur%3600,60), $avDur%60) : ($avDur >= 60 ? sprintf('%d 分 %d 秒', intdiv($avDur,60), $avDur%60) : $avDur.' 秒')) : null; @endphp
狀態
{{ $avBadge[1] }}
掃描目錄數
{{ number_format($av['dirs_scanned'] ?? 0) }}
掃描檔案數
{{ number_format($av['files_scanned'] ?? 0) }}
發現威脅
{{ $av['infected'] ?? 0 }}
@if($avDurStr) @endif
開始時間{{ $av['started_at'] ?? $av['scanned_at'] ?? 'N/A' }}
結束時間{{ $av['finished_at'] ?? 'N/A' }}
掃描耗時{{ $avDurStr }}
掃描路徑{{ implode('、', $av['paths'] ?? []) }}
掃描引擎{{ $av['engine'] ?? 'N/A' }}
@if(!empty($av['threats']))
發現 {{ count($av['threats']) }} 個威脅:
    @foreach($av['threats'] as $t)
  • {{ $t['file'] ?? '' }}{{ $t['virus'] ?? '' }}
  • @endforeach
@else

未發現任何威脅。

@endif @if(isset($av['error']))

{{ $av['error'] }}

@endif @endif
{{-- 23. 備份狀態(總覽)--}} @php $backups24 = collect($reportData['backup'] ?? []); @endphp
23. 備份狀態
@if($backups24->isNotEmpty()) @php $allOk = $backups24->every(fn($b) => ($b['dir_found'] ?? false) && ($b['last_backup_hours'] ?? -1) >= 0 && ($b['last_backup_hours'] ?? 999) <= 25 ); $anyWarn = $backups24->contains(fn($b) => ($b['dir_found'] ?? false) && ($b['last_backup_hours'] ?? -1) > 25 ); @endphp {{ $allOk ? '正常' : ($anyWarn ? '部分異常' : '未偵測到備份') }} @endif
@if($backups24->isEmpty())

Agent 尚未回報備份資料,請更新並重新執行 Agent。

@else
@foreach($backups24 as $bk) @php $bkHours = $bk['last_backup_hours'] ?? -1; $bkDirOk = $bk['dir_found'] ?? false; $bkHasFile = $bkHours >= 0; if (!$bkDirOk) { $bkStatus = ['danger', 'bi-x-circle', '未設定備份目錄']; } elseif (!$bkHasFile) { $bkStatus = ['danger', 'bi-x-circle', '目錄為空']; } elseif ($bkHours <= 25) { $bkStatus = ['success', 'bi-check-circle', '正常('.($bkHours < 1 ? '剛完成' : $bkHours.'h 前').')']; } elseif ($bkHours <= 48) { $bkStatus = ['warning', 'bi-exclamation-circle', '逾時('.$bkHours.'h 前)']; } else { $bkStatus = ['danger', 'bi-x-circle', '備份失敗('.$bkHours.'h 前)']; } @endphp @endforeach
備份類型 腳本 排程 備份目錄 最後備份時間 24h 檔案數 目錄大小 狀態
{{ $bk['type'] === 'data' ? '資料備份' : '系統備份' }} @if($bk['script_found'] ?? false) 存在
{{ $bk['script_path'] ?? '' }}
@else 未找到 @endif
@if($bk['in_cron'] ?? false) 已排程 @else 未排程 @endif @if($bkDirOk) 存在
{{ $bk['dir_path'] ?? '' }}
@else 未找到 @endif
@if($bkHasFile) {{ $bk['last_backup'] ?? '—' }} @else @endif @if($bkDirOk) {{ $bk['files_24h'] ?? 0 }} @else @endif {{ $bk['size'] ?? '—' }} {{ $bkStatus[2] }}
@endif {{-- 備份狀態檢查(每日詳細記錄)--}} @if(!empty($reportData['backup_check']))
備份狀態檢查(每日詳細記錄)
@foreach($reportData['backup_check'] as $bkIdx => $bk) @if(!empty($bk['subdirs']))
{{-- 目錄標題 --}}
{{ $bk['path'] }} ({{ $bk['size'] }})
@foreach($bk['subdirs'] as $sdIdx => $sub) @php $dp = $sub['daily_pass'] ?? 0; $dt = $sub['daily_total'] ?? 30; $ratio = $dt > 0 ? $dp / $dt : 0; $daysAgo = $sub['days_ago'] ?? 999; $colId = 'bkd_'.$bkIdx.'_'.$sdIdx; // 找最近一次有備份的那天的檔案清單 $latestFiles = []; foreach (array_reverse($sub['daily_check'] ?? []) as $_dc) { $_isOk = is_array($_dc) ? ($_dc['ok'] ?? false) : (bool)$_dc; if ($_isOk) { $latestFiles = is_array($_dc) ? ($_dc['files'] ?? []) : []; break; } } @endphp {{-- 摘要列 --}} {{-- 展開:每日明細 --}} @endforeach
子目錄 大小 檔案清單(前次備份) 距今 近 {{ $bk['subdirs'][0]['daily_total'] ?? 30 }} 天備份 詳細
{{ $sub['name'] }} {{ $sub['size'] }} @if(!empty($latestFiles)) @foreach($latestFiles as $fname)
{{ $fname }}
@endforeach @elseif(!empty($sub['newest_file'])) {{ $sub['newest_file'] }} @else @endif
@if($daysAgo > 3) {{ $daysAgo }} 天前 @elseif($daysAgo > 1) {{ $daysAgo }} 天前 @else {{ $daysAgo <= 0 ? '今天' : '昨天' }} @endif
{{ $dp }}/{{ $dt }} {{-- 日曆方格(假日同樣需備份,缺少一律紅色)--}}
@foreach(($sub['daily_check'] ?? []) as $date => $dayData) @php $ok = is_array($dayData) ? ($dayData['ok'] ?? false) : (bool)$dayData; $cnt = is_array($dayData) ? ($dayData['count'] ?? 0) : 0; $szB = is_array($dayData) ? ($dayData['size'] ?? 0) : 0; if ($szB >= 1073741824) $szStr = number_format($szB/1073741824, 1).' GB'; elseif ($szB >= 1048576) $szStr = number_format($szB/1048576, 1).' MB'; elseif ($szB >= 1024) $szStr = number_format($szB/1024, 0).' KB'; else $szStr = $szB > 0 ? $szB.' B' : '—'; $dayNum = (int)substr($date, 8); $tip = $date.' '.(\Carbon\Carbon::parse($date)->isoFormat('(ddd)')).'\n備份 '.$cnt.' 個 / '.$szStr; @endphp
{{ $dayNum }}
@endforeach
{{ $sub['name'] }} — 每日備份明細(近 {{ $dt }} 天)
@foreach(array_reverse(array_keys($sub['daily_check'] ?? [])) as $date) @php $dayData = $sub['daily_check'][$date]; $ok = is_array($dayData) ? ($dayData['ok'] ?? false) : (bool)$dayData; $cnt = is_array($dayData) ? ($dayData['count'] ?? 0) : 0; $szB = is_array($dayData) ? ($dayData['size'] ?? 0) : 0; $files = is_array($dayData) ? ($dayData['files'] ?? []) : []; if ($szB >= 1073741824) $szStr = number_format($szB/1073741824, 2).' GB'; elseif ($szB >= 1048576) $szStr = number_format($szB/1048576, 1).' MB'; elseif ($szB >= 1024) $szStr = number_format($szB/1024, 0).' KB'; else $szStr = $szB > 0 ? $szB.' B' : '—'; $carbonDate = \Carbon\Carbon::parse($date); $isToday = $carbonDate->isToday(); @endphp @endforeach
日期 星期 數量 大小 備份檔案 狀態
{{ $date }}{{ $isToday ? ' ★' : '' }} {{ $carbonDate->isoFormat('ddd') }} {{ $cnt > 0 ? $cnt.' 個' : '—' }} {{ $szB > 0 ? $szStr : '—' }} @if(!empty($files)) @foreach($files as $fname)
{{ $fname }}
@endforeach @else @endif
@if($ok) 有備份 @else 無備份 @endif
@else
{{ $bk['path'] }} 無子目錄
@endif @endforeach
@endif
{{-- 24. Log 完整性檢查 --}} @php $li = $reportData['log_integrity'] ?? null; @endphp
24. Log 完整性檢查 @if($li) @php $liStatus = $li['tampered'] > 0 ? ['danger', '發現異動'] : ($li['checked'] > 0 ? ['success', '正常'] : ['secondary', '尚無基準']); @endphp {{ $liStatus[1] }} @endif
@if(!$li)

Agent 尚未回報 Log 完整性資料,請更新並重新執行 Agent。

@else {{-- Summary stats --}}
檢查檔案數
{{ $li['checked'] ?? 0 }}
正常
{{ $li['clean'] ?? 0 }}
新增檔案
{{ $li['new_files'] ?? 0 }}
疑似異動
{{ $li['tampered'] ?? 0 }}
{{-- Per-category table --}} @if(!empty($li['categories']))
各類別統計 @if(!empty($li['files'])) @endif
@foreach($li['categories'] as $cat) @php $catTampered = $cat['tampered'] ?? 0; $catNew = $cat['new_files'] ?? 0; $catChecked = $cat['checked'] ?? 0; if ($catTampered > 0) [$catBadge, $catLabel] = ['danger', '異動']; elseif ($catChecked === 0) [$catBadge, $catLabel] = ['secondary','—']; elseif ($catNew > 0) [$catBadge, $catLabel] = ['info', '新增']; else [$catBadge, $catLabel] = ['success', '正常']; @endphp @endforeach
日誌類別 檢查 正常 新增 異動 狀態
{{ $cat['name'] ?? '—' }} {{ $catChecked }} {{ $cat['clean'] ?? 0 }} {{ $catNew > 0 ? $catNew : '—' }} {{ $catTampered > 0 ? $catTampered : '—' }} {{ $catLabel }}
{{-- Full file detail collapsible --}} @if(!empty($li['files'])) @php $liFilesByCat = collect($li['files'])->groupBy('cat'); @endphp
完整檢查紀錄(共 {{ count($li['files']) }} 個檔案)
@foreach($liFilesByCat as $catName => $catFiles)
{{ $catName }} ({{ count($catFiles) }} 個)
@foreach($catFiles as $f) @php $fStatus = $f['status'] ?? 'clean'; [$fBadge, $fLabel] = match($fStatus) { 'tampered' => ['danger', '異動'], 'new' => ['info', '新增'], default => ['success', '正常'], }; @endphp @endforeach
{{ $f['path'] ?? '' }} {{ $fLabel }}
@endforeach
@endif @endif
檢查時間:{{ $li['checked_at'] ?? '—' }}  |  僅比對非今日的輪替 log 檔(.1、.gz 等) @if(($li['tampered'] ?? 0) > 0) 需人工確認 @endif
{{-- Tampered files collapsible --}} @if(!empty($li['tampered_files']))
發現 {{ count($li['tampered_files']) }} 個 Log 檔 MD5 與基準不符
    @foreach($li['tampered_files'] as $tf)
  • {{ $tf }}
  • @endforeach
若為正常輪替造成,可刪除 /opt/host-agent/.log_hashes 重建基準。
@elseif(($li['checked'] ?? 0) === 0)
未找到可檢查的輪替 Log 檔案(可能 logrotate 尚未執行或路徑不符)。
@else
所有已知輪替 Log 檔案 MD5 均與基準一致,未發現異動。
@endif @endif
{{-- 25. SBOM 軟體清單 --}}
25. SBOM 軟體清單(Software Bill of Materials) @if ($host->sbom_collected_at) 最後收集:{{ $host->sbom_collected_at->format('Y-m-d H:i') }} @endif
@if (!$host->sbom_data)
尚未收集 SBOM 資料,Agent 將於每天首次執行時自動收集。
@else @php $sbom = $host->sbom_data; $aptPkgs = $sbom['apt'] ?? []; $pipPkgs = $sbom['pip'] ?? []; $npmPkgs = $sbom['npm'] ?? []; $compPkgs = $sbom['composer'] ?? []; $allPkgs = array_merge( array_map(fn($p) => array_merge($p, ['type'=>'apt']), $aptPkgs), array_map(fn($p) => array_merge($p, ['type'=>'pip']), $pipPkgs), array_map(fn($p) => array_merge($p, ['type'=>'npm']), $npmPkgs), array_map(fn($p) => array_merge($p, ['type'=>'composer']), $compPkgs), ); @endphp {{-- 統計卡 --}}
@foreach ([['apt','APT 套件','bg-primary',count($aptPkgs)],['pip','Python pip','bg-success',count($pipPkgs)],['npm','Node npm','bg-warning text-dark',count($npmPkgs)],['composer','PHP Composer','bg-info text-dark',count($compPkgs)]] as [$t,$label,$cls,$cnt]) @if ($cnt > 0)
{{ $cnt }}
{{ $label }}
@endif @endforeach
{{-- 搜尋 + 篩選 --}}
{{-- 套件表格 --}}
@foreach ($allPkgs as $pkg) @php $lic = $pkg['license'] ?? ''; $desc = $pkg['description'] ?? ''; $licBadge = match(true) { empty($lic) || $lic === 'unknown' => ['bg-secondary', '—'], str_contains(strtolower($lic), 'mit') => ['bg-success text-white', $lic], str_contains(strtolower($lic), 'apache') => ['bg-primary text-white', $lic], str_contains(strtolower($lic), 'gpl') => ['bg-warning text-dark', $lic], str_contains(strtolower($lic), 'bsd') => ['bg-info text-dark', $lic], str_contains(strtolower($lic), 'lgpl') => ['bg-warning text-dark', $lic], str_contains(strtolower($lic), 'isc') => ['bg-success text-white', $lic], default => ['bg-light text-dark border', $lic], }; $typeBadge = match($pkg['type']) { 'apt' => 'bg-primary', 'pip' => 'bg-success', 'npm' => 'bg-warning text-dark', 'composer' => 'bg-info text-dark', default => 'bg-secondary', }; @endphp @php $descZh = $pkg['description_zh'] ?? ''; @endphp @endforeach
套件名稱 版本 類型 授權 描述
{{ $pkg['name'] }} {{ $pkg['version'] }} {{ $pkg['type'] }} {{ Str::limit($licBadge[1], 30) }} @if($descZh) {{ Str::limit($descZh, 60) }} @if($desc)
{{ $desc }}
@endif @elseif($desc) {{ $desc }} @endif
@endif
{{-- 26. SBOM × CVE 漏洞比對 --}}
26. SBOM 套件漏洞比對 @if($host->sbom_vuln_scanned_at) 比對時間:{{ $host->sbom_vuln_scanned_at->format('Y-m-d H:i') }} @endif
@if(!$host->sbom_vuln_scanned_at)

尚未執行比對。Agent 提交新 SBOM 時會自動觸發,或由管理員執行 php artisan sbom:scan

@elseif(empty($host->sbom_vuln_data))

比對完成,在 CVE 資料庫中未發現有風險的套件。

@else @php $vulns = $host->sbom_vuln_data; $critical = collect($vulns)->where('max_severity', 'CRITICAL')->count(); $high = collect($vulns)->where('max_severity', 'HIGH')->count(); $medium = collect($vulns)->where('max_severity', 'MEDIUM')->count(); $low = collect($vulns)->where('max_severity', 'LOW')->count(); @endphp {{-- 摘要統計 --}}
CRITICAL
{{ $critical }}
HIGH
{{ $high }}
MEDIUM
{{ $medium }}
LOW
{{ $low }}
{{-- Backport 免責說明 --}} @php $hasDistroPatched = collect($vulns)->contains('is_distro_patched', true); @endphp @if($hasDistroPatched)
Ubuntu / Debian 回移修補(Backport)注意事項
標有 回移 的套件版本含 ubuntudeb 後綴, 代表 Ubuntu / Debian 安全團隊可能已將漏洞補丁回移至該版本, 即使套件版本號低於上游修復版本,漏洞實際上已修補。
建議在採取行動前,先至 Ubuntu Security Packages 確認是否已有對應 USN(Ubuntu Security Notice)。
@endif {{-- 套件列表 --}}
@foreach($vulns as $vi => $vuln) @php $sevColor = match($vuln['max_severity']) { 'CRITICAL' => 'danger', 'HIGH' => 'warning', 'MEDIUM' => 'info', default => 'secondary', }; $colId = 'vuln_' . $vi; $isDistro = !empty($vuln['is_distro_patched']); @endphp {{-- 展開:CVE 清單 --}} @endforeach
套件名稱 目前版本 來源 最高風險 CVE 數 詳細
{{ $vuln['name'] }} {{ $vuln['version'] ?: '—' }} @if($isDistro) 回移 @endif {{ $vuln['ecosystem'] }} {{ $vuln['max_severity'] }}
{{ number_format($vuln['max_score'], 1) }}
{{ $vuln['cve_count'] }}
@foreach($vuln['cves'] as $cve) @php $cs = match($cve['severity']) { 'CRITICAL' => 'danger', 'HIGH' => 'warning', 'MEDIUM' => 'info', default => 'secondary', }; $maybeBp = !empty($cve['maybe_backported']); @endphp @endforeach
CVE ID 嚴重性 分數 建議版本 發布日期 描述
{{ $cve['cve_id'] }} @if($maybeBp) 可能已回移 @endif {{ $cve['severity'] }} {{ number_format($cve['score'], 1) }} @if(!empty($cve['fixed_ver'])) ≥ {{ $cve['fixed_ver'] }} @else @endif {{ $cve['published'] ?? '—' }} {{ $cve['description'] }}
@endif
{{-- 27. 弱點掃描(Nuclei) --}}
27. 弱點掃描(OWASP Top 10) 新增掃描
@php $hostScans = \App\Models\VulnScan::where('host_id', $host->id) ->orderByDesc('created_at')->take(5)->get(); @endphp @if($hostScans->isEmpty())
尚未對此主機執行弱點掃描。點擊右上角「新增掃描」開始。
@else
@foreach($hostScans as $scan) @php $bySev = $scan->summary['by_severity'] ?? []; @endphp @endforeach
目標 URL Profile 狀態 C H M L 時間
{{ $scan->target_url }} {{ $scan->profile }} @if($scan->status === 'done') 完成 @elseif($scan->status === 'running') 執行中 @elseif($scan->status === 'failed') 失敗 @else 待執行 @endif @if(($bySev['critical']??0)>0){{$bySev['critical']}}@else-@endif @if(($bySev['high']??0)>0){{$bySev['high']}}@else-@endif @if(($bySev['medium']??0)>0){{$bySev['medium']}}@else-@endif @if(($bySev['low']??0)>0){{$bySev['low']}}@else-@endif {{ $scan->created_at->format('m-d H:i') }}
@if(\App\Models\VulnScan::where('host_id', $host->id)->count() > 5) @endif @endif
@endsection @push('scripts') @endpush