diff --git a/web/src/pages/Agents/AgentDetail.tsx b/web/src/pages/Agents/AgentDetail.tsx index b25cebf..0129bb1 100644 --- a/web/src/pages/Agents/AgentDetail.tsx +++ b/web/src/pages/Agents/AgentDetail.tsx @@ -1,21 +1,35 @@ import {useEffect, useState} from 'react'; -import {useNavigate, useParams} from 'react-router-dom'; -import type {MenuProps} from 'antd'; -import {App, Button, Card, Col, Descriptions, Dropdown, Row, Space, Spin, Tag} from 'antd'; -import {Activity, ArrowLeft, Clock, RefreshCw, Shield, Terminal} from 'lucide-react'; +import {useNavigate, useParams, useSearchParams} from 'react-router-dom'; +import type {MenuProps, TabsProps} from 'antd'; +import {App, Button, Card, Col, Descriptions, Dropdown, Row, Space, Spin, Tag, Tabs, Alert, Statistic} from 'antd'; +import { + Activity, + ArrowLeft, + Clock, + RefreshCw, + Shield, + Terminal, + CheckCircle, + XCircle, + AlertTriangle, + TrendingUp +} from 'lucide-react'; import {getAgentForAdmin, getAuditResult, sendAuditCommand, type VPSAuditResult} from '../../api/agent'; import type {Agent} from '../../types'; import dayjs from 'dayjs'; import {getErrorMessage} from '../../lib/utils'; +import AuditResultView from './AuditResultView'; const AgentDetail = () => { const {id} = useParams<{ id: string }>(); const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); const {message: messageApi} = App.useApp(); const [loading, setLoading] = useState(false); const [agent, setAgent] = useState(null); const [auditResult, setAuditResult] = useState(null); const [auditing, setAuditing] = useState(false); + const [activeTab, setActiveTab] = useState(searchParams.get('tab') || 'info'); const fetchData = async () => { if (!id) return; @@ -39,6 +53,12 @@ const AgentDetail = () => { const handleStartAudit = async () => { if (!id) return; + // 检查是否为 Linux 系统 + if (!agent?.os.toLowerCase().includes('linux')) { + messageApi.warning('安全审计功能仅支持 Linux 系统'); + return; + } + setAuditing(true); try { await sendAuditCommand(id); @@ -59,6 +79,11 @@ const AgentDetail = () => { fetchData(); }, [id]); + useEffect(() => { + // 同步 activeTab 到 URL + setSearchParams({tab: activeTab}); + }, [activeTab]); + if (loading && !agent) { return (
@@ -67,7 +92,7 @@ const AgentDetail = () => { ); } - // 命令菜单配置 - 方便扩展新命令 + // 命令菜单配置 const commandMenuItems: MenuProps['items'] = [ { key: 'audit', @@ -84,19 +109,200 @@ const AgentDetail = () => { label: '刷新数据', onClick: fetchData, }, - // 可以在这里添加更多命令 - // { - // key: 'restart', - // icon: , - // label: '重启探针', - // onClick: handleRestartAgent, - // }, - // { - // key: 'update', - // icon: , - // label: '更新探针', - // onClick: handleUpdateAgent, - // }, + ]; + + // 计算审计统计 + const passCount = auditResult?.securityChecks.filter(c => c.status === 'pass').length || 0; + const failCount = auditResult?.securityChecks.filter(c => c.status === 'fail').length || 0; + const warnCount = auditResult?.securityChecks.filter(c => c.status === 'warn').length || 0; + const totalCount = auditResult?.securityChecks.length || 0; + + // Tab 项配置 + const tabItems: TabsProps['items'] = [ + { + key: 'info', + label: ( + + + 基本信息 + + ), + children: ( + + {agent?.name} + {agent?.id} + {agent?.hostname} + {agent?.ip} + + {agent?.os} + + + {agent?.arch} + + + {agent?.platform ? {agent.platform} : '-'} + + + {agent?.location ? {agent.location} : '-'} + + + {agent?.expireTime ? ( +
+
{new Date(agent.expireTime).toLocaleDateString('zh-CN')}
+ {(() => { + const expireDate = new Date(agent.expireTime); + const now = new Date(); + const isExpired = expireDate < now; + const daysLeft = Math.ceil((expireDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); + + if (isExpired) { + return 已过期; + } else if (daysLeft <= 7) { + return {daysLeft}天后到期; + } else if (daysLeft <= 30) { + return {daysLeft}天后到期; + } + return null; + })()} +
+ ) : '-'} +
+ {agent?.version} + + + + {agent?.lastSeenAt && dayjs(agent.lastSeenAt).format('YYYY-MM-DD HH:mm:ss')} + + + + {agent?.createdAt && dayjs(agent.createdAt).format('YYYY-MM-DD HH:mm:ss')} + + + {agent?.updatedAt && dayjs(agent.updatedAt).format('YYYY-MM-DD HH:mm:ss')} + +
+ ), + }, + { + key: 'audit', + label: ( + + + 安全审计 + {auditResult && failCount > 0 && ( + {failCount} + )} + + ), + children: ( + + {/* 非 Linux 系统提示 */} + {agent && !agent.os.toLowerCase().includes('linux') && ( + + )} + + {!auditResult ? ( + agent?.os.toLowerCase().includes('linux') ? ( + + 该探针还没有进行过安全审计。点击右上角"下发命令"按钮,选择"安全审计"来执行首次审计。 + + + } + type="info" + showIcon + /> + ) : null + ) : ( + <> + {/* 审计概览 */} + + + + } + /> + + + } + /> + + + } + /> + + + } + /> + + + = 80 ? '#ff4d4f' : + auditResult.riskScore >= 50 ? '#faad14' : + auditResult.riskScore >= 20 ? '#1890ff' : '#52c41a' + }} + prefix={} + /> + + +
威胁等级
+ + {auditResult.threatLevel === 'critical' ? '严重风险' : + auditResult.threatLevel === 'high' ? '高风险' : + auditResult.threatLevel === 'medium' ? '中风险' : '低风险'} + + +
+
+ 最近审计时间: {dayjs(auditResult.startTime).format('YYYY-MM-DD HH:mm:ss')} +
+
+ + {/* 详细审计结果 */} + + + )} + + ), + }, ]; return ( @@ -112,16 +318,6 @@ const AgentDetail = () => { - {auditResult && ( - - )} {
- {/* 基本信息 */} -
- - - 探针信息 - {agent?.status === 1 && 在线} - {agent?.status !== 1 && 离线} - - } - className="rounded-lg border border-gray-200 mt-0" - > - - {agent?.name} - {agent?.id} - {agent?.hostname} - {agent?.ip} - - {agent?.os} - - - {agent?.arch} - - {agent?.version} - - - - {agent?.lastSeenAt && dayjs(agent.lastSeenAt).format('YYYY-MM-DD HH:mm:ss')} + {/* 探针状态卡片 */} + +
+
+ +
+

{agent?.name || agent?.hostname}

+ + {agent?.hostname} + {agent?.status === 1 ? ( + 在线 + ) : ( + 离线 + )} - - - {agent?.createdAt && dayjs(agent.createdAt).format('YYYY-MM-DD HH:mm:ss')} - - - {agent?.updatedAt && dayjs(agent.updatedAt).format('YYYY-MM-DD HH:mm:ss')} - - - -
+
+
+ {auditResult && ( +
+
安全评分
+
= 80 ? 'text-red-500' : + auditResult.riskScore >= 50 ? 'text-orange-500' : + auditResult.riskScore >= 20 ? 'text-blue-500' : 'text-green-500' + }`}> + {100 - auditResult.riskScore} +
+
+ )} +
+ - {/* 审计结果预览 */} -
- {auditResult && ( - - - 最近审计结果 - - {dayjs(auditResult.startTime).format('YYYY-MM-DD HH:mm')} - - - } - className="rounded-lg border border-gray-200 mt-4 lg:mt-6" - extra={ - - } - > - - -
-
- {auditResult.securityChecks.length} -
-
总检查项
-
- - -
-
- {auditResult.securityChecks.filter(c => c.status === 'pass').length} -
-
通过
-
- - -
-
- {auditResult.securityChecks.filter(c => c.status === 'fail').length} -
-
失败
-
- - -
-
- {auditResult.securityChecks.filter(c => c.status === 'warn').length} -
-
警告
-
- -
-
- )} -
+ {/* Tabs 内容 */} + ); }; export default AgentDetail; - diff --git a/web/src/pages/Agents/AgentList.tsx b/web/src/pages/Agents/AgentList.tsx index 1926629..6a120e2 100644 --- a/web/src/pages/Agents/AgentList.tsx +++ b/web/src/pages/Agents/AgentList.tsx @@ -3,7 +3,7 @@ import {useNavigate} from 'react-router-dom'; import type {ActionType, ProColumns} from '@ant-design/pro-components'; import {ProTable} from '@ant-design/pro-components'; import {App, Button, DatePicker, Divider, Form, Input, Modal, Space, Tag} from 'antd'; -import {Edit, Eye, RefreshCw} from 'lucide-react'; +import {Edit, Eye, RefreshCw, Plus, Shield} from 'lucide-react'; import {getAgentPaging, updateAgentInfo} from '../../api/agent'; import type {Agent} from '../../types'; import {getErrorMessage} from '../../lib/utils'; @@ -31,6 +31,7 @@ const AgentList = () => { setEditModalVisible(true); }; + // 保存探针信息 const handleSave = async () => { if (!currentAgent) return; @@ -167,10 +168,19 @@ const AgentList = () => { title: '操作', key: 'action', valueType: 'option', - width: 150, + width: 220, fixed: 'right', render: (_, record) => ( + - - - - - {!result && ( - - )} - - {result && ( - <> - {/* 统计信息 */} - - - - } - /> - - - } - /> - - - } - /> - - - } - /> - - - = 80 ? '#ff4d4f' : - result.riskScore >= 50 ? '#faad14' : - result.riskScore >= 20 ? '#1890ff' : '#52c41a' - }} - prefix={} - /> - - -
威胁等级
- - {getThreatLevelConfig(result.threatLevel).text} - - -
-
- - {/* 系统信息 */} - 系统信息} - className="rounded-lg border border-gray-200 mt-4 lg:mt-6" - > - - - {result.systemInfo.hostname} - - - {result.systemInfo.os} - - - {result.systemInfo.kernelVersion} - - - {formatUptime(result.systemInfo.uptime)} - - {result.systemInfo.publicIP && ( - - {result.systemInfo.publicIP} - - )} - - {dayjs(result.startTime).format('YYYY-MM-DD HH:mm:ss')} - {dayjs(result.endTime).format('HH:mm:ss')} - - (耗时: {((result.endTime - result.startTime) / 1000).toFixed(2)}秒) - - - - - - {/* 安全检查结果 */} - 安全检查结果} - className="rounded-lg border border-gray-200 mt-4 lg:mt-6" - > - - {result.securityChecks.map((check, index) => ( - - {getStatusIcon(check.status)} - - {getCategoryName(check.category)} - - {getStatusTag(check.status)} - - {check.message} - -
- } - > - {check.details && check.details.length > 0 && ( - ( - - - {getStatusIcon(detail.status)} - - {detail.name} - - {getStatusTag(detail.status)} - - {detail.message} - - - {detail.evidence && ( -
-
- - 证据信息 - {detail.evidence.riskLevel && ( - - {detail.evidence.riskLevel === 'high' ? '高危' : - detail.evidence.riskLevel === 'medium' ? '中危' : '低危'} - - )} -
- {detail.evidence.filePath && ( -
- 文件路径: - {detail.evidence.filePath} -
- )} - {detail.evidence.fileHash && ( -
- - SHA256: - {detail.evidence.fileHash} -
- )} - {detail.evidence.timestamp && ( -
- - 时间: - {dayjs(detail.evidence.timestamp).format('YYYY-MM-DD HH:mm:ss')} -
- )} - {detail.evidence.networkConn && ( -
- 网络连接: - {detail.evidence.networkConn} -
- )} - {detail.evidence.processTree && detail.evidence.processTree.length > 0 && ( -
-
- - 进程树: -
-
- {detail.evidence.processTree.map((line, idx) => ( -
- {line} -
- ))} -
-
- )} -
- )} -
- )} - /> - )} - - ))} - - - - {/* 修复建议 */} - {result.recommendations && result.recommendations.length > 0 && ( - 修复建议} - className="rounded-lg border border-gray-200 mt-4 lg:mt-6" - > - ( - - - {index + 1}. - - {item} - - - - )} - /> - - )} - - )} - - {/* 历史记录 */} - {historyList.length > 0 && ( - 审计历史} - className="rounded-lg border border-gray-200 mt-4 lg:mt-6" - > - ( - - - - {dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss')} - - {item.systemInfo.os} - - } - description={ - - - - 通过: {item.passCount} - - - - 失败: {item.failCount} - - - - 警告: {item.warnCount} - - - } - /> - - )} - /> - - )} - - ); -}; - -export default AuditResult; diff --git a/web/src/pages/Agents/AuditResultView.tsx b/web/src/pages/Agents/AuditResultView.tsx new file mode 100644 index 0000000..d474831 --- /dev/null +++ b/web/src/pages/Agents/AuditResultView.tsx @@ -0,0 +1,254 @@ +import {Card, Collapse, List, Space, Tag, Descriptions} from 'antd'; +import { + CheckCircle, + XCircle, + AlertTriangle, + MinusCircle, + FileText, + Hash, + GitBranch, + Clock +} from 'lucide-react'; +import type {VPSAuditResult} from '../../api/agent'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; + +dayjs.extend(duration); + +const {Panel} = Collapse; + +interface AuditResultViewProps { + result: VPSAuditResult; +} + +const AuditResultView = ({result}: AuditResultViewProps) => { + const getStatusIcon = (status: string) => { + switch (status) { + case 'pass': + return ; + case 'fail': + return ; + case 'warn': + return ; + case 'skip': + return ; + default: + return null; + } + }; + + const getStatusTag = (status: string) => { + const configs = { + pass: {color: 'success', text: '通过'}, + fail: {color: 'error', text: '失败'}, + warn: {color: 'warning', text: '警告'}, + skip: {color: 'default', text: '跳过'}, + }; + const config = configs[status as keyof typeof configs] || {color: 'default', text: status}; + return {config.text}; + }; + + const getCategoryName = (category: string) => { + const names: Record = { + 'non_root_user': '非Root用户', + 'ufw_security': 'UFW防火墙', + 'ssh_security': 'SSH安全', + 'access_control': '访问控制', + 'port_security': '端口安全', + 'unattended_upgrades': '自动更新', + 'fail2ban': 'Fail2ban', + 'rootkit_detection': 'Rootkit检测', + 'suspicious_processes': '可疑进程检测', + 'listening_ports': '端口监听检查', + 'cron_jobs': '定时任务检查', + 'suspicious_files': '可疑文件检查', + 'system_accounts': '系统账户检查', + 'network_connections': '网络连接检查', + 'file_integrity': '文件完整性检查', + 'login_history': '登录历史检查', + 'immutable_files': '不可变文件检查', + 'suspicious_env_vars': '环境变量检查', + }; + return names[category] || category; + }; + + const formatUptime = (seconds: number) => { + const d = dayjs.duration(seconds, 'seconds'); + const days = Math.floor(d.asDays()); + const hours = d.hours(); + const minutes = d.minutes(); + return `${days}天 ${hours}小时 ${minutes}分钟`; + }; + + return ( +
+ {/* 系统信息 */} + 系统信息} + variant={'outlined'} + > + + + {result.systemInfo.hostname} + + + {result.systemInfo.os} + + + {result.systemInfo.kernelVersion} + + + {formatUptime(result.systemInfo.uptime)} + + {result.systemInfo.publicIP && ( + + {result.systemInfo.publicIP} + + )} + + {dayjs(result.startTime).format('YYYY-MM-DD HH:mm:ss')} - {dayjs(result.endTime).format('HH:mm:ss')} + + (耗时: {((result.endTime - result.startTime) / 1000).toFixed(2)}秒) + + + + + + {/* 安全检查结果 */} + 安全检查结果} + variant={'outlined'} + > + + {result.securityChecks.map((check, index) => ( + + {getStatusIcon(check.status)} + + {getCategoryName(check.category)} + + {getStatusTag(check.status)} + + {check.message} + + + } + > + {check.details && check.details.length > 0 && ( + ( + + + {getStatusIcon(detail.status)} + + {detail.name} + + {getStatusTag(detail.status)} + + {detail.message} + + + {detail.evidence && ( +
+
+ + 证据信息 + {detail.evidence.riskLevel && ( + + {detail.evidence.riskLevel === 'high' ? '高危' : + detail.evidence.riskLevel === 'medium' ? '中危' : '低危'} + + )} +
+ {detail.evidence.filePath && ( +
+ 文件路径: + {detail.evidence.filePath} +
+ )} + {detail.evidence.fileHash && ( +
+ + SHA256: + {detail.evidence.fileHash} +
+ )} + {detail.evidence.timestamp && ( +
+ + 时间: + {dayjs(detail.evidence.timestamp).format('YYYY-MM-DD HH:mm:ss')} +
+ )} + {detail.evidence.networkConn && ( +
+ 网络连接: + {detail.evidence.networkConn} +
+ )} + {detail.evidence.processTree && detail.evidence.processTree.length > 0 && ( +
+
+ + 进程树: +
+
+ {detail.evidence.processTree.map((line, idx) => ( +
+ {line} +
+ ))} +
+
+ )} +
+ )} +
+ )} + /> + )} +
+ ))} +
+
+ + {/* 修复建议 */} + {result.recommendations && result.recommendations.length > 0 && ( + 修复建议} + variant={'outlined'} + > + ( + + + {index + 1}. + + {item} + + + + )} + /> + + )} +
+ ); +}; + +export default AuditResultView; diff --git a/web/src/pages/Public/MonitorList.tsx b/web/src/pages/Public/MonitorList.tsx index 670fdc7..cc25312 100644 --- a/web/src/pages/Public/MonitorList.tsx +++ b/web/src/pages/Public/MonitorList.tsx @@ -97,15 +97,6 @@ const MonitorList = () => { const monitorSummaries = monitors; - const lastUpdatedDisplay = - dataUpdatedAt && dataUpdatedAt > 0 - ? new Date(dataUpdatedAt).toLocaleTimeString('zh-CN', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }) - : '尚未刷新'; - const renderGridView = () => (
{monitorSummaries.map((stats) => { @@ -221,100 +212,6 @@ const MonitorList = () => { const renderListView = () => ( <> - {/* 移动端:使用卡片式布局 */} -
- {monitorSummaries.map((stats) => { - const hasCert = stats.certExpiryDate > 0; - const certExpiringSoon = hasCert && stats.certExpiryDays < 30; - - return ( -
navigate(`/monitors/${encodeURIComponent(stats.id)}`)} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - navigate(`/monitors/${encodeURIComponent(stats.id)}`); - } - }} - className="cursor-pointer rounded-xl border border-slate-200 bg-white p-4 transition hover:border-blue-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-300" - > -
-
-
- - {stats.name} - - -
-

- {stats.showTargetPublic ? stats.target : '***'} -

- {stats.agentCount > 1 && ( -

- {stats.agentCount} 个探针 -

- )} -
-
- -
-
- 当前响应 - {formatTime(stats.currentResponse)} -
-
- 24h 平均响应 - {formatTime(stats.avgResponse24h)} -
-
-
- 24h 在线率 - {formatPercentValue(stats.uptime24h)}% -
- -
-
-
- 30d 在线率 - {formatPercentValue(stats.uptime30d)}% -
- -
- {hasCert && ( -
-
- - 证书到期 -
-
-
- {formatDate(stats.certExpiryDate)} -
-
- 剩余 {stats.certExpiryDays} 天 -
-
-
- )} -
-
- ); - })} -
- {/* 桌面端:使用表格布局 */}
@@ -435,8 +332,6 @@ const MonitorList = () => { return (
{ return (
// 用于获取按网卡分组的网络指标 const useNetworkMetricsByInterface = (agentId: string | undefined, range: TimeRange) => { - const [networkMetrics, setNetworkMetrics] = useState([]); + const [networkMetrics, setNetworkMetrics] = useState([]); useEffect(() => { if (!agentId) { @@ -577,7 +594,12 @@ const ServerDetail = () => { // GPU 图表数据(汇总所有GPU的平均利用率) const gpuChartData = useMemo(() => { - const aggregated: Record = {}; + const aggregated: Record = {}; metricsData.gpu.forEach((item) => { const time = new Date(item.timestamp).toLocaleTimeString('zh-CN', { @@ -775,8 +797,10 @@ const ServerDetail = () => { return (
-
-
+
+
@@ -789,7 +813,8 @@ const ServerDetail = () => { 返回概览
-
+
@@ -1209,7 +1234,8 @@ const ServerDetail = () => { >
- +
@@ -1224,7 +1250,8 @@ const ServerDetail = () => {
温度 - {gpu.temperature.toFixed(1)}°C + {gpu.temperature.toFixed(1)}°C
显存 @@ -1234,11 +1261,13 @@ const ServerDetail = () => {
功耗 - {gpu.powerDraw.toFixed(1)}W + {gpu.powerDraw.toFixed(1)}W
风扇转速 - {gpu.fanSpeed.toFixed(0)}% + {gpu.fanSpeed.toFixed(0)}%