mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-06 13:54:44 +08:00
修复了一些已知问题
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef, type ReactNode } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
ArrowLeft,
|
||||
AlertTriangle,
|
||||
Camera,
|
||||
Clock,
|
||||
Copy,
|
||||
@@ -299,8 +300,17 @@ export default function ContainerDetail() {
|
||||
start: '开机中...', stop: '关机中...', restart: '重启中...', delete: '删除中...', reinstall: '重装中...',
|
||||
}
|
||||
|
||||
const ensureSubUserCanOperate = async () => {
|
||||
if (isSubUser && container?.policy_blocked) {
|
||||
await dialog.alert('策略临时封禁', container.policy_blocked_reason || '虚拟机被策略临时封禁,暂不能执行操作。')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleAction = async (action: string) => {
|
||||
if (!containerIdentifier) return
|
||||
if (!(await ensureSubUserCanOperate())) return
|
||||
setActionLoading(action)
|
||||
try {
|
||||
switch (action) {
|
||||
@@ -462,11 +472,13 @@ export default function ContainerDetail() {
|
||||
}
|
||||
|
||||
const openAddMapping = () => {
|
||||
if (isSubUser && container?.policy_blocked) return
|
||||
setDraft(emptyDraft)
|
||||
setShowNat(true)
|
||||
}
|
||||
|
||||
const openEditMapping = (pm: PortMapping, index: number) => {
|
||||
if (isSubUser && container?.policy_blocked) return
|
||||
if (isSubUser) {
|
||||
// Sub-user: only edit container_port in a simple modal
|
||||
setDraft({
|
||||
@@ -489,6 +501,7 @@ export default function ContainerDetail() {
|
||||
|
||||
const submitMapping = async (): Promise<boolean> => {
|
||||
if (!containerIdentifier) return false
|
||||
if (!(await ensureSubUserCanOperate())) return false
|
||||
if (draft.index === null && container) {
|
||||
const currentCount = container.port_mappings?.length || 0
|
||||
const limit = container.port_mapping_limit || Math.max(currentCount, 2)
|
||||
@@ -541,6 +554,7 @@ export default function ContainerDetail() {
|
||||
|
||||
const removeMapping = async (index: number) => {
|
||||
if (!containerIdentifier || !(await dialog.confirm('删除映射', '确定要删除这条映射规则吗?'))) return
|
||||
if (!(await ensureSubUserCanOperate())) return
|
||||
try {
|
||||
await deletePortMapping(containerIdentifier, index)
|
||||
await fetchContainer()
|
||||
@@ -553,6 +567,7 @@ export default function ContainerDetail() {
|
||||
|
||||
const handleCreateSnapshot = async () => {
|
||||
if (!containerIdentifier) return
|
||||
if (!(await ensureSubUserCanOperate())) return
|
||||
if (isSubUser && snapshots.length >= snapshotQuota) {
|
||||
await dialog.alert('快照配额已满', '已达到管理员设置的快照配额,请先删除旧快照。')
|
||||
return
|
||||
@@ -577,6 +592,7 @@ export default function ContainerDetail() {
|
||||
}
|
||||
|
||||
const openSnapshotSchedule = () => {
|
||||
if (isSubUser && container?.policy_blocked) return
|
||||
setSnapshotScheduleDraft({
|
||||
intervalHours: Math.max(snapshotSchedule?.interval_hours || 24, 24),
|
||||
time: snapshotSchedule?.time || '03:00',
|
||||
@@ -586,6 +602,7 @@ export default function ContainerDetail() {
|
||||
|
||||
const saveSnapshotSchedule = async (enabled: boolean) => {
|
||||
if (!containerIdentifier) return
|
||||
if (!(await ensureSubUserCanOperate())) return
|
||||
const intervalHours = snapshotScheduleDraft.intervalHours
|
||||
const scheduleTime = snapshotScheduleDraft.time || '03:00'
|
||||
if (enabled && intervalHours < 24) {
|
||||
@@ -625,6 +642,7 @@ export default function ContainerDetail() {
|
||||
|
||||
const handleDeleteSnapshot = async (snapshot: Snapshot) => {
|
||||
if (!containerIdentifier) return
|
||||
if (!(await ensureSubUserCanOperate())) return
|
||||
if (!(await dialog.confirm('删除快照', `确定删除 ${snapshot.created_at} 的快照吗?`))) return
|
||||
setSnapshotBusy(snapshot.id)
|
||||
try {
|
||||
@@ -640,6 +658,7 @@ export default function ContainerDetail() {
|
||||
|
||||
const handleRestoreSnapshot = async (snapshot: Snapshot) => {
|
||||
if (!containerIdentifier) return
|
||||
if (!(await ensureSubUserCanOperate())) return
|
||||
if (!(await dialog.confirm('恢复快照', `确定恢复到 ${snapshot.created_at} 的快照吗?当前容器数据会被覆盖。`))) return
|
||||
setSnapshotBusy(snapshot.id)
|
||||
try {
|
||||
@@ -681,6 +700,9 @@ export default function ContainerDetail() {
|
||||
const isWindows = container.template?.includes('windows')
|
||||
const canOpenVNC = isKVM && isRunning
|
||||
const isExpired = container.expires_at ? new Date(container.expires_at) < new Date() : false
|
||||
const isPolicyBlocked = !!container.policy_blocked
|
||||
const isSubUserPolicyBlocked = isSubUser && isPolicyBlocked
|
||||
const policyBlockedText = container.policy_blocked_reason || '虚拟机被策略临时封禁'
|
||||
const publicHost = hostInfo?.network.public_ipv4 || PUBLIC_HOST
|
||||
const maxVCPU = hostInfo?.cpu.cores || 64
|
||||
const maxRAMMB = hostInfo?.ram.total_mb ? Number(hostInfo.ram.total_mb) : undefined
|
||||
@@ -703,7 +725,7 @@ export default function ContainerDetail() {
|
||||
const diskIOBps = (usage?.disk_read_bps || 0) + (usage?.disk_write_bps || 0)
|
||||
const mappingCount = container.port_mappings?.length || 0
|
||||
const mappingLimit = container.port_mapping_limit || Math.max(mappingCount, 2)
|
||||
const canAddMapping = isSubUser ? mappingCount < mappingLimit : true
|
||||
const canAddMapping = isSubUser ? mappingCount < mappingLimit && !isSubUserPolicyBlocked : true
|
||||
const managementUrl = subUser?.access_code
|
||||
? `${window.location.origin}/login?code=${encodeURIComponent(subUser.access_code)}`
|
||||
: ''
|
||||
@@ -774,34 +796,35 @@ export default function ContainerDetail() {
|
||||
<InfoTag color="emerald">内网 {container.ip || '-'}</InfoTag>
|
||||
<InfoTag color="amber">NAT {mappingCount} 条</InfoTag>
|
||||
<InfoTag color="violet">{isWindows ? 'RDP' : 'SSH'} {publicHost}:{container.ssh_port}</InfoTag>
|
||||
{isPolicyBlocked && <InfoTag color="red">策略封禁</InfoTag>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 flex-wrap justify-end">
|
||||
{!isRunning ? (
|
||||
<ActionButton dark disabled={!!taskStatus || isExpired} onClick={() => handleAction('start')}>
|
||||
<ActionButton dark disabled={!!taskStatus || isExpired || isSubUserPolicyBlocked} onClick={() => handleAction('start')}>
|
||||
<Play className="w-3.5 h-3.5" />
|
||||
{isExpired ? '已到期' : taskStatus === 'start' ? taskActionLabels['start'] : '开机'}
|
||||
{isSubUserPolicyBlocked ? '已封禁' : isExpired ? '已到期' : taskStatus === 'start' ? taskActionLabels['start'] : '开机'}
|
||||
</ActionButton>
|
||||
) : (
|
||||
<>
|
||||
<ActionButton disabled={!!taskStatus || isExpired} onClick={() => handleAction('stop')}>
|
||||
<ActionButton disabled={!!taskStatus || isExpired || isSubUserPolicyBlocked} onClick={() => handleAction('stop')}>
|
||||
<Square className="w-3.5 h-3.5" />
|
||||
{isExpired ? '已到期' : taskStatus === 'stop' ? taskActionLabels['stop'] : '关机'}
|
||||
{isSubUserPolicyBlocked ? '已封禁' : isExpired ? '已到期' : taskStatus === 'stop' ? taskActionLabels['stop'] : '关机'}
|
||||
</ActionButton>
|
||||
<ActionButton disabled={!!taskStatus || isExpired} onClick={() => handleAction('restart')}>
|
||||
<ActionButton disabled={!!taskStatus || isExpired || isSubUserPolicyBlocked} onClick={() => handleAction('restart')}>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
{isExpired ? '已到期' : taskStatus === 'restart' ? taskActionLabels['restart'] : '重启'}
|
||||
{isSubUserPolicyBlocked ? '已封禁' : isExpired ? '已到期' : taskStatus === 'restart' ? taskActionLabels['restart'] : '重启'}
|
||||
</ActionButton>
|
||||
{!isWindows && (
|
||||
<ActionButton dark onClick={() => setShowSSH(true)}>
|
||||
<ActionButton dark disabled={isSubUserPolicyBlocked} onClick={() => setShowSSH(true)}>
|
||||
<TerminalSquare className="w-3.5 h-3.5" />
|
||||
WebSSH
|
||||
</ActionButton>
|
||||
)}
|
||||
{isKVM && (
|
||||
<ActionButton dark disabled={!canOpenVNC} onClick={() => setShowVNC(true)}>
|
||||
<ActionButton dark disabled={!canOpenVNC || isSubUserPolicyBlocked} onClick={() => setShowVNC(true)}>
|
||||
<Monitor className="w-3.5 h-3.5" />
|
||||
WebVNC
|
||||
</ActionButton>
|
||||
@@ -815,12 +838,12 @@ export default function ContainerDetail() {
|
||||
</ActionButton>
|
||||
)}
|
||||
<>
|
||||
<ActionButton onClick={() => setShowNat(true)}>
|
||||
<ActionButton disabled={isSubUserPolicyBlocked} onClick={() => setShowNat(true)}>
|
||||
<Settings className="w-3.5 h-3.5" />
|
||||
NAT 管理
|
||||
</ActionButton>
|
||||
</>
|
||||
<ActionButton onClick={() => setShowSnapshots(true)} disabled={!!taskStatus || !!snapshotBusy}>
|
||||
<ActionButton onClick={() => setShowSnapshots(true)} disabled={!!taskStatus || !!snapshotBusy || isSubUserPolicyBlocked}>
|
||||
<Camera className="w-3.5 h-3.5" />
|
||||
快照
|
||||
</ActionButton>
|
||||
@@ -840,9 +863,23 @@ export default function ContainerDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSubUserPolicyBlocked && (
|
||||
<div className="flex items-start gap-3 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<div>
|
||||
<div className="font-medium">虚拟机被策略临时封禁</div>
|
||||
<div className="mt-1 text-xs text-red-600">{policyBlockedText}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
||||
<Panel title="连接信息">
|
||||
{isWindows ? (
|
||||
{isSubUserPolicyBlocked ? (
|
||||
<div className="rounded-md border border-red-100 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||
虚拟机被策略临时封禁,连接信息暂不可用。
|
||||
</div>
|
||||
) : isWindows ? (
|
||||
<>
|
||||
<PlainRow label="RDP 地址" value={`${publicHost}:${container.ssh_port}`} mono />
|
||||
<PlainRow label="用户名" value="Administrator" mono />
|
||||
@@ -1507,13 +1544,14 @@ function StatusBadge({ running }: { running: boolean }) {
|
||||
)
|
||||
}
|
||||
|
||||
function InfoTag({ color, children }: { color: 'blue' | 'emerald' | 'amber' | 'violet' | 'slate'; children: ReactNode }) {
|
||||
function InfoTag({ color, children }: { color: 'blue' | 'emerald' | 'amber' | 'violet' | 'slate' | 'red'; children: ReactNode }) {
|
||||
const classes = {
|
||||
blue: 'bg-blue-50 text-blue-700 border-blue-100',
|
||||
emerald: 'bg-emerald-50 text-emerald-700 border-emerald-100',
|
||||
amber: 'bg-amber-50 text-amber-700 border-amber-100',
|
||||
violet: 'bg-violet-50 text-violet-700 border-violet-100',
|
||||
slate: 'bg-slate-50 text-slate-700 border-slate-100',
|
||||
red: 'bg-red-50 text-red-700 border-red-100',
|
||||
}
|
||||
return <span className={`px-1.5 py-0.5 border rounded text-[11px] whitespace-nowrap ${classes[color]}`}>{children}</span>
|
||||
}
|
||||
|
||||
@@ -392,6 +392,7 @@ export default function Containers() {
|
||||
const isRunning = container.status === 'running'
|
||||
const task = (container.id > 0 ? taskStatusMap[container.id] : taskNameMap[container.name]) || container.createTask
|
||||
const isPlaceholder = !!container.isPlaceholder
|
||||
const isPolicyBlocked = !!container.policy_blocked
|
||||
const usage = usageByName[container.name]
|
||||
const isKVM = (container.virtualization || 'lxc') === 'kvm'
|
||||
|
||||
@@ -436,7 +437,7 @@ export default function Containers() {
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-2.5 py-2 align-top">
|
||||
<StatusBadge running={isRunning} task={task} placeholder={isPlaceholder} />
|
||||
<StatusBadge running={isRunning} task={task} placeholder={isPlaceholder} policyBlocked={isPolicyBlocked} />
|
||||
</td>
|
||||
<td className="px-2.5 py-2 align-top text-xs text-gray-600 whitespace-nowrap">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
@@ -580,8 +581,17 @@ type DisplayContainer = Container & {
|
||||
createTask?: Task
|
||||
}
|
||||
|
||||
function StatusBadge({ running, task, placeholder }: { running: boolean; task?: Task; placeholder?: boolean }) {
|
||||
function StatusBadge({ running, task, placeholder, policyBlocked }: { running: boolean; task?: Task; placeholder?: boolean; policyBlocked?: boolean }) {
|
||||
const baseClass = "inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-medium whitespace-nowrap"
|
||||
if (policyBlocked) {
|
||||
return (
|
||||
<span className={`${baseClass} bg-red-50 text-red-700`}>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-red-500"></span>
|
||||
策略封禁
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (task?.status === 'failed') {
|
||||
return (
|
||||
<span className={`${baseClass} bg-red-50 text-red-700`}>
|
||||
|
||||
+156
-13
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { RefreshCw } from 'lucide-react'
|
||||
import { getSecurityAlerts, SecurityAlert } from '../services/api'
|
||||
import { FileText, Power, RefreshCw, X } from 'lucide-react'
|
||||
import { getSecurityAlerts, getSecurityLogs, getSecuritySettings, SecurityAlert, SecurityLog, updateSecuritySettings } from '../services/api'
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
port_scan: '端口扫描',
|
||||
@@ -23,12 +23,18 @@ const severityLabels: Record<string, string> = {
|
||||
|
||||
export default function Security() {
|
||||
const [alerts, setAlerts] = useState<SecurityAlert[]>([])
|
||||
const [autoShutdown, setAutoShutdown] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [savingSettings, setSavingSettings] = useState(false)
|
||||
const [logAlert, setLogAlert] = useState<SecurityAlert | null>(null)
|
||||
const [logs, setLogs] = useState<SecurityLog[]>([])
|
||||
const [logsLoading, setLogsLoading] = useState(false)
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const alertRes = await getSecurityAlerts()
|
||||
const [alertRes, settingsRes] = await Promise.all([getSecurityAlerts(), getSecuritySettings()])
|
||||
if (alertRes.data.data) setAlerts(alertRes.data.data)
|
||||
if (settingsRes.data.data) setAutoShutdown(settingsRes.data.data.auto_shutdown)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
@@ -42,6 +48,36 @@ export default function Security() {
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchData])
|
||||
|
||||
const handleAutoShutdownChange = async () => {
|
||||
const next = !autoShutdown
|
||||
setAutoShutdown(next)
|
||||
setSavingSettings(true)
|
||||
try {
|
||||
const res = await updateSecuritySettings({ auto_shutdown: next })
|
||||
if (res.data.data) setAutoShutdown(res.data.data.auto_shutdown)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setAutoShutdown(!next)
|
||||
} finally {
|
||||
setSavingSettings(false)
|
||||
}
|
||||
}
|
||||
|
||||
const openLogs = async (alert: SecurityAlert) => {
|
||||
setLogAlert(alert)
|
||||
setLogs([])
|
||||
setLogsLoading(true)
|
||||
try {
|
||||
const res = await getSecurityLogs(alert.container_name)
|
||||
setLogs(filterRelatedLogs(res.data.data || [], alert))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setLogs([])
|
||||
} finally {
|
||||
setLogsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
@@ -52,15 +88,33 @@ export default function Security() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h1 className="text-xl font-semibold text-black">安全告警</h1>
|
||||
<button
|
||||
onClick={fetchData}
|
||||
className="inline-flex items-center gap-2 px-3 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 text-sm"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
刷新
|
||||
</button>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={autoShutdown}
|
||||
onClick={handleAutoShutdownChange}
|
||||
disabled={savingSettings}
|
||||
title="告警自动关机"
|
||||
className={`inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm transition-colors disabled:opacity-60 ${
|
||||
autoShutdown
|
||||
? 'border-red-200 bg-red-50 text-red-700 hover:bg-red-100'
|
||||
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Power className="w-4 h-4" />
|
||||
<span>{autoShutdown ? '自动关机已开' : '自动关机已关'}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={fetchData}
|
||||
className="inline-flex items-center gap-2 px-3 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 text-sm"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
||||
@@ -93,12 +147,24 @@ export default function Security() {
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-gray-800 whitespace-nowrap">{typeLabels[alert.type] || alert.type}</td>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-gray-700 whitespace-nowrap">{alert.container_name}</td>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-gray-600 whitespace-nowrap">{alert.source_ip}</td>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-gray-600 whitespace-nowrap">{alert.source_ip || '-'}</td>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-gray-600 whitespace-nowrap">
|
||||
{formatTarget(alert)}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-gray-600 whitespace-nowrap">{alert.count}</td>
|
||||
<td className="px-4 py-2.5 text-gray-600 min-w-[260px]">{alert.detail}</td>
|
||||
<td className="px-4 py-2.5 text-gray-600 min-w-[300px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="min-w-0 flex-1">{alert.detail}</span>
|
||||
<button
|
||||
onClick={() => openLogs(alert)}
|
||||
className="inline-flex shrink-0 items-center gap-1 rounded-md border border-gray-300 px-2 py-1 text-xs text-gray-700 hover:bg-gray-50"
|
||||
title="查看相关记录"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
查看
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -106,6 +172,69 @@ export default function Security() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{logAlert && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
||||
<div className="w-full max-w-4xl overflow-hidden rounded-lg border border-gray-200 bg-white shadow-xl">
|
||||
<div className="flex items-start justify-between gap-3 border-b border-gray-200 px-4 py-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-black">相关连接记录</h3>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
{logAlert.container_name} · {typeLabels[logAlert.type] || logAlert.type} · {formatTarget(logAlert)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setLogAlert(null)}
|
||||
className="rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-black"
|
||||
title="关闭"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[70vh] overflow-auto">
|
||||
{logAlert.log_line && (
|
||||
<div className="border-b border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<div className="mb-1 text-xs font-medium text-gray-600">告警原始记录</div>
|
||||
<pre className="whitespace-pre-wrap break-all rounded border border-gray-200 bg-white p-3 text-xs text-gray-700">{logAlert.log_line}</pre>
|
||||
</div>
|
||||
)}
|
||||
{logsLoading ? (
|
||||
<div className="p-8 text-center text-sm text-gray-500">正在加载连接记录...</div>
|
||||
) : logs.length === 0 ? (
|
||||
<div className="p-8 text-center text-sm text-gray-500">
|
||||
暂无可用连接记录。历史告警对应的 conntrack 记录可能已经过期。
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-100 bg-gray-50 text-left text-xs font-medium text-gray-500">
|
||||
<th className="px-4 py-2.5">协议</th>
|
||||
<th className="px-4 py-2.5">状态</th>
|
||||
<th className="px-4 py-2.5">源地址</th>
|
||||
<th className="px-4 py-2.5">目标地址</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{logs.map((log, index) => (
|
||||
<tr key={`${log.src_ip}-${log.src_port}-${log.dst_ip}-${log.dst_port}-${index}`}>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-gray-700">{log.protocol || '-'}</td>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-gray-700">{log.state || '-'}</td>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-gray-600">
|
||||
{formatEndpoint(log.src_ip, log.src_port)}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 font-mono text-xs text-gray-600">
|
||||
{formatEndpoint(log.dst_ip, log.dst_port)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -129,3 +258,17 @@ function formatTarget(alert: SecurityAlert): string {
|
||||
if (!alert.target_ip) return '-'
|
||||
return alert.target_port > 0 ? `${alert.target_ip}:${alert.target_port}` : alert.target_ip
|
||||
}
|
||||
|
||||
function filterRelatedLogs(logs: SecurityLog[], alert: SecurityAlert): SecurityLog[] {
|
||||
return logs.filter((log) => {
|
||||
if (alert.source_ip && log.src_ip !== alert.source_ip) return false
|
||||
if (alert.target_ip && alert.target_ip !== '*' && log.dst_ip !== alert.target_ip) return false
|
||||
if (alert.target_port > 0 && log.dst_port !== alert.target_port) return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function formatEndpoint(ip: string, port: number): string {
|
||||
if (!ip) return '-'
|
||||
return port > 0 ? `${ip}:${port}` : ip
|
||||
}
|
||||
|
||||
@@ -81,6 +81,9 @@ export interface Container {
|
||||
snapshot_schedule_last_run: string
|
||||
snapshot_schedule_next_run: string
|
||||
snapshot_schedule_created_by: string
|
||||
policy_blocked?: boolean
|
||||
policy_blocked_reason?: string
|
||||
policy_blocked_at?: string
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
@@ -524,6 +527,19 @@ export interface SecuritySummary {
|
||||
low: number
|
||||
}
|
||||
|
||||
export interface SecuritySettings {
|
||||
auto_shutdown: boolean
|
||||
}
|
||||
|
||||
export interface SecurityLog {
|
||||
src_ip: string
|
||||
dst_ip: string
|
||||
src_port: number
|
||||
dst_port: number
|
||||
protocol: string
|
||||
state: string
|
||||
}
|
||||
|
||||
export const getSecurityAlerts = () =>
|
||||
api.get<APIResponse<SecurityAlert[]>>('/security/alerts')
|
||||
|
||||
@@ -531,11 +547,17 @@ export const checkContainerSecurity = (containerName: string) =>
|
||||
api.post<APIResponse>('/security/check', { container_name: containerName })
|
||||
|
||||
export const getSecurityLogs = (containerName: string) =>
|
||||
api.get<APIResponse>('/security/logs', { params: { container: containerName } })
|
||||
api.get<APIResponse<SecurityLog[]>>('/security/logs', { params: { container: containerName } })
|
||||
|
||||
export const getSecuritySummary = () =>
|
||||
api.get<APIResponse<SecuritySummary>>('/security/summary')
|
||||
|
||||
export const getSecuritySettings = () =>
|
||||
api.get<APIResponse<SecuritySettings>>('/security/settings')
|
||||
|
||||
export const updateSecuritySettings = (data: SecuritySettings) =>
|
||||
api.put<APIResponse<SecuritySettings>>('/security/settings', data)
|
||||
|
||||
export const createWebSSHTicket = (containerName: string) =>
|
||||
api.post<APIResponse<{ ticket: string }>>('/ssh-ticket', { container_name: containerName })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user