Files
zclaw_openfang/desktop/src/components/SecurityStatus.tsx
iven 0d4fa96b82
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
refactor: 统一项目名称从OpenFang到ZCLAW
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括:
- 配置文件中的项目名称
- 代码注释和文档引用
- 环境变量和路径
- 类型定义和接口名称
- 测试用例和模拟数据

同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
2026-03-27 07:36:03 +08:00

256 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}