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

+188 -77
View File
@@ -216,19 +216,6 @@ export default function TicketDetailPage() {
/* Parse execute result for visualization */
/* ---------------------------------------------------------------- */
const executeStats = useMemo(() => {
if (!ticket?.execute_result) return null;
// Try to extract affected rows and duration from execute result
const result = ticket.execute_result;
const affectedMatch = result.match(/(\d+)\s*(?:row|rows)?\s*(?:affected|changed)/i);
const durationMatch = result.match(/(\d+(?:\.\d+)?)\s*(?:ms|seconds?|s)/i);
return {
affectedRows: ticket.affected_rows || (affectedMatch ? parseInt(affectedMatch[1], 10) : undefined),
durationMs: ticket.execute_duration_ms || (durationMatch ? parseFloat(durationMatch[1]) : undefined),
raw: result,
};
}, [ticket]);
/* ---------------------------------------------------------------- */
/* Loading & Error states */
/* ---------------------------------------------------------------- */
@@ -442,39 +429,6 @@ export default function TicketDetailPage() {
);
};
const renderExecuteStats = () => {
if (!executeStats) return null;
return (
<div className="mt-3 grid grid-cols-2 gap-3">
{executeStats.affectedRows !== undefined && (
<div className="p-3 bg-[var(--dg-bg-primary)] rounded-lg border border-[var(--dg-border)]">
<div className="flex items-center gap-1.5 text-xs text-[var(--dg-text-muted)] mb-1">
<BarChart3 className="w-3.5 h-3.5" />
影响行数
</div>
<div className="text-xl font-semibold text-[var(--dg-text-primary)]">
{executeStats.affectedRows.toLocaleString()}
</div>
</div>
)}
{executeStats.durationMs !== undefined && (
<div className="p-3 bg-[var(--dg-bg-primary)] rounded-lg border border-[var(--dg-border)]">
<div className="flex items-center gap-1.5 text-xs text-[var(--dg-text-muted)] mb-1">
<Clock className="w-3.5 h-3.5" />
执行耗时
</div>
<div className="text-xl font-semibold text-[var(--dg-text-primary)]">
{executeStats.durationMs >= 1000
? `${(executeStats.durationMs / 1000).toFixed(2)}s`
: `${Math.round(executeStats.durationMs)}ms`}
</div>
</div>
)}
</div>
);
};
const renderRelatedTickets = () => {
if (relatedTickets.length === 0) return null;
@@ -624,22 +578,90 @@ export default function TicketDetailPage() {
</div>
{/* Audit result */}
{ticket.audit_result && (
<div className="bg-[var(--dg-bg-secondary)] rounded-lg p-5">
<button
onClick={() => setShowFullAudit(!showFullAudit)}
className="flex items-center justify-between w-full text-left"
>
<h3 className="text-[15px] font-semibold flex items-center gap-2">
<Shield className="w-4 h-4 text-blue-400" /> 审核结果
</h3>
{showFullAudit ? <ChevronUp className="w-4 h-4 text-[var(--dg-text-muted)]" /> : <ChevronDown className="w-4 h-4 text-[var(--dg-text-muted)]" />}
</button>
<div className={`mt-3 text-sm text-[var(--dg-text-secondary)] ${showFullAudit ? '' : 'line-clamp-3'}`}>
{ticket.audit_result}
{ticket.audit_result && (() => {
let audit: any = null;
try { audit = typeof ticket.audit_result === 'string' ? JSON.parse(ticket.audit_result) : ticket.audit_result; } catch { audit = null; }
if (!audit || !audit.findings) {
return (
<div className="bg-[var(--dg-bg-secondary)] rounded-lg p-5">
<h3 className="text-[15px] font-semibold flex items-center gap-2">
<Shield className="w-4 h-4 text-blue-400" /> 审核结果
</h3>
<p className="mt-2 text-sm text-[var(--dg-text-secondary)]">{ticket.audit_result}</p>
</div>
);
}
const errorCount = audit.findings.filter((f: any) => f.level === 'error').length;
const warningCount = audit.findings.filter((f: any) => f.level === 'warning').length;
const infoCount = audit.findings.filter((f: any) => f.level === 'info').length;
const scoreColor = audit.score >= 90 ? 'text-green-400' : audit.score >= 60 ? 'text-amber-400' : 'text-red-400';
return (
<div className="bg-[var(--dg-bg-secondary)] rounded-lg p-5">
<button
onClick={() => setShowFullAudit(!showFullAudit)}
className="flex items-center justify-between w-full text-left"
>
<h3 className="text-[15px] font-semibold flex items-center gap-2">
<Shield className="w-4 h-4 text-blue-400" /> SQL 审核结果
</h3>
<div className="flex items-center gap-3">
<span className={`text-xl font-bold ${scoreColor}`}>{audit.score ?? '-'}</span>
<span className="text-xs text-[var(--dg-text-muted)]">分</span>
{showFullAudit ? <ChevronUp className="w-4 h-4 text-[var(--dg-text-muted)]" /> : <ChevronDown className="w-4 h-4 text-[var(--dg-text-muted)]" />}
</div>
</button>
{/* Summary badges */}
<div className="mt-3 flex items-center gap-2 flex-wrap">
{audit.passed ? (
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded text-xs font-medium bg-green-500/15 text-green-400">
<CheckCircle className="w-3 h-3" /> 审核通过
</span>
) : (
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded text-xs font-medium bg-red-500/15 text-red-400">
<XCircle className="w-3 h-3" /> 存在阻断项
</span>
)}
{errorCount > 0 && <span className="px-2 py-0.5 rounded text-xs bg-red-500/15 text-red-400">{errorCount} 错误</span>}
{warningCount > 0 && <span className="px-2 py-0.5 rounded text-xs bg-amber-500/15 text-amber-400">{warningCount} 警告</span>}
{infoCount > 0 && <span className="px-2 py-0.5 rounded text-xs bg-blue-500/15 text-blue-400">{infoCount} 提示</span>}
</div>
{/* Findings table */}
{showFullAudit && audit.findings.length > 0 && (
<div className="mt-4 space-y-2">
{audit.findings.map((f: any, idx: number) => {
const levelConfig: Record<string, { label: string; color: string; icon: any }> = {
error: { label: '错误', color: 'text-red-400 bg-red-500/15', icon: XCircle },
warning: { label: '警告', color: 'text-amber-400 bg-amber-500/15', icon: AlertTriangle },
info: { label: '提示', color: 'text-blue-400 bg-blue-500/15', icon: CheckCircle },
};
const lc = levelConfig[f.level] || levelConfig.info;
const LevelIcon = lc.icon;
return (
<div key={idx} className={`p-3 rounded-lg border border-[var(--dg-border)] ${f.level === 'error' ? 'border-red-500/20' : ''}`}>
<div className="flex items-center gap-2 mb-1">
<LevelIcon className={`w-3.5 h-3.5 ${lc.color.split(' ')[0]}`} />
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${lc.color}`}>{lc.label}</span>
{f.rule && <span className="text-xs text-[var(--dg-text-muted)] font-mono">{f.rule}</span>}
</div>
<p className="text-sm text-[var(--dg-text-primary)] pl-6">{f.message}</p>
</div>
);
})}
</div>
)}
{/* Summary text */}
{!showFullAudit && audit.summary && (
<p className="mt-2 text-xs text-[var(--dg-text-muted)]">{audit.summary}</p>
)}
</div>
</div>
)}
);
})()}
{/* Rollback SQL */}
{ticket.rollback_sql && (
@@ -658,23 +680,112 @@ export default function TicketDetailPage() {
)}
{/* Execute result */}
{ticket.execute_result && (
<div className="bg-[var(--dg-bg-secondary)] rounded-lg p-5">
<h3 className="text-[15px] font-semibold flex items-center gap-2 border-b border-[var(--dg-border)] pb-3 mb-3">
<Play className="w-4 h-4 text-purple-400" /> 执行结果
</h3>
<pre className="p-3 bg-[var(--dg-bg-primary)] rounded-lg text-xs font-mono whitespace-pre-wrap overflow-x-auto">
{ticket.execute_result}
</pre>
{renderExecuteStats()}
{ticket.execute_time && (
<div className="text-xs text-[var(--dg-text-muted)] mt-2 flex items-center gap-1">
<Clock className="w-3 h-3" />
执行时间: {new Date(ticket.execute_time).toLocaleString()}
{ticket.execute_result && (() => {
let execResult: any = null;
try { execResult = typeof ticket.execute_result === 'string' ? JSON.parse(ticket.execute_result) : ticket.execute_result; } catch { execResult = null; }
if (!execResult || typeof execResult !== 'object') {
return (
<div className="bg-[var(--dg-bg-secondary)] rounded-lg p-5">
<h3 className="text-[15px] font-semibold flex items-center gap-2 border-b border-[var(--dg-border)] pb-3 mb-3">
<Play className="w-4 h-4 text-purple-400" /> 执行结果
</h3>
<pre className="p-3 bg-[var(--dg-bg-primary)] rounded-lg text-xs font-mono whitespace-pre-wrap overflow-x-auto">
{ticket.execute_result}
</pre>
{ticket.execute_time && (
<div className="text-xs text-[var(--dg-text-muted)] mt-2 flex items-center gap-1">
<Clock className="w-3 h-3" />
执行时间: {new Date(ticket.execute_time).toLocaleString()}
</div>
)}
</div>
)}
</div>
)}
);
}
const isSuccess = execResult.status === 'success';
const StatusIcon = isSuccess ? CheckCircle : XCircle;
const statusColor = isSuccess ? 'text-green-400' : 'text-red-400';
const statusBg = isSuccess ? 'bg-green-500/15' : 'bg-red-500/15';
return (
<div className="bg-[var(--dg-bg-secondary)] rounded-lg p-5">
<h3 className="text-[15px] font-semibold flex items-center gap-2 border-b border-[var(--dg-border)] pb-3 mb-3">
<Play className="w-4 h-4 text-purple-400" /> 执行结果
</h3>
{/* Status badge + key metrics */}
<div className="flex items-center gap-3 flex-wrap mb-3">
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded text-xs font-medium ${statusBg} ${statusColor}`}>
<StatusIcon className="w-3.5 h-3.5" />
{isSuccess ? '执行成功' : '执行失败'}
</span>
{execResult.affected_rows !== undefined && (
<span className="px-2.5 py-1 rounded text-xs bg-[var(--dg-bg-tertiary)]">
影响行数: <span className="font-medium">{execResult.affected_rows}</span>
</span>
)}
{execResult.duration_ms !== undefined && (
<span className="px-2.5 py-1 rounded text-xs bg-[var(--dg-bg-tertiary)]">
耗时: <span className="font-medium">{execResult.duration_ms}ms</span>
</span>
)}
{execResult.validation_passed !== undefined && (
<span className={`px-2.5 py-1 rounded text-xs ${execResult.validation_passed ? 'text-green-400 bg-green-500/10' : 'text-amber-400 bg-amber-500/10'}`}>
{execResult.validation_passed ? '✓ 验证通过' : '✗ 验证未通过'}
</span>
)}
</div>
{/* Errors */}
{execResult.errors && execResult.errors.length > 0 && (
<div className="space-y-2 mb-3">
{execResult.errors.map((err: string, idx: number) => (
<div key={idx} className="p-3 rounded-lg bg-red-500/10 border border-red-500/20">
<div className="flex items-center gap-1.5 mb-1">
<XCircle className="w-3.5 h-3.5 text-red-400" />
<span className="text-xs font-medium text-red-400">错误 {execResult.errors.length > 1 ? `#${idx + 1}` : ''}</span>
</div>
<p className="text-sm text-red-300 pl-5">{err}</p>
</div>
))}
</div>
)}
{/* Warnings */}
{execResult.warnings && execResult.warnings.length > 0 && (
<div className="space-y-2 mb-3">
{execResult.warnings.map((w: string, idx: number) => (
<div key={idx} className="p-3 rounded-lg bg-amber-500/10 border border-amber-500/20">
<div className="flex items-center gap-1.5 mb-1">
<AlertTriangle className="w-3.5 h-3.5 text-amber-400" />
<span className="text-xs font-medium text-amber-400">警告 {execResult.warnings.length > 1 ? `#${idx + 1}` : ''}</span>
</div>
<p className="text-sm text-amber-300 pl-5">{w}</p>
</div>
))}
</div>
)}
{/* Raw output (if present) */}
{execResult.raw_output && (
<div className="mt-3">
<p className="text-xs text-[var(--dg-text-muted)] mb-1">原始输出</p>
<pre className="p-3 bg-[var(--dg-bg-primary)] rounded-lg text-xs font-mono whitespace-pre-wrap overflow-x-auto border border-[var(--dg-border-light)]">
{execResult.raw_output}
</pre>
</div>
)}
{ticket.execute_time && (
<div className="text-xs text-[var(--dg-text-muted)] mt-3 flex items-center gap-1">
<Clock className="w-3 h-3" />
执行时间: {new Date(ticket.execute_time).toLocaleString()}
</div>
)}
</div>
);
})()}
{/* Timeline */}
{renderTimeline()}