增加批量配置防篡改保护和 SSH 登录监控

This commit is contained in:
dushixiang committed 2026-01-17 12:26:32 +08:00
1 parent 013bf4af73
commit 0788f1739f
3 files changed
+357 -1

No files matched your search

+67 -1
View File
@@ -3,7 +3,7 @@ import {Link, useNavigate, useSearchParams} from 'react-router-dom';
import type {MenuProps} from 'antd';
import {App, Button, Divider, Dropdown, Form, Input, Select, Space, Table, Tag} from 'antd';
import type {ColumnsType, TablePaginationConfig} from 'antd/es/table';
import {Edit, Eye, MoreVertical, Plus, RefreshCw, Shield, Tags, Trash2} from 'lucide-react';
import {Edit, Eye, FileWarning, Lock, MoreVertical, Plus, RefreshCw, Shield, Tags, Trash2} from 'lucide-react';
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import dayjs from 'dayjs';
import {deleteAgent, getAgentPaging, getTags} from '@/api/agent.ts';
@@ -12,6 +12,8 @@ import {getErrorMessage} from '@/lib/utils';
import {PageHeader} from '@admin/components';
import AgentEditModal from './AgentEditModal';
import BatchTagsModal from './BatchTagsModal';
import BatchTamperProtectionModal from './BatchTamperProtectionModal';
import BatchSSHLoginConfigModal from './BatchSSHLoginConfigModal';
interface AgentFilters {
name?: string;
@@ -30,6 +32,8 @@ const AgentList = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [editModalVisible, setEditModalVisible] = useState(false);
const [batchTagModalVisible, setBatchTagModalVisible] = useState(false);
const [batchTamperModalVisible, setBatchTamperModalVisible] = useState(false);
const [batchSSHModalVisible, setBatchSSHModalVisible] = useState(false);
const [editingAgentId, setEditingAgentId] = useState<string | undefined>(undefined);
const current = Number(searchParams.get('page')) || 1;
@@ -202,6 +206,22 @@ const AgentList = () => {
setBatchTagModalVisible(true);
};
const handleBatchTamperConfig = () => {
if (selectedRowKeys.length === 0) {
messageApi.warning('请先选择要操作的探针');
return;
}
setBatchTamperModalVisible(true);
};
const handleBatchSSHConfig = () => {
if (selectedRowKeys.length === 0) {
messageApi.warning('请先选择要操作的探针');
return;
}
setBatchSSHModalVisible(true);
};
const columns: ColumnsType<Agent> = [
{
title: '名称',
@@ -377,6 +397,18 @@ const AgentList = () => {
icon: <Shield size={14}/>,
onClick: () => navigate(`/admin/agents/${record.id}?tab=audit`),
},
{
key: 'tamper',
label: '防篡改保护',
icon: <FileWarning size={14}/>,
onClick: () => navigate(`/admin/agents/${record.id}?tab=tamper`),
},
{
key: 'ssh-login',
label: 'SSH 登录监控',
icon: <Lock size={14}/>,
onClick: () => navigate(`/admin/agents/${record.id}?tab=ssh-login`),
},
{
key: 'edit',
label: '编辑信息',
@@ -434,6 +466,20 @@ const AgentList = () => {
onClick: handleBatchTags,
disabled: selectedRowKeys.length === 0,
},
{
key: 'batch-tamper',
label: `批量配置防篡改保护${selectedRowKeys.length > 0 ? ` (${selectedRowKeys.length})` : ''}`,
icon: <FileWarning size={16}/>,
onClick: handleBatchTamperConfig,
disabled: selectedRowKeys.length === 0,
},
{
key: 'batch-ssh',
label: `批量配置 SSH 登录监控${selectedRowKeys.length > 0 ? ` (${selectedRowKeys.length})` : ''}`,
icon: <Lock size={16}/>,
onClick: handleBatchSSHConfig,
disabled: selectedRowKeys.length === 0,
},
{
key: 'register',
label: '注册探针',
@@ -533,6 +579,26 @@ const AgentList = () => {
setSelectedRowKeys([]);
}}
/>
<BatchTamperProtectionModal
open={batchTamperModalVisible}
agentIds={selectedRowKeys as string[]}
onCancel={() => setBatchTamperModalVisible(false)}
onSuccess={() => {
setBatchTamperModalVisible(false);
setSelectedRowKeys([]);
}}
/>
<BatchSSHLoginConfigModal
open={batchSSHModalVisible}
agentIds={selectedRowKeys as string[]}
onCancel={() => setBatchSSHModalVisible(false)}
onSuccess={() => {
setBatchSSHModalVisible(false);
setSelectedRowKeys([]);
}}
/>
</div>
);
};
@@ -0,0 +1,133 @@
import {useEffect} from 'react';
import {App, Form, Input, Modal, Switch} from 'antd';
import {useMutation, useQueryClient} from '@tanstack/react-query';
import {updateSSHLoginConfig} from '@/api/agent';
import {getErrorMessage} from '@/lib/utils';
const {TextArea} = Input;
// 验证 IP 地址格式(支持 IPv4 和 CIDR)
const validateIPOrCIDR = (value: string): boolean => {
const ipv4Regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const cidrRegex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(3[0-2]|[12]?[0-9])$/;
return ipv4Regex.test(value) || cidrRegex.test(value);
};
const parseIPWhitelist = (text: string): string[] => {
return text
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0);
};
interface BatchSSHLoginConfigModalProps {
open: boolean;
agentIds: string[];
onCancel: () => void;
onSuccess: () => void;
}
const BatchSSHLoginConfigModal = ({open, agentIds, onCancel, onSuccess}: BatchSSHLoginConfigModalProps) => {
const {message: messageApi} = App.useApp();
const [form] = Form.useForm();
const queryClient = useQueryClient();
useEffect(() => {
if (!open) {
return;
}
form.setFieldsValue({
enabled: false,
ipWhitelistText: '',
});
}, [open, form]);
const batchMutation = useMutation({
mutationFn: async (payload: {enabled: boolean; ipWhitelist: string[]}) => {
const results = await Promise.allSettled(
agentIds.map(agentId => updateSSHLoginConfig(agentId, payload)),
);
const failedCount = results.filter(result => result.status === 'rejected').length;
return {
total: agentIds.length,
successCount: agentIds.length - failedCount,
failedCount,
};
},
onSuccess: (result) => {
if (result.failedCount > 0) {
messageApi.warning(`已配置 ${result.successCount} 个探针,失败 ${result.failedCount} 个`);
} else {
messageApi.success(`成功配置 ${result.total} 个探针的 SSH 登录监控`);
}
queryClient.invalidateQueries({queryKey: ['admin', 'agents']});
queryClient.invalidateQueries({queryKey: ['sshLoginConfig']});
onSuccess();
},
onError: (error: unknown) => {
messageApi.error(getErrorMessage(error, '批量配置 SSH 登录监控失败'));
},
});
const handleOk = async () => {
if (agentIds.length === 0) {
messageApi.warning('请先选择要操作的探针');
return;
}
try {
const values = await form.validateFields();
const ipWhitelistText = values.ipWhitelistText || '';
const ipWhitelist = parseIPWhitelist(ipWhitelistText);
const invalidIPs = ipWhitelist.filter(ip => !validateIPOrCIDR(ip));
if (invalidIPs.length > 0) {
messageApi.error(`以下 IP 地址或 CIDR 格式不正确:\n${invalidIPs.join('\n')}`);
return;
}
await batchMutation.mutateAsync({
enabled: values.enabled,
ipWhitelist: ipWhitelist,
});
} catch (error: unknown) {
if (typeof error === 'object' && error !== null && 'errorFields' in error) {
return;
}
messageApi.error(getErrorMessage(error, '批量配置 SSH 登录监控失败'));
}
};
return (
<Modal
title={`批量配置 SSH 登录监控 (已选择 ${agentIds.length} 个探针)`}
open={open}
onOk={handleOk}
onCancel={onCancel}
confirmLoading={batchMutation.isPending}
width={640}
destroyOnHidden
>
<Form form={form} layout="vertical">
<Form.Item
label="启用监控"
name="enabled"
valuePropName="checked"
extra="启用后,探针将自动安装 PAM Hook 并开始监控 SSH 登录事件"
>
<Switch checkedChildren="已启用" unCheckedChildren="已禁用"/>
</Form.Item>
<Form.Item
label="IP 白名单"
name="ipWhitelistText"
extra="白名单中的 IP 地址登录时只记录不发送通知。每行一个 IP 地址或 CIDR 网段"
>
<TextArea
rows={6}
placeholder={'每行一个 IP 地址或 CIDR 网段,例如:\n192.168.1.1\n192.168.1.0/24\n10.0.0.0/8'}
/>
</Form.Item>
</Form>
</Modal>
);
};
export default BatchSSHLoginConfigModal;
@@ -0,0 +1,157 @@
import {useEffect, useState} from 'react';
import {App, Button, Form, Input, List, Modal, Space, Switch} from 'antd';
import {Plus, Shield, Trash2} from 'lucide-react';
import {useMutation, useQueryClient} from '@tanstack/react-query';
import {updateTamperConfig} from '@/api/tamper';
import {getErrorMessage} from '@/lib/utils';
interface BatchTamperProtectionModalProps {
open: boolean;
agentIds: string[];
onCancel: () => void;
onSuccess: () => void;
}
const BatchTamperProtectionModal = ({open, agentIds, onCancel, onSuccess}: BatchTamperProtectionModalProps) => {
const {message: messageApi} = App.useApp();
const [form] = Form.useForm();
const queryClient = useQueryClient();
const [editPaths, setEditPaths] = useState<string[]>([]);
const [newPath, setNewPath] = useState('');
useEffect(() => {
if (!open) {
return;
}
form.setFieldsValue({enabled: false});
setEditPaths([]);
setNewPath('');
}, [open, form]);
const handleAddPath = () => {
const trimmed = newPath.trim();
if (trimmed && !editPaths.includes(trimmed)) {
setEditPaths(prev => [...prev, trimmed]);
setNewPath('');
}
};
const handleRemovePath = (path: string) => {
setEditPaths(prev => prev.filter(item => item !== path));
};
const batchMutation = useMutation({
mutationFn: async (payload: {enabled: boolean; paths: string[]}) => {
const results = await Promise.allSettled(
agentIds.map(agentId => updateTamperConfig(agentId, payload.enabled, payload.paths)),
);
const failedCount = results.filter(result => result.status === 'rejected').length;
return {
total: agentIds.length,
successCount: agentIds.length - failedCount,
failedCount,
};
},
onSuccess: (result) => {
if (result.failedCount > 0) {
messageApi.warning(`已配置 ${result.successCount} 个探针,失败 ${result.failedCount} 个`);
} else {
messageApi.success(`成功配置 ${result.total} 个探针的防篡改保护`);
}
queryClient.invalidateQueries({queryKey: ['admin', 'agents']});
queryClient.invalidateQueries({queryKey: ['tamperConfig']});
onSuccess();
},
onError: (error: unknown) => {
messageApi.error(getErrorMessage(error, '批量配置防篡改保护失败'));
},
});
const handleOk = async () => {
if (agentIds.length === 0) {
messageApi.warning('请先选择要操作的探针');
return;
}
try {
const values = await form.validateFields();
await batchMutation.mutateAsync({
enabled: values.enabled,
paths: editPaths,
});
} catch (error: unknown) {
if (typeof error === 'object' && error !== null && 'errorFields' in error) {
return;
}
messageApi.error(getErrorMessage(error, '批量配置防篡改保护失败'));
}
};
return (
<Modal
title={`批量配置防篡改保护 (已选择 ${agentIds.length} 个探针)`}
open={open}
onOk={handleOk}
onCancel={onCancel}
confirmLoading={batchMutation.isPending}
width={640}
destroyOnHidden
>
<Space direction="vertical" size="large" style={{width: '100%'}}>
<Form form={form} layout="vertical">
<Form.Item
label="启用防篡改保护"
name="enabled"
valuePropName="checked"
extra="开启后将对下方目录进行实时监控和保护"
>
<Switch checkedChildren="已启用" unCheckedChildren="已禁用"/>
</Form.Item>
</Form>
<div>
<div className="mb-2 text-sm font-medium">受保护的目录列表</div>
<Space.Compact style={{width: '100%'}} className="mb-3">
<Input
placeholder="输入要保护的目录路径,如 /etc/nginx"
value={newPath}
onChange={(e) => setNewPath(e.target.value)}
onPressEnter={handleAddPath}
/>
<Button type="primary" icon={<Plus size={16}/>} onClick={handleAddPath}>
添加
</Button>
</Space.Compact>
{editPaths.length === 0 ? (
<div className="rounded border border-dashed border-gray-300 p-6 text-center">
<Shield size={40} className="mx-auto mb-2 text-gray-300"/>
<p className="text-sm text-gray-500">暂未配置保护目录</p>
</div>
) : (
<List
bordered
dataSource={editPaths}
renderItem={(path) => (
<List.Item
actions={[
<Button
key="delete"
type="text"
danger
icon={<Trash2 size={16}/>}
onClick={() => handleRemovePath(path)}
/>,
]}
>
<span className="font-mono text-sm">{path}</span>
</List.Item>
)}
/>
)}
</div>
</Space>
</Modal>
);
};
export default BatchTamperProtectionModal;