fix: 审核引擎优化 + 执行结果结构化展示

后端审核规则修复:
- Redis SEC-001: SET/DEL等常规命令不再误报AUTH要求,改为仅对管理级命令WARNING
- Redis SEC-003: 降级为INFO,排除已由CMD-001拦截的FLUSHDB/FLUSHALL
- Redis key提取: 排除CONFIG/ACL等管理命令,不再把子命令参数误认为key名
- MySQL SEC-006: 新增GRANT ALL/REVOKE ALL全局权限拦截
- MySQL SEC-007: 新增LOAD DATA/LOAD XML拦截
- MySQL SEC-008: 新增SLEEP()/BENCHMARK()时间盲注拦截
- MySQL SEC-009: 新增INTO OUTFILE/DUMPFILE文件写入拦截

前端执行结果结构化:
- TicketDetailPage执行结果从原始JSON改为结构化展示
- 解析JSON展示: 状态徽章+影响行数+耗时+验证状态
- Errors/Warnings逐条渲染: 红色错误框+黄色警告框+具体消息
- 删除废弃的executeStats/renderExecuteStats

其他: 审核规则6分组/慢查询移除/查询数据重命名/AuditLogs修复
测试: 1040 passed, TSC零错误
This commit is contained in:
logan committed 2026-05-24 01:31:33 +08:00
1 parent 9f56e866f2
commit 4ac2528f8e
19 files changed
+1202 -325

No files matched your search

+22 -149
View File
@@ -1,8 +1,8 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import {
Plus, Search, Eye, CheckCircle, XCircle, Play, RotateCcw,
FileCode, History, Undo2, XCircle as XIcon,
Clock, AlertTriangle, AlertCircle, Info, ChevronLeft,
Plus, Search, Eye, CheckCircle, XCircle, Play,
FileCode, History, XCircle as XIcon,
Clock, AlertTriangle, ChevronLeft,
Filter, X, Loader2, Users, Calendar,
CheckSquare, Square, ArrowUpDown, FileDown, Ban,
} from 'lucide-react';
@@ -24,9 +24,7 @@ interface TicketItem {
instance_id: number;
target_database: string;
sql_content: string;
rollback_sql: string | null;
status: string;
priority: string;
applicant_id: number;
applicant_name: string | null;
approver_id: number | null;
@@ -62,11 +60,7 @@ type DateRange = 'all' | 'today' | 'week' | 'month';
/* ------------------------------------------------------------------ */
/* Constants */
/* ------------------------------------------------------------------ */
const priorityConfig: Record<string, { label: string; color: string; bg: string; border: string; icon: React.ElementType }> = {
high: { label: '高', color: 'text-red-400', bg: 'bg-red-500/15', border: 'border-red-500/30', icon: AlertCircle },
medium: { label: '中', color: 'text-yellow-400', bg: 'bg-yellow-500/15', border: 'border-yellow-500/30', icon: AlertTriangle },
low: { label: '低', color: 'text-blue-400', bg: 'bg-blue-500/15', border: 'border-blue-500/30', icon: Info },
};
const statusFlow = ['pending', 'reviewing', 'approved', 'executing', 'success'];
@@ -74,14 +68,13 @@ const statusFlow = ['pending', 'reviewing', 'approved', 'executing', 'success'];
/* Helpers */
/* ------------------------------------------------------------------ */
function exportTicketsToCSV(tickets: TicketItem[]): string {
const columns = ['工单号', '标题', '数据库类型', '目标数据库', '申请人', '优先级', '状态', '创建时间', '变更原因'];
const columns = ['工单号', '标题', '数据库类型', '目标数据库', '申请人', '状态', '创建时间', '变更原因'];
const rows = tickets.map(t => ({
工单号: `TK-${t.id}`,
标题: t.title,
数据库类型: t.db_type.toUpperCase(),
目标数据库: t.target_database,
申请人: t.applicant_name || '-',
优先级: t.priority === 'high' ? '高' : t.priority === 'medium' ? '中' : '低',
状态: statusMap[t.status] || t.status,
创建时间: new Date(t.create_time).toLocaleString(),
变更原因: t.reason || '-',
@@ -144,8 +137,6 @@ function TableRowSkeleton() {
<td className="px-5 py-3.5"><div className="h-4 w-32 animate-pulse rounded bg-[var(--dg-bg-tertiary)]" /></td>
<td className="px-5 py-3.5"><div className="h-4 w-16 animate-pulse rounded bg-[var(--dg-bg-tertiary)]" /></td>
<td className="px-5 py-3.5"><div className="h-4 w-16 animate-pulse rounded bg-[var(--dg-bg-tertiary)]" /></td>
<td className="px-5 py-3.5"><div className="h-4 w-10 animate-pulse rounded bg-[var(--dg-bg-tertiary)]" /></td>
<td className="px-5 py-3.5"><div className="h-4 w-16 animate-pulse rounded bg-[var(--dg-bg-tertiary)]" /></td>
<td className="px-5 py-3.5"><div className="h-4 w-24 animate-pulse rounded bg-[var(--dg-bg-tertiary)]" /></td>
<td className="px-5 py-3.5"><div className="h-4 w-12 animate-pulse rounded bg-[var(--dg-bg-tertiary)]" /></td>
</tr>
@@ -163,7 +154,6 @@ export default function TicketsPage() {
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [dbTypeFilter, setDbTypeFilter] = useState('');
const [priorityFilter, setPriorityFilter] = useState('');
const [dateRange, setDateRange] = useState<DateRange>('all');
const [applicantFilter, setApplicantFilter] = useState('');
const [selectedTicket, setSelectedTicket] = useState<TicketItem | null>(null);
@@ -177,7 +167,7 @@ export default function TicketsPage() {
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
const [batchApproving, setBatchApproving] = useState(false);
const [showFilters, setShowFilters] = useState(false);
const [sortField, setSortField] = useState<'create_time' | 'priority'>('create_time');
const [sortField, setSortField] = useState<'create_time'>('create_time');
const [sortAsc, setSortAsc] = useState(false);
const fetchTickets = useCallback(async () => {
@@ -223,11 +213,6 @@ export default function TicketsPage() {
}
/* Client-side sort */
result.sort((a, b) => {
if (sortField === 'priority') {
const order = { high: 0, medium: 1, low: 2 };
const cmp = (order[a.priority as keyof typeof order] ?? 3) - (order[b.priority as keyof typeof order] ?? 3);
return sortAsc ? cmp : -cmp;
}
return sortAsc
? new Date(a.create_time).getTime() - new Date(b.create_time).getTime()
: new Date(b.create_time).getTime() - new Date(a.create_time).getTime();
@@ -310,7 +295,7 @@ export default function TicketsPage() {
);
}
const hasActiveFilters = search || statusFilter || dbTypeFilter || priorityFilter || dateRange !== 'all' || applicantFilter;
const hasActiveFilters = search || statusFilter || dbTypeFilter || dateRange !== 'all' || applicantFilter;
return (
<div className="space-y-5">
@@ -401,16 +386,7 @@ export default function TicketsPage() {
<option value="postgres">PostgreSQL</option>
<option value="redis">Redis</option>
</select>
<select
className="px-3 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none transition-colors"
value={priorityFilter}
onChange={e => { setPriorityFilter(e.target.value); setPage(1); }}
>
<option value="">全部优先级</option>
<option value="high">高</option>
<option value="medium">中</option>
<option value="low">低</option>
</select>
<select
className="px-3 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none transition-colors"
value={dateRange}
@@ -446,12 +422,7 @@ export default function TicketsPage() {
<button onClick={() => setDbTypeFilter('')}><X className="w-3 h-3" /></button>
</span>
)}
{priorityFilter && (
<span className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-md bg-blue-500/10 text-blue-400">
优先级: {priorityConfig[priorityFilter]?.label || priorityFilter}
<button onClick={() => setPriorityFilter('')}><X className="w-3 h-3" /></button>
</span>
)}
{dateRange !== 'all' && (
<span className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-md bg-blue-500/10 text-blue-400">
<Calendar className="w-3 h-3" />
@@ -460,7 +431,7 @@ export default function TicketsPage() {
</span>
)}
<button
onClick={() => { setSearch(''); setStatusFilter(''); setDbTypeFilter(''); setPriorityFilter(''); setDateRange('all'); setApplicantFilter(''); setPage(1); }}
onClick={() => { setSearch(''); setStatusFilter(''); setDbTypeFilter(''); setDateRange('all'); setApplicantFilter(''); setPage(1); }}
className="text-xs text-red-400 hover:text-red-300 ml-auto"
>
清除全部
@@ -510,7 +481,6 @@ export default function TicketsPage() {
<th className="text-left px-5 py-3 font-semibold">标题</th>
<th className="text-left px-5 py-3 font-semibold">数据库</th>
<th className="text-left px-5 py-3 font-semibold">申请人</th>
<th className="text-left px-5 py-3 font-semibold">优先级</th>
<th className="text-left px-5 py-3 font-semibold">状态</th>
<th className="text-left px-5 py-3 font-semibold">创建时间</th>
<th className="text-left px-5 py-3 font-semibold">操作</th>
@@ -546,15 +516,7 @@ export default function TicketsPage() {
<th className="text-left px-4 py-3 font-semibold">标题</th>
<th className="text-left px-4 py-3 font-semibold">数据库</th>
<th className="text-left px-4 py-3 font-semibold">申请人</th>
<th className="text-left px-4 py-3 font-semibold">
<button
onClick={() => { setSortField('priority'); setSortAsc(!sortAsc); }}
className="flex items-center gap-1 hover:text-blue-400 transition-colors"
>
优先级
<ArrowUpDown className="w-3 h-3" />
</button>
</th>
<th className="text-left px-4 py-3 font-semibold">状态</th>
<th className="text-left px-4 py-3 font-semibold">
<button
@@ -570,8 +532,6 @@ export default function TicketsPage() {
</thead>
<tbody>
{clientFilteredTickets.map(ticket => {
const pConfig = priorityConfig[ticket.priority] || priorityConfig.medium;
const PIcon = pConfig.icon;
const isSelected = selectedIds.has(ticket.id);
return (
<tr
@@ -599,12 +559,7 @@ export default function TicketsPage() {
{ticket.applicant_name || '-'}
</div>
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium border ${pConfig.bg} ${pConfig.color} ${pConfig.border}`}>
<PIcon className="w-3 h-3" />
{pConfig.label}
</span>
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${statusColorMap[ticket.status] || ''}`}>
<span className={`w-1.5 h-1.5 rounded-full ${ticket.status === 'reviewing' || ticket.status === 'executing' ? 'animate-pulse' : ''}`} />
@@ -688,14 +643,10 @@ function NewTicketForm({ onCancel, onCreated }: { onCancel: () => void; onCreate
const [instanceId, setInstanceId] = useState('');
const [targetDatabase, setTargetDatabase] = useState('');
const [sqlContent, setSqlContent] = useState('');
const [rollbackSql, setRollbackSql] = useState('');
const [reason, setReason] = useState('');
const [priority, setPriority] = useState<'high' | 'medium' | 'low'>('medium');
const [sqlTab, setSqlTab] = useState<'change' | 'rollback'>('change');
const [submitting, setSubmitting] = useState(false);
const [instances, setInstances] = useState<Array<{ id: number; name: string; host: string; port: number }>>([]);
const [error, setError] = useState('');
const [charCount, setCharCount] = useState({ change: 0, rollback: 0 });
useEffect(() => {
instanceApi.list().then(res => {
@@ -718,9 +669,7 @@ function NewTicketForm({ onCancel, onCreated }: { onCancel: () => void; onCreate
instance_id: Number(instanceId),
target_database: targetDatabase,
sql_content: sqlContent,
rollback_sql: rollbackSql || undefined,
reason,
priority,
});
onCreated();
} catch (e: unknown) {
@@ -729,25 +678,11 @@ function NewTicketForm({ onCancel, onCreated }: { onCancel: () => void; onCreate
} finally {
setSubmitting(false);
}
}, [title, dbType, instanceId, targetDatabase, sqlContent, rollbackSql, reason, priority, onCreated]);
}, [title, dbType, instanceId, targetDatabase, sqlContent, reason, onCreated]);
const handleSqlChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
const val = e.target.value;
if (sqlTab === 'change') {
setSqlContent(val);
setCharCount(prev => ({ ...prev, change: val.length }));
} else {
setRollbackSql(val);
setCharCount(prev => ({ ...prev, rollback: val.length }));
}
}, [sqlTab]);
const currentSql = sqlTab === 'change' ? sqlContent : rollbackSql;
const priorityOptions: Array<{ value: 'high' | 'medium' | 'low'; label: string }> = [
{ value: 'high', label: '高' },
{ value: 'medium', label: '中' },
{ value: 'low', label: '低' },
];
return (
<div className="space-y-5 max-w-5xl">
@@ -807,62 +742,17 @@ function NewTicketForm({ onCancel, onCreated }: { onCancel: () => void; onCreate
</div>
</div>
{/* Priority */}
<div>
<label className="block text-sm font-medium mb-2">优先级</label>
<div className="flex gap-3">
{priorityOptions.map(p => {
const pConf = priorityConfig[p.value];
return (
<button
key={p.value}
onClick={() => setPriority(p.value)}
className={`flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm border transition-all font-medium ${
priority === p.value
? `${pConf.bg} ${pConf.color} ${pConf.border}`
: 'bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:border-[var(--dg-text-muted)]'
}`}
>
<pConf.icon className="w-4 h-4" />
{p.label}
</button>
);
})}
</div>
</div>
{/* SQL Editor */}
<div>
<label className="block text-sm font-medium mb-2">SQL 语句 <span className="text-red-400">*</span></label>
<div className="dg-card overflow-hidden">
<div className="flex items-center gap-1 p-2 bg-[var(--dg-bg-tertiary)] border-b border-[var(--dg-border)]">
<button
onClick={() => setSqlTab('change')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs transition-all ${
sqlTab === 'change' ? 'bg-blue-500/15 text-blue-400 border border-blue-500/30' : 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'
}`}
>
<FileCode className="w-3.5 h-3.5" />
变更 SQL
</button>
<button
onClick={() => setSqlTab('rollback')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs transition-all ${
sqlTab === 'rollback' ? 'bg-blue-500/15 text-blue-400 border border-blue-500/30' : 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'
}`}
>
<RotateCcw className="w-3.5 h-3.5" />
回滚 SQL
</button>
<span className="ml-auto text-xs text-[var(--dg-text-muted)]">
{charCount[sqlTab]} 字符
</span>
</div>
<textarea
className="w-full min-h-[200px] p-4 bg-[var(--dg-bg-primary)] text-sm font-mono text-[var(--dg-text-primary)] resize-y border-none outline-none placeholder:text-[var(--dg-text-muted)]"
placeholder={sqlTab === 'change' ? "UPDATE users SET status = 'active' WHERE id = 1;" : "-- 请输入回滚 SQL"}
value={currentSql}
onChange={handleSqlChange}
placeholder="UPDATE users SET status = 'active' WHERE id = 1;"
value={sqlContent}
onChange={e => setSqlContent(e.target.value)}
/>
</div>
</div>
@@ -914,7 +804,6 @@ function TicketDetail({ ticket, onBack, canApprove, canExecute, onActionDone }:
}) {
const [loading, setLoading] = useState(false);
const pConfig = priorityConfig[ticket.priority] || priorityConfig.medium;
const handleReview = useCallback(async (action: 'approve' | 'reject') => {
setLoading(true);
@@ -956,10 +845,7 @@ function TicketDetail({ ticket, onBack, canApprove, canExecute, onActionDone }:
<div>
<h1 className="text-xl font-semibold flex items-center gap-3 flex-wrap">
{ticket.title}
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border ${pConfig.bg} ${pConfig.color} ${pConfig.border}`}>
<pConfig.icon className="w-3 h-3" />
{pConfig.label}优先级
</span>
<span className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium ${statusColorMap[ticket.status] || ''}`}>
{statusMap[ticket.status] || ticket.status}
</span>
@@ -1061,7 +947,7 @@ function TicketDetail({ ticket, onBack, canApprove, canExecute, onActionDone }:
{ label: '目标数据库', value: ticket.target_database },
{ label: '申请人', value: ticket.applicant_name || '-' },
{ label: '审批人', value: ticket.approver_name || '-' },
{ label: '优先级', value: <span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium border ${pConfig.bg} ${pConfig.color} ${pConfig.border}`}><pConfig.icon className="w-3 h-3" />{pConfig.label}</span> },
{ label: '创建时间', value: new Date(ticket.create_time).toLocaleString() },
{ label: '变更原因', value: ticket.reason || '-' },
].map((item, idx) => (
@@ -1087,20 +973,7 @@ function TicketDetail({ ticket, onBack, canApprove, canExecute, onActionDone }:
</Card>
</div>
{/* Rollback SQL */}
{ticket.rollback_sql && (
<Card className="dg-card">
<CardHeader className="border-b border-[var(--dg-border)]">
<CardTitle className="text-[15px] font-semibold flex items-center gap-2">
<Undo2 className="w-4 h-4 text-purple-400" />
回滚 SQL
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<pre className="p-5 text-xs font-mono text-[var(--dg-text-primary)] bg-[var(--dg-bg-primary)] overflow-x-auto whitespace-pre-wrap max-h-[300px] overflow-y-auto">{ticket.rollback_sql}</pre>
</CardContent>
</Card>
)}
{/* Audit Record */}
{ticket.audit_result && (