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:
1 parent
9f56e866f2
commit
4ac2528f8e
19 files changed
+1202
-325
No files matched your search
@@ -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()}
|
||||
|
||||
Reference in new issue
Block a user