增加批量配置防篡改保护和 SSH 登录监控
This commit is contained in:
1 parent
013bf4af73
commit
0788f1739f
3 files changed
+357
-1
No files matched your search
@@ -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;
|
||||
Reference in new issue
Block a user