Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括: - 配置文件中的项目名称 - 代码注释和文档引用 - 环境变量和路径 - 类型定义和接口名称 - 测试用例和模拟数据 同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
256 lines
9.1 KiB
TypeScript
256 lines
9.1 KiB
TypeScript
import { useEffect } from 'react';
|
||
import { Shield, ShieldCheck, ShieldAlert, ShieldX, RefreshCw, Loader2, AlertCircle } from 'lucide-react';
|
||
import { useSecurityStore } from '../store/securityStore';
|
||
|
||
// ZCLAW 16-layer security architecture names (Chinese)
|
||
const SECURITY_LAYER_NAMES: Record<string, string> = {
|
||
// Layer 1: Network
|
||
'network.firewall': '网络防火墙',
|
||
'network.tls': 'TLS 加密',
|
||
'network.rate_limit': '速率限制',
|
||
// Layer 2: Authentication
|
||
'auth.device': '设备认证',
|
||
'auth.jwt': 'JWT 令牌',
|
||
'auth.session': '会话管理',
|
||
// Layer 3: Authorization
|
||
'auth.rbac': '角色权限',
|
||
'auth.capabilities': '能力控制',
|
||
// Layer 4: Input Validation
|
||
'input.sanitization': '输入净化',
|
||
'input.schema': '模式验证',
|
||
// Layer 5: Execution
|
||
'exec.sandbox': '沙箱隔离',
|
||
'exec.timeout': '执行超时',
|
||
'exec.resource_limit': '资源限制',
|
||
// Layer 6: Audit & Logging
|
||
'audit.logging': '审计日志',
|
||
'audit.tracing': '请求追踪',
|
||
'audit.alerting': '审计告警',
|
||
};
|
||
|
||
// Default 16 layers for display when API returns minimal data
|
||
const DEFAULT_LAYERS = [
|
||
{ name: 'network.firewall', enabled: false },
|
||
{ name: 'network.tls', enabled: false },
|
||
{ name: 'network.rate_limit', enabled: false },
|
||
{ name: 'auth.device', enabled: false },
|
||
{ name: 'auth.jwt', enabled: false },
|
||
{ name: 'auth.session', enabled: false },
|
||
{ name: 'auth.rbac', enabled: false },
|
||
{ name: 'auth.capabilities', enabled: false },
|
||
{ name: 'input.sanitization', enabled: false },
|
||
{ name: 'input.schema', enabled: false },
|
||
{ name: 'exec.sandbox', enabled: false },
|
||
{ name: 'exec.timeout', enabled: false },
|
||
{ name: 'exec.resource_limit', enabled: false },
|
||
{ name: 'audit.logging', enabled: false },
|
||
{ name: 'audit.tracing', enabled: false },
|
||
{ name: 'audit.alerting', enabled: false },
|
||
];
|
||
|
||
function getSecurityIcon(level: 'critical' | 'high' | 'medium' | 'low') {
|
||
switch (level) {
|
||
case 'critical':
|
||
return <ShieldCheck className="w-5 h-5 text-green-600" />;
|
||
case 'high':
|
||
return <Shield className="w-5 h-5 text-blue-600" />;
|
||
case 'medium':
|
||
return <ShieldAlert className="w-5 h-5 text-yellow-600" />;
|
||
case 'low':
|
||
return <ShieldX className="w-5 h-5 text-red-600" />;
|
||
}
|
||
}
|
||
|
||
function getSecurityLabel(level: 'critical' | 'high' | 'medium' | 'low') {
|
||
switch (level) {
|
||
case 'critical':
|
||
return { text: '极高', color: 'text-green-600 bg-green-50 border-green-200' };
|
||
case 'high':
|
||
return { text: '高', color: 'text-blue-600 bg-blue-50 border-blue-200' };
|
||
case 'medium':
|
||
return { text: '中', color: 'text-yellow-600 bg-yellow-50 border-yellow-200' };
|
||
case 'low':
|
||
return { text: '低', color: 'text-red-600 bg-red-50 border-red-200' };
|
||
}
|
||
}
|
||
|
||
export function SecurityStatus() {
|
||
const securityStatus = useSecurityStore((s) => s.securityStatus);
|
||
const securityStatusLoading = useSecurityStore((s) => s.securityStatusLoading);
|
||
const securityStatusError = useSecurityStore((s) => s.securityStatusError);
|
||
const loadSecurityStatus = useSecurityStore((s) => s.loadSecurityStatus);
|
||
|
||
useEffect(() => {
|
||
loadSecurityStatus();
|
||
}, [loadSecurityStatus]);
|
||
|
||
// Loading state
|
||
if (securityStatusLoading && !securityStatus) {
|
||
return (
|
||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<Loader2 className="w-4 h-4 text-gray-400 animate-spin" />
|
||
<span className="text-sm font-semibold text-gray-900">安全状态</span>
|
||
</div>
|
||
<p className="text-xs text-gray-400">加载中...</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// API error state - show friendly message
|
||
if (securityStatusError && !securityStatus) {
|
||
return (
|
||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="flex items-center gap-2">
|
||
<AlertCircle className="w-4 h-4 text-yellow-500" />
|
||
<span className="text-sm font-semibold text-gray-900">安全状态</span>
|
||
</div>
|
||
<button
|
||
onClick={() => loadSecurityStatus()}
|
||
className="p-1 text-gray-400 hover:text-orange-500 rounded transition-colors"
|
||
title="重试"
|
||
>
|
||
<RefreshCw className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
<p className="text-xs text-gray-500 mb-2">安全状态检测失败</p>
|
||
<p className="text-xs text-gray-400">
|
||
本地安全检测模块加载失败,请检查安全组件是否正确初始化
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Use default layers if no data, or merge with API data
|
||
const displayLayers = securityStatus?.layers?.length
|
||
? DEFAULT_LAYERS.map((defaultLayer) => {
|
||
const apiLayer = securityStatus.layers.find((l) => l.name === defaultLayer.name);
|
||
return apiLayer || defaultLayer;
|
||
})
|
||
: DEFAULT_LAYERS;
|
||
|
||
const enabledCount = displayLayers.filter((l) => l.enabled).length;
|
||
const totalCount = displayLayers.length;
|
||
const securityLevel = securityStatus?.securityLevel ||
|
||
(enabledCount >= 14 ? 'critical' : enabledCount >= 10 ? 'high' : enabledCount >= 6 ? 'medium' : 'low');
|
||
|
||
const levelLabel = getSecurityLabel(securityLevel);
|
||
|
||
return (
|
||
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="flex items-center gap-2">
|
||
{getSecurityIcon(securityLevel)}
|
||
<span className="text-sm font-semibold text-gray-900">安全状态</span>
|
||
{securityStatusLoading && (
|
||
<Loader2 className="w-3 h-3 text-gray-400 animate-spin" />
|
||
)}
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<span className={`text-xs px-2 py-0.5 rounded-full border ${levelLabel.color}`}>
|
||
{levelLabel.text}
|
||
</span>
|
||
<button
|
||
onClick={() => loadSecurityStatus()}
|
||
className="p-1 text-gray-400 hover:text-orange-500 rounded transition-colors disabled:opacity-50"
|
||
title="刷新安全状态"
|
||
disabled={securityStatusLoading}
|
||
>
|
||
<RefreshCw className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Summary */}
|
||
<div className="mb-3 text-xs text-gray-500">
|
||
已启用 {enabledCount} / {totalCount} 层防护
|
||
</div>
|
||
|
||
{/* Progress bar */}
|
||
<div className="mb-4 h-2 bg-gray-100 rounded-full overflow-hidden">
|
||
<div
|
||
className={`h-full transition-all duration-300 ${
|
||
securityLevel === 'critical'
|
||
? 'bg-green-500'
|
||
: securityLevel === 'high'
|
||
? 'bg-blue-500'
|
||
: securityLevel === 'medium'
|
||
? 'bg-yellow-500'
|
||
: 'bg-red-500'
|
||
}`}
|
||
style={{ width: `${(enabledCount / totalCount) * 100}%` }}
|
||
/>
|
||
</div>
|
||
|
||
{/* Layers Grid */}
|
||
<div className="grid grid-cols-2 gap-1.5">
|
||
{displayLayers.map((layer) => {
|
||
const label = SECURITY_LAYER_NAMES[layer.name] || layer.name;
|
||
return (
|
||
<div
|
||
key={layer.name}
|
||
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs ${
|
||
layer.enabled
|
||
? 'bg-green-50 text-green-700'
|
||
: 'bg-gray-50 text-gray-400'
|
||
}`}
|
||
title={layer.name}
|
||
>
|
||
<div
|
||
className={`w-1.5 h-1.5 rounded-full ${
|
||
layer.enabled ? 'bg-green-500' : 'bg-gray-300'
|
||
}`}
|
||
/>
|
||
<span className="truncate">{label}</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Layer Categories */}
|
||
<div className="mt-4 pt-3 border-t border-gray-100">
|
||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||
<CategorySummary
|
||
label="网络"
|
||
layers={displayLayers.filter((l) => l.name.startsWith('network.'))}
|
||
/>
|
||
<CategorySummary
|
||
label="认证"
|
||
layers={displayLayers.filter((l) => l.name.startsWith('auth.'))}
|
||
/>
|
||
<CategorySummary
|
||
label="执行"
|
||
layers={displayLayers.filter((l) => l.name.startsWith('exec.'))}
|
||
/>
|
||
<CategorySummary
|
||
label="输入"
|
||
layers={displayLayers.filter((l) => l.name.startsWith('input.'))}
|
||
/>
|
||
<CategorySummary
|
||
label="审计"
|
||
layers={displayLayers.filter((l) => l.name.startsWith('audit.'))}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function CategorySummary({ label, layers }: { label: string; layers: { enabled: boolean }[] }) {
|
||
if (layers.length === 0) return null;
|
||
const enabled = layers.filter((l) => l.enabled).length;
|
||
const total = layers.length;
|
||
const allEnabled = enabled === total;
|
||
|
||
return (
|
||
<div className="flex flex-col items-center">
|
||
<span className={`font-medium ${allEnabled ? 'text-green-600' : 'text-gray-500'}`}>
|
||
{enabled}/{total}
|
||
</span>
|
||
<span className="text-gray-400">{label}</span>
|
||
</div>
|
||
);
|
||
}
|