{{-- ===== 頁首:標題 + 主機摘要 ===== --}}
{{ $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
@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
@foreach ($secChecks as $chk)
{{ $chk['name'] }}
{{ $chk['pass'] ? 'PASS' : 'FAIL' }}
@endforeach
{{-- ====================================================
1-20 詳細檢查項目
===================================================== --}}
{{-- 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. 硬體資訊 --}}
| 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
|
@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
|
{{ $disk['mounted_on'] ?? '?' }}
@if($pct >= $alertThreshold)
@endif
|
已用 {{ $disk['used'] ?? '?' }} / 總計 {{ $disk['size'] ?? '?' }}
{{ $pct }}%
{{ $disk['filesystem'] ?? '' }} 剩餘 {{ $disk['avail'] ?? '?' }}
|
@empty
| 磁碟 | N/A |
@endforelse
@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. 網路與服務資訊 --}}
@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)
| {{ $name }} | {{ $ver }} |
@endforeach
| NTP 校時 |
@if (($reportData['ntp_status'] ?? '') === 'active')
已啟用
@else
{{ $reportData['ntp_status'] ?? 'N/A' }}
@endif
@if (!empty($reportData['ntp_servers']))
{{ implode(', ', $reportData['ntp_servers']) }}
@endif
|
{{-- 4. 帳號管理(僅特權帳號摘要) --}}
特權帳號(sudo):
@foreach (($reportData['privileged_accounts'] ?? []) as $acct)
{{ $acct }}
@endforeach
@if (empty($reportData['privileged_accounts'])) N/A @endif
詳細帳號安全檢查請見第 15 項
{{-- 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. 防火牆設定 --}}
類型:{{ 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 安全設定 --}}
| 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. 密碼政策 --}}
| 最長有效天數 | {{ $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 入侵防護 --}}
狀態:
@if (($reportData['fail2ban_status'] ?? '') === 'active')
已啟用
@else
{{ $reportData['fail2ban_status'] ?? 'N/A' }}
@endif
@if (!empty($reportData['fail2ban_jails']))
啟用的 Jail
{{ $reportData['fail2ban_jails'] }}
@endif
{{-- 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
| 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
| 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
{{-- 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
| {{ $hName }} |
@if (strlen($hVal) > 0 && !$isServer)
已設定
{{ Str::limit($hVal, 60) }}
@elseif ($isServer)
{{ $label }}
@if ($hasVersionLeak)
版本資訊外洩
@endif
@else
未設定
@endif
|
@endforeach
{{-- SSL + Error Log --}}
SSL 憑證狀態
| 到期日 |
@if ($sslDays >= 0)
{{ $sslDate }}
@else
未偵測到 HTTPS
@endif
|
@if ($sslDays >= 0)
| 剩餘天數 |
@if ($sslDays <= 7)
{{ $sslDays }} 天
@elseif ($sslDays <= 30)
{{ $sslDays }} 天
@else
{{ $sslDays }} 天
@endif
|
@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 行)
| Error 筆數 |
@if ($errCount >= 50)
{{ $errCount }} 筆
@elseif ($errCount > 0)
{{ $errCount }} 筆
@else
無異常
@endif
|
@if ($errSamp)
| 範例 |
@foreach (explode('§', $errSamp) as $errLine)
@if ($errLine)
{{ Str::limit($errLine, 120) }}
@endif
@endforeach
|
@endif
@endif
{{-- 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 管理設定 --}}
| 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. 螢幕保護設定 --}}
@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']))
| 帳號 |
Sudo |
Shell |
所屬群組 |
最後更改密碼 |
密碼到期 |
帳號到期 |
最大間隔 |
最小間隔 |
警告天數 |
@foreach($reportData['accounts_detail'] as $acctDetail)
@php $c = $acctDetail['chage'] ?? []; @endphp
| {{ $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' }} |
@endforeach
@endif
{{-- 16. 最近登入紀錄 --}}
@if (!empty($reportData['last_logins']))
{{ str_replace('§', "\n", $reportData['last_logins'] ?? '') }}
@endif
{{-- 16. 對外開放 Port 清單 --}}
@if (!empty($reportData['listening_ports_parsed']))
| 協定 | Port | 服務 | 監聽位址 |
@foreach ($reportData['listening_ports_parsed'] as $p)
| {{ $p['protocol'] ?? '' }} |
{{ $p['port'] ?? '' }} |
{{ $p['service'] ?? $p['process'] ?? '-' }} |
{{ $p['address'] ?? '' }} |
@endforeach
@endif
{{-- 18. 系統排程(root / www-data) --}}
@if(!empty($reportData['cron_jobs']))
| 來源 | 排程內容 |
@foreach($reportData['cron_jobs'] as $cj)
{{ $cj['source'] }} |
{{ $cj['job'] }} |
@endforeach
@endif
{{-- 20. 子站 SSL 憑證資訊 --}}
@if (!empty($reportData['apache_sites_certificates']['exists']))
| 站台名稱 | 憑證檔案 | 到期日 | 剩餘天數 |
@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
| {{ $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
|
@endforeach
@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
@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']))
| 金鑰長度 |
{{ $tls21['cert_bits'] }} bit
@if(intval($tls21['cert_bits']) < 2048)
弱
@elseif(intval($tls21['cert_bits']) >= 4096)
強
@endif
|
@endif
@if(!empty($tls21['cert_sig']))
| 簽章演算法 |
{{ $tls21['cert_sig'] }}
@if(str_contains(strtolower($tls21['cert_sig']), 'sha1'))
已棄用
@endif
|
@endif
@if(!empty($tls21['cert_issuer']))
| 簽發機構 |
{{ $tls21['cert_issuer'] }} |
@endif
@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. 掃毒結果 --}}
@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
掃描目錄數
{{ number_format($av['dirs_scanned'] ?? 0) }}
掃描檔案數
{{ number_format($av['files_scanned'] ?? 0) }}
發現威脅
{{ $av['infected'] ?? 0 }}
| 開始時間 | {{ $av['started_at'] ?? $av['scanned_at'] ?? 'N/A' }} |
| 結束時間 | {{ $av['finished_at'] ?? 'N/A' }} |
@if($avDurStr)
| 掃描耗時 | {{ $avDurStr }} |
@endif
| 掃描路徑 | {{ 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
@if($backups24->isEmpty())
Agent 尚未回報備份資料,請更新並重新執行 Agent。
@else
| 備份類型 |
腳本 |
排程 |
備份目錄 |
最後備份時間 |
24h 檔案數 |
目錄大小 |
狀態 |
@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
| {{ $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] }}
|
@endforeach
@endif
{{-- 備份狀態檢查(每日詳細記錄)--}}
@if(!empty($reportData['backup_check']))
備份狀態檢查(每日詳細記錄)
@foreach($reportData['backup_check'] as $bkIdx => $bk)
@if(!empty($bk['subdirs']))
{{-- 目錄標題 --}}
{{ $bk['path'] }}
({{ $bk['size'] }})
| 子目錄 |
大小 |
檔案清單(前次備份) |
距今 |
近 {{ $bk['subdirs'][0]['daily_total'] ?? 30 }} 天備份 |
詳細 |
@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
{{-- 摘要列 --}}
| {{ $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
| {{ $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
|
@endforeach
|
@endforeach
@else
{{ $bk['path'] }} 無子目錄
@endif
@endforeach
@endif
{{-- 24. Log 完整性檢查 --}}
@php
$li = $reportData['log_integrity'] ?? null;
@endphp
@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
| {{ $cat['name'] ?? '—' }} |
{{ $catChecked }} |
{{ $cat['clean'] ?? 0 }} |
{{ $catNew > 0 ? $catNew : '—' }} |
{{ $catTampered > 0 ? $catTampered : '—' }} |
{{ $catLabel }} |
@endforeach
{{-- 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
{{ $f['path'] ?? '' }} |
{{ $fLabel }}
|
@endforeach
@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 軟體清單 --}}
@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)
@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
| {{ $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
|
@endforeach
@endif
{{-- 26. SBOM × CVE 漏洞比對 --}}
@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
{{-- 摘要統計 --}}
{{-- Backport 免責說明 --}}
@php $hasDistroPatched = collect($vulns)->contains('is_distro_patched', true); @endphp
@if($hasDistroPatched)
Ubuntu / Debian 回移修補(Backport)注意事項
標有
回移 的套件版本含
ubuntu 或
deb 後綴,
代表 Ubuntu / Debian 安全團隊可能已將漏洞補丁回移至該版本,
即使套件版本號低於上游修復版本,漏洞實際上已修補。
建議在採取行動前,先至
Ubuntu Security Packages
確認是否已有對應 USN(Ubuntu Security Notice)。
@endif
{{-- 套件列表 --}}
| 套件名稱 |
目前版本 |
來源 |
最高風險 |
CVE 數 |
詳細 |
@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
| {{ $vuln['name'] }} |
{{ $vuln['version'] ?: '—' }}
@if($isDistro)
回移
@endif
|
{{ $vuln['ecosystem'] }} |
{{ $vuln['max_severity'] }}
{{ number_format($vuln['max_score'], 1) }}
|
{{ $vuln['cve_count'] }} |
|
{{-- 展開:CVE 清單 --}}
| CVE ID |
嚴重性 |
分數 |
建議版本 |
發布日期 |
描述 |
@foreach($vuln['cves'] as $cve)
@php
$cs = match($cve['severity']) {
'CRITICAL' => 'danger',
'HIGH' => 'warning',
'MEDIUM' => 'info',
default => 'secondary',
};
$maybeBp = !empty($cve['maybe_backported']);
@endphp
|
{{ $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'] }} |
@endforeach
|
@endforeach
@endif
{{-- 27. 弱點掃描(Nuclei) --}}
@php
$hostScans = \App\Models\VulnScan::where('host_id', $host->id)
->orderByDesc('created_at')->take(5)->get();
@endphp
@if($hostScans->isEmpty())
尚未對此主機執行弱點掃描。點擊右上角「新增掃描」開始。
@else
| 目標 URL |
Profile |
狀態 |
C |
H |
M |
L |
時間 |
|
@foreach($hostScans as $scan)
@php $bySev = $scan->summary['by_severity'] ?? []; @endphp
| {{ $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') }} |
|
@endforeach
@if(\App\Models\VulnScan::where('host_id', $host->id)->count() > 5)
@endif
@endif
@endsection
@push('scripts')
@endpush