diff --git a/.flocks/plugins/contracts/webui/asset-mapping/manifest.json b/.flocks/plugins/contracts/webui/asset-mapping/manifest.json new file mode 100644 index 00000000..5746e282 --- /dev/null +++ b/.flocks/plugins/contracts/webui/asset-mapping/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "asset-mapping", + "title": "资产测绘大屏", + "route": "/contracts/webui/asset-mapping", + "icon": "Globe", + "order": 100, + "enabled": true, + "placement": "home.after", + "entry": "src/index.tsx", + "updatedAt": 0 +} diff --git a/.flocks/plugins/contracts/webui/asset-mapping/src/Page.tsx b/.flocks/plugins/contracts/webui/asset-mapping/src/Page.tsx new file mode 100644 index 00000000..5c0f3429 --- /dev/null +++ b/.flocks/plugins/contracts/webui/asset-mapping/src/Page.tsx @@ -0,0 +1,502 @@ +import { useState, useEffect } from 'react'; +import { Card } from '@flocks/webui-contract-sdk'; + +interface AssetItem { + id: string; + name: string; + type: string; + ip: string; + ports: string; + risk: 'critical' | 'high' | 'medium' | 'low'; + source: string; + tag: string; +} + +interface TimelineEvent { + time: string; + type: 'added' | 'removed' | 'changed'; + asset: string; + detail: string; + ai: boolean; +} + +interface MultiSourceRow { + source: string; + icon: string; + subdomains: number; + ips: number; + ports: number; + services: number; + unique: number; +} + +const MOCK_SOURCES: MultiSourceRow[] = [ + { source: 'ThreatBook', icon: 'TB', subdomains: 28, ips: 9, ports: 45, services: 18, unique: 3 }, + { source: 'FOFA', icon: 'FF', subdomains: 24, ips: 8, ports: 52, services: 15, unique: 5 }, + { source: 'Shodan', icon: 'SH', subdomains: 20, ips: 11, ports: 38, services: 12, unique: 2 }, + { source: 'AI 合并去重', icon: 'AI', subdomains: 32, ips: 11, ports: 67, services: 21, unique: 0 }, +]; + +const MOCK_RISK: { label: string; value: number; color: string }[] = [ + { label: '严重', value: 3, color: '#ef4444' }, + { label: '高危', value: 8, color: '#f97316' }, + { label: '中危', value: 14, color: '#eab308' }, + { label: '低危', value: 22, color: '#22c55e' }, + { label: '无风险', value: 95, color: '#3b82f6' }, +]; + +const MOCK_CLASSIFICATION: { label: string; value: number; color: string }[] = [ + { label: '主站/官网', value: 6, color: '#00d4ff' }, + { label: 'API 服务', value: 11, color: '#6366f1' }, + { label: '管理后台', value: 4, color: '#ef4444' }, + { label: 'CDN/静态', value: 18, color: '#22c55e' }, + { label: '文档/知识库', value: 3, color: '#eab308' }, + { label: '三方托管', value: 7, color: '#8b5cf6' }, + { label: '邮件服务', value: 2, color: '#06b6d4' }, + { label: '测试/预发', value: 5, color: '#f97316' }, +]; + +const MOCK_TOP_RISK: AssetItem[] = [ + { id: '1', name: 'admin.example.com', type: '管理后台', ip: '203.0.113.10', ports: '443, 8080', risk: 'critical', source: 'FOFA', tag: '公网暴露无认证' }, + { id: '2', name: 'redis-prod.example.com', type: '缓存', ip: '203.0.113.55', ports: '6379', risk: 'critical', source: 'Shodan', tag: '未授权访问' }, + { id: '3', name: 'old-portal.example.com', type: 'Web', ip: '203.0.113.30', ports: '80, 443', risk: 'high', source: 'ThreatBook', tag: 'CVE-2024-4577' }, + { id: '4', name: 'jenkins.example.com', type: 'CI/CD', ip: '203.0.113.40', ports: '8080', risk: 'high', source: 'FOFA', tag: '弱口令风险' }, + { id: '5', name: 'api-v1.example.com', type: 'API', ip: '203.0.113.20', ports: '443', risk: 'high', source: 'ThreatBook', tag: 'TLS 1.0 弱协议' }, + { id: '6', name: 'db-admin.example.com', type: '数据库管理', ip: '203.0.113.60', ports: '3306, 5432', risk: 'medium', source: 'Shodan', tag: '端口暴露公网' }, + { id: '7', name: 'staging.example.com', type: '预发环境', ip: '203.0.113.70', ports: '443', risk: 'medium', source: 'FOFA', tag: '调试接口暴露' }, +]; + +const MOCK_TIMELINE: TimelineEvent[] = [ + { time: '14:32', type: 'added', asset: 'api-v2.example.com', detail: '新 API 服务上线 443/tcp', ai: true }, + { time: '12:15', type: 'changed', asset: 'console.example.com', detail: '证书变更 Let\'s Encrypt → DigiCert', ai: true }, + { time: '10:48', type: 'added', asset: '*.s3.amazonaws.com', detail: '新存储桶发现', ai: true }, + { time: '09:47', type: 'removed', asset: 'staging-old.example.com', detail: '已下线 需确认是否合规', ai: false }, + { time: '08:20', type: 'changed', asset: 'www.example.com', detail: 'HTTP → HTTPS 强制跳转', ai: true }, + { time: '07:05', type: 'added', asset: 'grafana.example.com', detail: 'AI 识别为影子资产 无人认领', ai: true }, +]; + +const RISK_TOTAL = MOCK_RISK.reduce((s, r) => s + r.value, 0); +const CLASS_TOTAL = MOCK_CLASSIFICATION.reduce((s, c) => s + c.value, 0); + +function DonutChart({ data, size = 140, thickness = 28 }: { data: { label: string; value: number; color: string }[]; size?: number; thickness?: number }) { + const total = data.reduce((s, d) => s + d.value, 0); + const radius = (size - thickness) / 2; + const center = size / 2; + const circumference = 2 * Math.PI * radius; + let offset = 0; + + return ( + + {data.map((d, i) => { + const ratio = total > 0 ? d.value / total : 0; + const dash = ratio * circumference; + const gap = total > 1 ? 1.5 : 0; + const strokeDash = Math.max(0, dash - gap); + const seg = ( + + ); + offset += dash; + return seg; + })} + {total} + 风险资产 + + ); +} + +function BarChart({ data, maxBars = 8 }: { data: { label: string; value: number; color: string }[]; maxBars?: number }) { + const maxVal = Math.max(...data.map(d => d.value), 1); + const barH = 20; + const gap = 8; + const labelW = 80; + const chartW = 180; + const h = data.length * (barH + gap) + 4; + + return ( + + {data.map((d, i) => { + const y = i * (barH + gap); + const w = (d.value / maxVal) * chartW; + return ( + + {d.label} + + {d.value} + + ); + })} + + ); +} + +function riskColor(r: string) { + const map: Record = { critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#22c55e' }; + return map[r] || '#6b7280'; +} + +function riskLabel(r: string) { + const map: Record = { critical: '严重', high: '高危', medium: '中危', low: '低危' }; + return map[r] || r; +} + +export default function Page() { + const [ready, setReady] = useState(false); + + useEffect(() => { setReady(true); }, []); + + const c = (n: number) => n >= 0 ? `+${n}` : `${n}`; + + return ( +
+ {/* ====== HEADER ====== */} +
+
+ 🌐 + 互联网资产测绘大屏 + AI 驱动 · 多源融合 +
+
+ 扫描对象: example.com + 最后扫描: 14 分钟前 + 数据源: ThreatBook + FOFA + Shodan +
+
+ + {/* ====== KPI ROW ====== */} +
+ + + + + + + +
+ + {/* ====== MAIN CONTENT ====== */} +
+ {/* 多源测绘数据对比 */} +
+
+ 多源测绘数据对比 + AI 合并去重 +
+ + + + + + + + + + + + + {MOCK_SOURCES.map((row, i) => ( + + + + + + + + + ))} + +
数据源子域IP端口服务独有
+ {row.icon} {row.source} + {row.subdomains}{row.ips}{row.ports}{row.services} 0 ? '#f59e0b' : '#64748b' }}>{row.unique > 0 ? row.unique : '—'}
+
+ AI 去重合并:3 源原始数据 187 条 → 唯一资产 142 条,去重率 24.1% +
+
+ + {/* 风险分布 */} +
+
+ 资产风险分布 + AI 自动打标 +
+
+ +
+ {MOCK_RISK.map((r, i) => ( +
+ + {r.label} + {r.value} +
+ ))} +
+
+
+
+ + {/* ====== SECOND ROW ====== */} +
+ {/* AI 自动分类 */} +
+
+ AI 自动资产分类 + 准确率 96.8% +
+ +
+ + {/* 高危资产列表 */} +
+
+ 高危资产 Top 7 + 需优先处置 +
+
+ + + + + + + + + + + {MOCK_TOP_RISK.map(a => ( + + + + + + + ))} + +
资产类型风险AI 标签
{a.name}{a.type} + + {riskLabel(a.risk)} + + {a.tag}
+
+
+
+ + {/* ====== AI 提效看板 + 变更时间线 ====== */} +
+ {/* AI 提效看板 */} +
+
+ AI 提效看板 +
+
+ + + + + + +
+
+ + {/* 变更时间线 */} +
+
+ 近期资产变更 + AI 实时感知 +
+
+ {MOCK_TIMELINE.map((e, i) => ( +
+ + {e.type === 'added' ? '+' : e.type === 'removed' ? '-' : '△'} + +
+
+ {e.asset} + {e.ai && AI} +
+
{e.detail}
+
+ {e.time} +
+ ))} +
+
+
+ + {/* ====== FOOTER ====== */} +
+ Flocks AI · 互联网资产测绘大屏 + 数据更新于 2 分钟前 + 资产来源: 公开测绘数据 (FOFA / Shodan / ThreatBook) · 非攻击性扫描 +
+ + {!ready &&
加载中...
} +
+ ); +} + +function KpiCard({ label, value, sub, color, highlight }: { label: string; value: string; sub: string; color: string; highlight?: boolean }) { + return ( +
+
+
{label}
+
{value}
+
{sub}
+
+ ); +} + +function EfficiencyItem({ icon, title, value, sub }: { icon: string; title: string; value: string; sub: string }) { + return ( +
+
{icon}
+
+
{title}
+
{value}
+
{sub}
+
+
+ ); +} + +const styles: Record = { + wrapper: { + minHeight: '100vh', + background: 'linear-gradient(135deg, #060b14 0%, #0a1628 40%, #0d1f3c 100%)', + padding: '20px 24px', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + color: '#e2e8f0', + position: 'relative', + }, + loading: { + position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', + background: 'rgba(6,11,20,0.85)', color: '#00d4ff', fontSize: 16, + }, + + /* ---- HEADER ---- */ + header: { + display: 'flex', justifyContent: 'space-between', alignItems: 'center', + padding: '14px 20px', marginBottom: 16, + background: 'rgba(12,24,48,0.7)', backdropFilter: 'blur(12px)', + border: '1px solid rgba(0,212,255,0.12)', borderRadius: 10, + }, + headerLeft: { display: 'flex', alignItems: 'center', gap: 12 }, + headerIcon: { fontSize: 22 }, + headerTitle: { fontSize: 18, fontWeight: 700, color: '#f1f5f9', letterSpacing: 1 }, + headerBadge: { + fontSize: 10, fontWeight: 600, padding: '2px 10px', borderRadius: 10, + background: 'linear-gradient(135deg, rgba(0,212,255,0.2), rgba(139,92,246,0.2))', + border: '1px solid rgba(0,212,255,0.3)', color: '#00d4ff', + }, + headerRight: { display: 'flex', gap: 20 }, + scanInfo: { fontSize: 11, color: '#64748b' }, + + /* ---- KPI ---- */ + kpiRow: { + display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 12, marginBottom: 16, + }, + kpiCard: { + position: 'relative', + background: 'rgba(12,24,48,0.7)', backdropFilter: 'blur(10px)', + border: '1px solid', borderRadius: 10, padding: '14px 16px', + overflow: 'hidden', + }, + kpiGlow: { position: 'absolute', inset: 0, pointerEvents: 'none' }, + kpiLabel: { fontSize: 11, color: '#64748b', marginBottom: 6, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 1 }, + kpiValue: { fontSize: 26, fontWeight: 800, fontFamily: 'monospace', lineHeight: 1.1 }, + kpiSub: { fontSize: 10, color: '#475569', marginTop: 4 }, + + /* ---- PANELS ---- */ + mainGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }, + panel: { + background: 'rgba(12,24,48,0.65)', backdropFilter: 'blur(10px)', + border: '1px solid rgba(255,255,255,0.06)', borderRadius: 10, + padding: 16, overflow: 'hidden', + }, + panelHeader: { + fontSize: 13, fontWeight: 700, color: '#cbd5e1', marginBottom: 14, + display: 'flex', alignItems: 'center', gap: 8, + }, + panelDot: { color: '#00d4ff', fontSize: 10 }, + panelTag: { + fontSize: 10, fontWeight: 600, padding: '1px 8px', borderRadius: 8, + background: 'rgba(0,212,255,0.12)', color: '#00d4ff', marginLeft: 'auto', + }, + + /* ---- TABLE ---- */ + table: { width: '100%', borderCollapse: 'collapse' }, + th: { + textAlign: 'left', padding: '6px 8px', fontSize: 10, fontWeight: 600, + color: '#64748b', borderBottom: '1px solid rgba(255,255,255,0.06)', textTransform: 'uppercase', + }, + td: { + padding: '7px 8px', fontSize: 11, borderBottom: '1px solid rgba(255,255,255,0.03)', color: '#cbd5e1', + }, + trHighlight: { background: 'rgba(0,212,255,0.06)' }, + thSmall: { padding: '5px 6px', fontSize: 10, fontWeight: 600, color: '#64748b', textAlign: 'left', borderBottom: '1px solid rgba(255,255,255,0.06)' }, + tdSmall: { padding: '5px 6px', fontSize: 11, color: '#cbd5e1', borderBottom: '1px solid rgba(255,255,255,0.03)' }, + sourceBadge: { + display: 'inline-block', width: 22, height: 18, lineHeight: '18px', textAlign: 'center', + fontSize: 9, fontWeight: 700, borderRadius: 3, marginRight: 6, + background: 'rgba(0,212,255,0.15)', color: '#00d4ff', + }, + tableHint: { + marginTop: 10, fontSize: 10, color: '#475569', textAlign: 'center', + borderTop: '1px solid rgba(255,255,255,0.04)', paddingTop: 8, + }, + badge: { + display: 'inline-block', padding: '1px 7px', borderRadius: 4, fontSize: 10, fontWeight: 600, + border: '1px solid', + }, + + /* ---- DONUT ---- */ + donutWrap: { display: 'flex', alignItems: 'center', gap: 24, justifyContent: 'center' }, + legendList: { display: 'flex', flexDirection: 'column', gap: 8 }, + legendItem: { display: 'flex', alignItems: 'center', gap: 8 }, + legendDot: { width: 8, height: 8, borderRadius: '50%', flexShrink: 0 }, + legendLabel: { fontSize: 11, color: '#94a3b8', width: 36 }, + legendVal: { fontSize: 12, fontWeight: 700, fontFamily: 'monospace', color: '#e2e8f0', width: 24, textAlign: 'right' }, + + /* ---- EFFICIENCY ---- */ + efficiencyGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }, + effItem: { + display: 'flex', gap: 10, padding: 10, borderRadius: 8, + background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.04)', + }, + effIcon: { fontSize: 18, flexShrink: 0, width: 28, textAlign: 'center' }, + effTitle: { fontSize: 11, color: '#94a3b8', marginBottom: 2 }, + effValue: { fontSize: 15, fontWeight: 700, color: '#00d4ff', fontFamily: 'monospace' }, + effSub: { fontSize: 10, color: '#475569', marginTop: 1 }, + + /* ---- TIMELINE ---- */ + timelineItem: { + display: 'flex', alignItems: 'flex-start', gap: 10, padding: '6px 0', + borderBottom: '1px solid rgba(255,255,255,0.03)', + }, + timelineIcon: { fontSize: 14, fontWeight: 700, width: 18, textAlign: 'center', flexShrink: 0 }, + timelineAsset: { fontSize: 12, fontWeight: 600, color: '#e2e8f0', display: 'flex', alignItems: 'center', gap: 6 }, + timelineDetail: { fontSize: 10, color: '#64748b', marginTop: 1 }, + timelineTime: { fontSize: 10, color: '#475569', flexShrink: 0 }, + aiTag: { + fontSize: 8, fontWeight: 700, padding: '0px 5px', borderRadius: 3, + background: 'linear-gradient(135deg, rgba(139,92,246,0.3), rgba(0,212,255,0.3))', + color: '#a78bfa', border: '1px solid rgba(139,92,246,0.3)', + }, + + /* ---- FOOTER ---- */ + footer: { + display: 'flex', justifyContent: 'space-between', padding: '10px 16px', + fontSize: 10, color: '#334155', borderTop: '1px solid rgba(255,255,255,0.04)', + }, +}; diff --git a/.flocks/plugins/contracts/webui/asset-mapping/src/index.tsx b/.flocks/plugins/contracts/webui/asset-mapping/src/index.tsx new file mode 100644 index 00000000..dd7caa63 --- /dev/null +++ b/.flocks/plugins/contracts/webui/asset-mapping/src/index.tsx @@ -0,0 +1,2 @@ +import Page from './Page'; +export default Page; diff --git a/.flocks/plugins/contracts/webui/cross-device-investigation/api/handlers.py b/.flocks/plugins/contracts/webui/cross-device-investigation/api/handlers.py new file mode 100644 index 00000000..bf218a4a --- /dev/null +++ b/.flocks/plugins/contracts/webui/cross-device-investigation/api/handlers.py @@ -0,0 +1,241 @@ +import random +from datetime import datetime, timezone + +DASHBOARD = { + "securityScore": 85, + "scoreTrend": [82, 84, 81, 86, 85, 83, 85], + "alertsToday": {"total": 47, "critical": 5, "high": 12, "medium": 18, "low": 12}, + "investigations": {"active": 8, "resolved": 119, "total": 127}, + "mttr": {"value": 23, "unit": "min", "trend": "down"}, + "mttd": {"value": 4, "unit": "min", "trend": "down"}, + "killChain": [ + {"phase": "侦察", "active": 12, "blocked": 45, "color": "#6366f1"}, + {"phase": "初始入侵", "active": 5, "blocked": 32, "color": "#ef4444"}, + {"phase": "执行", "active": 3, "blocked": 28, "color": "#f97316"}, + {"phase": "持久化", "active": 2, "blocked": 19, "color": "#eab308"}, + {"phase": "C2 通信", "active": 4, "blocked": 15, "color": "#8b5cf6"}, + {"phase": "横向移动", "active": 2, "blocked": 11, "color": "#06b6d4"}, + {"phase": "数据渗出", "active": 0, "blocked": 7, "color": "#ec4899"}, + ], + "activeCases": [ + {"id": "CS-0701-001", "title": "疑似 Webshell + 内网横向", "host": "web-prod-01", "ip": "10.10.15.22", + "progress": 77, "severity": "critical", "checklist": "48/62", "elapsed": "25min"}, + {"id": "CS-0702-002", "title": "DNS 隧道 + 数据外传", "host": "db-replica-02", "ip": "10.10.12.88", + "progress": 19, "severity": "high", "checklist": "12/62", "elapsed": "3h12min"}, + {"id": "CS-0702-003", "title": "异常 SSH 暴力破解", "host": "app-backend-03", "ip": "10.10.8.45", + "progress": 45, "severity": "medium", "checklist": "28/62", "elapsed": "1h45min"}, + {"id": "CS-0702-004", "title": "可疑 PowerShell 执行", "host": "dc-admin-01", "ip": "10.10.3.201", + "progress": 31, "severity": "high", "checklist": "19/62", "elapsed": "45min"}, + ], + "deviceHealth": [ + {"name": "NDR 流量检测", "id": "ndr", "health": 85, "status": "healthy", "alerts24h": 23, "latency": "12ms", "uptime": "99.97%", + "color": "#6366f1"}, + {"name": "HIDS 主机安全", "id": "hids", "health": 72, "status": "warning", "alerts24h": 15, "latency": "45ms", "uptime": "99.82%", + "color": "#10b981", "warning": "Agent 覆盖率 72% · 3 台主机未部署"}, + {"name": "防火墙", "id": "firewall", "health": 90, "status": "healthy", "alerts24h": 8, "latency": "5ms", "uptime": "99.99%", + "color": "#8b5cf6"}, + {"name": "EDR 终端响应", "id": "edr", "health": 78, "status": "healthy", "alerts24h": 19, "latency": "28ms", "uptime": "99.91%", + "color": "#06b6d4"}, + ], + "hotTargets": [ + {"ip": "10.10.15.22", "hostname": "web-prod-01", "attacks": 47, "severity": "critical", "trend": "up"}, + {"ip": "10.10.12.88", "hostname": "db-replica-02", "attacks": 32, "severity": "high", "trend": "up"}, + {"ip": "10.10.8.45", "hostname": "app-backend-03", "attacks": 28, "severity": "medium", "trend": "stable"}, + {"ip": "10.10.3.201", "hostname": "dc-admin-01", "attacks": 21, "severity": "high", "trend": "down"}, + {"ip": "10.10.19.7", "hostname": "file-srv-05", "attacks": 15, "severity": "medium", "trend": "stable"}, + ], + "recentIocs": [ + {"type": "ip", "value": "109.233.42.1", "label": "C2 服务器", "threat": "critical", "status": "blocked"}, + {"type": "domain", "value": "update.remote-c2.com", "label": "C2 域名 DGA", "threat": "critical", "status": "blocked"}, + {"type": "ip", "value": "198.51.100.23", "label": "C2 服务器", "threat": "high", "status": "monitoring"}, + {"type": "hash", "value": "a1b2c3d4e5f6", "label": "木马 MD5", "threat": "high", "status": "signatured"}, + {"type": "url", "value": "http://185.220.101.x/payload", "label": "载荷分发 URL", "threat": "high", "status": "investigating"}, + {"type": "file", "value": "/tmp/.sysupdate", "label": "后门文件", "threat": "critical", "status": "quarantined"}, + ], + "timeline": [ + {"time": "08:12", "event": "NDR 外联告警", "target": "10.10.15.22", "severity": "critical", "caseId": "CS-0701-001"}, + {"time": "08:15", "event": "HIDS Webshell 创建", "target": "10.10.15.22", "severity": "critical", "caseId": "CS-0701-001"}, + {"time": "08:23", "event": "EDR 横向移动告警", "target": "10.10.15.35", "severity": "critical", "caseId": "CS-0701-001"}, + {"time": "09:05", "event": "NDR DNS 异常检测", "target": "10.10.12.88", "severity": "high", "caseId": "CS-0702-002"}, + {"time": "10:22", "event": "HIDS SSH 暴力破解", "target": "10.10.8.45", "severity": "medium", "caseId": "CS-0702-003"}, + {"time": "11:08", "event": "EDR PowerShell 告警", "target": "10.10.3.201", "severity": "high", "caseId": "CS-0702-004"}, + {"time": "12:15", "event": "FW 端口扫描告警", "target": "10.10.15.22", "severity": "medium", "caseId": None}, + {"time": "13:42", "event": "NDR C2 外联检测", "target": "10.10.12.88", "severity": "high", "caseId": "CS-0702-002"}, + ], + "lastUpdated": datetime.now(timezone.utc).isoformat(), +} + +CHECKLIST = [ + {"id": "ph1", "label": "告警接入与优先级判定", "icon": "alert", + "children": [ + {"id": "ph1-1", "label": "告警源确认与去重", "children": [ + {"id": "ph1-1-1", "label": "确认告警触发设备类型(NDR / HIDS / FW / EDR)", "status": "passed"}, + {"id": "ph1-1-2", "label": "核实告警时间戳与日志完整性", "status": "passed"}, + {"id": "ph1-1-3", "label": "检查是否存在重复告警(去重)", "status": "passed"}, + {"id": "ph1-1-4", "label": "验证告警原始日志是否存在篡改痕迹", "status": "passed"}, + ]}, + {"id": "ph1-2", "label": "告警关联与上下文分析", "children": [ + {"id": "ph1-2-1", "label": "查询同时间窗口(±30min)内其他设备告警", "status": "passed", "note": "NDR 2+HIDS 1"}, + {"id": "ph1-2-2", "label": "提取并关联关键实体:IP、域名、进程名、用户账号", "status": "passed"}, + {"id": "ph1-2-3", "label": "检索历史同类告警记录,判断是否为已知误报", "status": "passed"}, + {"id": "ph1-2-4", "label": "查询告警源 IP 的历史行为基线", "status": "passed"}, + ]}, + {"id": "ph1-3", "label": "威胁评分与优先级判定", "children": [ + {"id": "ph1-3-1", "label": "基于 MITRE ATT&CK 进行技战术预标注", "status": "passed"}, + {"id": "ph1-3-2", "label": "结合资产关键度计算威胁评分", "status": "passed", "note": "78"}, + {"id": "ph1-3-3", "label": "判定调查优先级(P0-P3)", "status": "passed", "note": "P0"}, + ]}, + ]}, + {"id": "ph2", "label": "多设备并发取证", "icon": "server", + "children": [ + {"id": "ph2-1", "label": "取证范围定义", "children": [ + {"id": "ph2-1-1", "label": "确定需查询的设备类型与覆盖范围", "status": "passed"}, + {"id": "ph2-1-2", "label": "设定取证时间窗口(含前置回溯窗口)", "status": "passed", "note": "±30min"}, + {"id": "ph2-1-3", "label": "定义取证关键字段", "status": "passed"}, + ]}, + {"id": "ph2-2", "label": "网络层取证 NDR", "children": [ + {"id": "ph2-2-1", "label": "检索目标 IP 的全量会话记录", "status": "passed"}, + {"id": "ph2-2-2", "label": "导出关键会话 PCAP(不可篡改原始证据)", "status": "passed"}, + {"id": "ph2-2-3", "label": "解析 TLS/HTTP/DNS/SMB 协议载荷", "status": "passed"}, + {"id": "ph2-2-4", "label": "提取 JA3/JARM 指纹并与威胁情报比对", "status": "passed", "note": "命中 C2"}, + ]}, + {"id": "ph2-3", "label": "终端层取证 HIDS / EDR", "children": [ + {"id": "ph2-3-1", "label": "提取目标主机进程树与父子关系", "status": "passed"}, + {"id": "ph2-3-2", "label": "检索文件系统变更记录", "status": "passed"}, + {"id": "ph2-3-3", "label": "提取网络连接与 DNS 查询记录", "status": "passed"}, + {"id": "ph2-3-4", "label": "查询计划任务 / 服务 / 启动项变更", "status": "passed"}, + ]}, + {"id": "ph2-4", "label": "边界层取证 防火墙", "children": [ + {"id": "ph2-4-1", "label": "检索出站/入站会话日志", "status": "passed"}, + {"id": "ph2-4-2", "label": "逐跳还原 NAT 转换链路", "status": "passed"}, + {"id": "ph2-4-3", "label": "审查涉及的安全策略与放行规则", "status": "warning", "note": "POL-1024 过宽"}, + ]}, + {"id": "ph2-5", "label": "取证数据质量校验", "children": [ + {"id": "ph2-5-1", "label": "检查各设备时间戳 NTP 偏移量", "status": "passed"}, + {"id": "ph2-5-2", "label": "验证日志字段完整性(8 核心字段)", "status": "passed"}, + {"id": "ph2-5-3", "label": "识别数据缺失设备并标注覆盖率盲区", "status": "warning", "note": "10.10.15.35 无 Agent"}, + ]}, + ]}, + {"id": "ph3", "label": "时间线重建与因果分析", "icon": "clock", + "children": [ + {"id": "ph3-1", "label": "时间线构建", "children": [ + {"id": "ph3-1-1", "label": "统一所有设备时区至 UTC+8", "status": "passed"}, + {"id": "ph3-1-2", "label": "按时间戳正序排列所有事件", "status": "passed"}, + {"id": "ph3-1-3", "label": "标记关键时间节点", "status": "passed"}, + ]}, + {"id": "ph3-2", "label": "因果链推断", "children": [ + {"id": "ph3-2-1", "label": "分析事件先后关系与触发依赖", "status": "passed"}, + {"id": "ph3-2-2", "label": "识别父进程衍生链", "status": "passed"}, + {"id": "ph3-2-3", "label": "检测时间序列异常", "status": "passed"}, + ]}, + {"id": "ph3-3", "label": "Kill Chain 阶段判定", "children": [ + {"id": "ph3-3-1", "label": "标注 Initial Access", "status": "passed"}, + {"id": "ph3-3-2", "label": "标注 Execution", "status": "passed"}, + {"id": "ph3-3-3", "label": "标注 C2 通信", "status": "passed"}, + {"id": "ph3-3-4", "label": "标注 Lateral Movement", "status": "passed"}, + {"id": "ph3-3-5", "label": "标注 Persistence", "status": "passed"}, + ]}, + ]}, + {"id": "ph4", "label": "攻击链路还原与 IOC 提取", "icon": "link", + "children": [ + {"id": "ph4-1", "label": "完整攻击路径重建", "children": [ + {"id": "ph4-1-1", "label": "定位初始入侵向量", "status": "passed", "note": "PHP 文件上传"}, + {"id": "ph4-1-2", "label": "还原横向移动跳板路径", "status": "passed"}, + {"id": "ph4-1-3", "label": "绘制完整攻击链路图", "status": "passed"}, + ]}, + {"id": "ph4-2", "label": "IOC 提取与分类", "children": [ + {"id": "ph4-2-1", "label": "提取网络层 IOC:IP / 域名 / URL", "status": "passed"}, + {"id": "ph4-2-2", "label": "提取主机层 IOC:文件路径 / 哈希", "status": "passed"}, + {"id": "ph4-2-3", "label": "IOC 威胁等级评估与情报关联", "status": "passed"}, + {"id": "ph4-2-4", "label": "IOC 入库并推送至边界设备黑名单", "status": "pending"}, + ]}, + {"id": "ph4-3", "label": "MITRE ATT&CK 完整映射", "children": [ + {"id": "ph4-3-1", "label": "映射所有已知技战术 ID", "status": "passed", "note": "8 条"}, + {"id": "ph4-3-2", "label": "评估每项技战术的置信度", "status": "passed"}, + {"id": "ph4-3-3", "label": "识别检测盲区(未覆盖 ATT&CK 技战术)", "status": "passed"}, + ]}, + ]}, + {"id": "ph5", "label": "影响范围评估与风险定级", "icon": "target", + "children": [ + {"id": "ph5-1", "label": "受影响资产盘点", "children": [ + {"id": "ph5-1-1", "label": "确定已失陷主机清单", "status": "passed", "note": "1 台"}, + {"id": "ph5-1-2", "label": "确定疑似受影响主机清单", "status": "passed", "note": "2 台"}, + {"id": "ph5-1-3", "label": "标注每台资产的关键度与业务归属", "status": "passed"}, + ]}, + {"id": "ph5-2", "label": "业务影响分析", "children": [ + {"id": "ph5-2-1", "label": "评估对核心业务的可用性影响", "status": "warning", "note": "高风险"}, + {"id": "ph5-2-2", "label": "评估数据泄露风险(含数据流向追踪)", "status": "warning", "note": "中风险"}, + {"id": "ph5-2-3", "label": "计算 Blast Radius 影响半径", "status": "passed"}, + ]}, + {"id": "ph5-3", "label": "风险定级", "children": [ + {"id": "ph5-3-1", "label": "综合威胁评分与业务影响进行风险定级", "status": "passed", "note": "78"}, + {"id": "ph5-3-2", "label": "输出风险报告摘要", "status": "passed"}, + ]}, + ]}, + {"id": "ph6", "label": "处置建议与闭环反馈", "icon": "zap", + "children": [ + {"id": "ph6-1", "label": "紧急处置 Containment", "children": [ + {"id": "ph6-1-1", "label": "边界防火墙封禁 C2 IP 与域名", "status": "pending"}, + {"id": "ph6-1-2", "label": "隔离已失陷主机网络通信", "status": "pending"}, + ]}, + {"id": "ph6-2", "label": "根除与恢复", "children": [ + {"id": "ph6-2-1", "label": "委派主机取证 Agent 到失陷主机", "status": "pending"}, + {"id": "ph6-2-2", "label": "清除 Webshell 与后门文件", "status": "pending"}, + {"id": "ph6-2-3", "label": "修补 PHP 文件上传漏洞", "status": "pending"}, + ]}, + {"id": "ph6-3", "label": "加固与预防", "children": [ + {"id": "ph6-3-1", "label": "创建东西向 SMB 隔离策略", "status": "pending"}, + {"id": "ph6-3-2", "label": "收窄防火墙出站策略", "status": "pending"}, + {"id": "ph6-3-3", "label": "补充未覆盖主机 Agent 部署", "status": "pending"}, + ]}, + {"id": "ph6-4", "label": "闭环反馈", "children": [ + {"id": "ph6-4-1", "label": "更新 SIEM 检测规则(IOC + 行为特征)", "status": "pending"}, + {"id": "ph6-4-2", "label": "更新 MITRE ATT&CK 覆盖矩阵", "status": "pending"}, + {"id": "ph6-4-3", "label": "归档威胁狩猎 Hypothesis", "status": "pending"}, + {"id": "ph6-4-4", "label": "输出标准化溯源报告", "status": "pending"}, + ]}, + ]}, +] + +CASES = [ + {"id": "CS-0701-001", "title": "疑似 Webshell + 内网横向移动", "status": "resolved", "severity": "critical", + "createdAt": "2026-07-01T08:05:00+08:00", + "target": {"ip": "10.10.15.22", "hostname": "web-prod-01", "role": "Web 服务器", "sector": "核心业务"}, + "summary": {"threatScore": 78, "affectedHosts": 3}, + "checklist": CHECKLIST, "checklistProgress": {"total": 62, "passed": 48, "warning": 4, "pending": 10}}, + {"id": "CS-0702-002", "title": "DNS 隧道 + 数据外传可疑", "status": "investigating", "severity": "high", + "createdAt": "2026-07-02T14:10:00+08:00", + "target": {"ip": "10.10.12.88", "hostname": "db-replica-02", "role": "数据库从库", "sector": "数据平台"}, + "summary": {"threatScore": 62, "affectedHosts": 1}, + "checklist": CHECKLIST, "checklistProgress": {"total": 62, "passed": 12, "warning": 2, "pending": 48}}, +] + +CASES_STORE = {c["id"]: c for c in CASES} + + +async def get_dashboard(ctx, request): + return DASHBOARD + + +async def get_cases(ctx, request): + return {"cases": [{"id": c["id"], "title": c["title"], "status": c["status"], + "severity": c["severity"], "createdAt": c["createdAt"], + "target": c["target"], "summary": c.get("summary")} for c in CASES]} + + +async def get_case(ctx, request): + case_id = None + try: + if hasattr(request, 'path_params') and request.path_params: + case_id = request.path_params.get("id") + except Exception: + pass + if not case_id: + try: + case_id = str(request.url.path).rstrip('/').split('/')[-1] + except Exception: + pass + c = CASES_STORE.get(case_id) if case_id else None + if c and not c.get("checklist"): + c["checklist"] = CHECKLIST + return {"ok": True, "case": c} if c else {"ok": False, "error": "案件不存在"} \ No newline at end of file diff --git a/.flocks/plugins/contracts/webui/cross-device-investigation/api/routes.yaml b/.flocks/plugins/contracts/webui/cross-device-investigation/api/routes.yaml new file mode 100644 index 00000000..16c359e5 --- /dev/null +++ b/.flocks/plugins/contracts/webui/cross-device-investigation/api/routes.yaml @@ -0,0 +1,15 @@ +routes: + - method: GET + path: /dashboard + handler: handlers.get_dashboard + timeoutMs: 5000 + + - method: GET + path: /cases + handler: handlers.get_cases + timeoutMs: 5000 + + - method: GET + path: /cases/{id} + handler: handlers.get_case + timeoutMs: 5000 \ No newline at end of file diff --git a/.flocks/plugins/contracts/webui/cross-device-investigation/manifest.json b/.flocks/plugins/contracts/webui/cross-device-investigation/manifest.json new file mode 100644 index 00000000..62246f70 --- /dev/null +++ b/.flocks/plugins/contracts/webui/cross-device-investigation/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "cross-device-investigation", + "title": "跨设备分析调查", + "route": "/contracts/webui/cross-device-investigation", + "icon": "Network", + "order": 100, + "enabled": true, + "placement": "home.after", + "entry": "src/index.tsx", + "updatedAt": 0 +} diff --git a/.flocks/plugins/contracts/webui/cross-device-investigation/src/Page.tsx b/.flocks/plugins/contracts/webui/cross-device-investigation/src/Page.tsx new file mode 100644 index 00000000..0d817f9b --- /dev/null +++ b/.flocks/plugins/contracts/webui/cross-device-investigation/src/Page.tsx @@ -0,0 +1,505 @@ +import { useState, useEffect, useCallback } from 'react'; + +const API = '/api/contracts/webui/pages/cross-device-investigation/api'; + +function Icon({ n, s }: { n: string; s?: string }) { + const c = s || 'w-4 h-4'; + const paths: Record = { + shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z', + alert: 'M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4M12 17h.01', + check: 'M20 6 9 17l-5-5', + activity: 'M22 12h-4l-3 9L9 3l-3 9H2', + target: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM12 6a6 6 0 1 0 0 12 6 6 0 0 0 0-12zM12 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4z', + clock: 'M12 6v6l4 2M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z', + server: 'M5 2h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zM5 14h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2z', + barChart: 'M18 20V10M12 20V4M4 20v-4', + zap: 'M13 2 3 14h9l-1 8 10-12h-9l1-8z', + search: 'M21 21l-4.35-4.35M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z', + layers: 'M12 2 2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5', + }; + return ; +} + +const SEV_L: Record = { critical: '严重', high: '高危', medium: '中危', low: '低危' }; + +export default function Page() { + const [d, setD] = useState(null); + const [cases, setCases] = useState([]); + const [selId, setSelId] = useState(null); + const [selCase, setSelCase] = useState(null); + const [loading, setLoading] = useState(true); + const [dtLoad, setDtLoad] = useState(false); + const [view, setView] = useState<'dashboard' | 'cases'>('dashboard'); + const [ts, setTs] = useState(''); + + const fetchAll = useCallback(async () => { + try { + const [dr, cr] = await Promise.all([fetch(`${API}/dashboard`), fetch(`${API}/cases`)]); + setD(await dr.json()); setCases((await cr.json()).cases || []); + setTs(new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })); + } catch {} finally { setLoading(false); } + }, []); + useEffect(() => { fetchAll(); const i = setInterval(() => setTs(new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })), 30000); return () => clearInterval(i); }, [fetchAll]); + + const selectCase = async (id: string) => { + setSelId(id); setDtLoad(true); setSelCase(null); + try { const r = await fetch(`${API}/cases/${encodeURIComponent(id)}`); const data = await r.json(); if (data.ok) setSelCase(data.case); } catch {} finally { setDtLoad(false); } + }; + + if (loading) return
; + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

跨设备安全调查大屏

+

Cross-Device Investigation Command Center

+
+
+
+
+ {(['dashboard','cases'] as const).map(k => ( + + ))} +
+
+ + {ts} +
+
+
+ + {view === 'dashboard' && d && } + {view === 'cases' && } +
+ ); +} + +/* ================================================ + DASHBOARD + ================================================ */ +function DashboardView({ d }: { d: any }) { + return ( +
+ {/* Row 1 – Metric Cards */} +
+ = 80 ? '#10b981' : d.securityScore >= 60 ? '#f59e0b' : '#ef4444'} + icon="shield" sub={`较昨日 +3`} + /> + + + + +
+ + {/* Row 2 – Kill Chain (60%) + Active Cases (40%) */} +
+ + +
+ + {/* Row 3 – Devices + Targets + IOC */} +
+ + + +
+ + {/* Row 4 – Timeline */} + +
+ ); +} + +/* ---------- Metric Card ---------- */ +function MetricCard({ label, value, unit, color, icon, sub, subColors, subLabels }: any) { + return ( +
+
+ +
+
+
{label}
+
+ {value} + {unit} +
+ {sub && !subColors &&
{sub}
} + {subColors && ( +
+ {(sub as string[]).map((s, i) => ( + + {s}{subLabels[i]} + + ))} +
+ )} +
+
+ ); +} + +/* ---------- Kill Chain ---------- */ +function KillChainPanel({ data }: { data: any[] }) { + const phases = data || []; + const max = Math.max(...phases.map(p => p.blocked || 0), 1); + return ( +
+
+

+ 攻击链路态势 +

+
+ 活跃 + 已阻断 +
+
+
+ {phases.map((p: any, i: number) => { + const ah = Math.max((p.active || 0) / max * 112, 3); + const bh = Math.max((p.blocked || 0) / max * 112, 3); + return ( +
+
+
+
+
+
+ {p.active} + {p.phase} +
+ ); + })} +
+
+ ); +} + +/* ---------- Active Cases ---------- */ +function ActivePanel({ cases }: { cases: any[] }) { + const items = cases || []; + const sevDot: Record = { critical: 'bg-rose-500', high: 'bg-amber-500', medium: 'bg-yellow-500' }; + return ( +
+

+ 在查案件进度 +

+
+ {items.map((c: any, i: number) => { + const bc = c.progress >= 70 ? 'from-emerald-500 to-emerald-400' : c.progress >= 40 ? 'from-amber-500 to-amber-400' : 'from-blue-500 to-blue-400'; + return ( +
+
+
+ + {c.host} + {c.ip} +
+ {c.elapsed} +
+
+
+
=70?'52,211,153':c.progress>=40?'245,158,11':'59,130,246'},0.4)` }} /> +
+ {c.progress}% +
+
+ ); + })} +
+
+ ); +} + +/* ---------- Device Panel ---------- */ +function DevicePanel({ devices }: { devices: any[] }) { + const items = devices || []; + const colors: Record = { ndr: '#818cf8', hids: '#34d399', firewall: '#a78bfa', edr: '#22d3ee' }; + return ( +
+

+ 设备健康度 +

+
+ {items.map((d: any, i: number) => { + const c = colors[d.id] || '#94a3b8'; + return ( +
+
+
+ + {d.name} +
+ {d.health}% +
+
+
+
+
+ {d.alerts24h} 告警{d.latency}{d.uptime} +
+
+ ); + })} +
+
+ ); +} + +/* ---------- Hot Targets ---------- */ +function TargetPanel({ targets }: { targets: any[] }) { + const items = targets || []; + const max = Math.max(...items.map(t => t.attacks || 0), 1); + return ( +
+

+ 热点攻击目标 +

+
+ {items.map((t: any, i: number) => { + const c = t.severity==='critical'?'#f43f5e':t.severity==='high'?'#f97316':'#818cf8'; + return ( +
+ {t.ip} +
+
+
+ {t.attacks} +
+ ); + })} +
+
+ ); +} + +/* ---------- IOC ---------- */ +function IOCPanel({ iocs }: { iocs: any[] }) { + const items = iocs || []; + const tc: Record = { ip:'text-blue-400', domain:'text-purple-400', url:'text-orange-400', hash:'text-emerald-400', file:'text-rose-400' }; + const sc: Record = { + blocked: 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20', + monitoring: 'text-amber-400 bg-amber-500/10 border-amber-500/20', + signatured: 'text-indigo-400 bg-indigo-500/10 border-indigo-500/20', + investigating: 'text-blue-400 bg-blue-500/10 border-blue-500/20', + quarantined: 'text-rose-400 bg-rose-500/10 border-rose-500/20', + }; + const sl: Record = { blocked:'已封禁', monitoring:'监控中', signatured:'已入库', investigating:'调查中', quarantined:'已隔离' }; + return ( +
+

+ 近期 IOC +

+
+ {items.map((io: any, i: number) => ( +
+ {io.type} + {io.value} + {sl[io.status]||io.status} +
+ ))} +
+
+ ); +} + +/* ---------- Timeline ---------- */ +function TimelinePanel({ events }: { events: any[] }) { + const items = events || []; + const dc: Record = { critical:'bg-rose-500', high:'bg-amber-500', medium:'bg-yellow-500' }; + return ( +
+
+ + 调查事件时间线 + {items.length} events +
+
+
+ {items.map((e: any, i: number) => ( +
+ + {e.time} + {e.event} + {e.target} +
+ ))} +
+
+ ); +} + +/* ================================================ + CASES VIEW + ================================================ */ +function CasesView({ cases, selId, selCase, dtLoad, onSelect }: any) { + return ( +
+
+
+ 案件列表 + ({cases.length}) +
+
+ {cases.map((c: any) => { + const active = selId === c.id; + return ( + + ); + })} +
+
+
+ {!selId ? ( +
+ +

选择左侧案件查看检查清单

+
+ ) : dtLoad ? ( +
+
+
+ ) : selCase ? ( + + ) : ( +
+

加载失败

+ +
+ )} +
+
+ ); +} + +/* ---------- Case Detail + Checklist ---------- */ +function CaseDetail({ c }: { c: any }) { + const cl = c.checklist || []; + const cp = c.checklistProgress || { total: 62, passed: 0, warning: 0, pending: 62 }; + const pct = cp.total ? Math.round((cp.passed/cp.total)*100) : 0; + + return ( +
+
+
+
+
+ {c.status==='resolved'?'已处置':'调查中'} + {SEV_L[c.severity]}优先级 +
+

{c.title}

+

{c.target?.ip} ({c.target?.hostname}) · {c.target?.role} · {c.target?.sector}

+
+
+
{c.summary?.threatScore}
评分
+
+
+
{cp.passed}
通过
+
{cp.warning}
告警
+
{cp.pending}
待办
+
+
+
+
+
+
+
+ {pct}% +
+
+ {cl.length > 0 && ( +
+
+ + 调查检查清单 + {cp.total} 项 +
+
+ {cl.map((ph: any, pi: number) => )} +
+
+ )} +
+ ); +} + +/* Checklist Tree */ +const PHB: Record = { 0: 'border-l-rose-500', 1: 'border-l-amber-500', 2: 'border-l-emerald-500', 3: 'border-l-indigo-500', 4: 'border-l-violet-500', 5: 'border-l-cyan-500' }; + +function PhaseItem({ item, idx }: { item: any; idx: number }) { + const [open, setOpen] = useState(idx === 0); + const cnt = cnt(item.children || []); + return ( +
+ + {open && item.children &&
{item.children.map((ch: any) => ch.children ? : )}
} +
+ ); +} +function GroupItem({ item }: { item: any }) { + const [open, setOpen] = useState(true); + const cnts = cnt(item.children || []); + return ( +
+ + {open && item.children &&
{item.children.map((lf: any) => )}
} +
+ ); +} +function LeafItem({ item }: { item: any }) { + const s = item.status || 'pending'; + const d = s==='passed'?'bg-emerald-500':s==='warning'?'bg-amber-500':'bg-slate-600'; + return ( +
+ + {item.label} + {item.note && {item.note}} +
+ ); +} +function cnt(children: any[]) { + let p=0,w=0,pd=0,t=0; + (function walk(items:any[]){for(const it of items){t++;if(it.status==='passed')p++;else if(it.status==='warning')w++;else if(it.status==='pending')pd++;if(it.children)walk(it.children);}})(children); + return {passed:p,warning:w,pending:pd,total:t}; +} \ No newline at end of file diff --git a/.flocks/plugins/contracts/webui/cross-device-investigation/src/index.tsx b/.flocks/plugins/contracts/webui/cross-device-investigation/src/index.tsx new file mode 100644 index 00000000..8c1408bb --- /dev/null +++ b/.flocks/plugins/contracts/webui/cross-device-investigation/src/index.tsx @@ -0,0 +1,2 @@ +import Page from './Page'; +export default Page; \ No newline at end of file diff --git a/.flocks/plugins/contracts/webui/dlp-audit-dashboard/manifest.json b/.flocks/plugins/contracts/webui/dlp-audit-dashboard/manifest.json new file mode 100644 index 00000000..e894400c --- /dev/null +++ b/.flocks/plugins/contracts/webui/dlp-audit-dashboard/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "dlp-audit-dashboard", + "title": "DLP外发审计大屏", + "route": "/contracts/webui/dlp-audit-dashboard", + "icon": "ShieldAlert", + "order": 100, + "enabled": true, + "placement": "home.after", + "entry": "src/index.tsx", + "updatedAt": 0 +} diff --git a/.flocks/plugins/contracts/webui/dlp-audit-dashboard/src/Page.tsx b/.flocks/plugins/contracts/webui/dlp-audit-dashboard/src/Page.tsx new file mode 100644 index 00000000..317262ef --- /dev/null +++ b/.flocks/plugins/contracts/webui/dlp-audit-dashboard/src/Page.tsx @@ -0,0 +1,274 @@ +import { useEffect, useState, useCallback } from 'react'; +import { Card } from '@flocks/webui-contract-sdk'; +import { + KpiCard, DonutChart, PieChart, LineChart, + HorizontalBar, HeatmapGrid, Gauge, Timeline, +} from './charts'; + +/* ====== Types ====== */ +interface ChannelItem { name: string; value: number; color: string; } +interface FileTypeItem { name: string; value: number; color: string; } +interface HeatmapCell { dept: string; high: number; medium: number; low: number; } +interface BarItem { name: string; dept: string; value: number; risk: 'high' | 'medium' | 'low'; } +interface TimelineItem { time: string; user: string; dept: string; action: string; risk: 'high' | 'medium' | 'low'; status: string; } + +/* ====== Constants ====== */ +const C = { + green: '#10b981', + yellow: '#f59e0b', + orange: '#f97316', + red: '#ef4444', + blue: '#3b82f6', + purple: '#8b5cf6', + cyan: '#06b6d4', +}; + +/* ====== Mock Data Generators ====== */ +function genChannelData(): ChannelItem[] { + return [ + { name: '邮件外发', value: 423, color: C.blue }, + { name: '企业云盘', value: 348, color: C.purple }, + { name: '即时通讯', value: 312, color: C.orange }, + { name: 'USB拷贝', value: 164, color: C.red }, + ]; +} + +function genFileTypeData(): FileTypeItem[] { + return [ + { name: '合同文件', value: 318, color: C.orange }, + { name: '客户资料', value: 212, color: C.yellow }, + { name: '源代码', value: 185, color: C.red }, + { name: '财务数据', value: 97, color: C.blue }, + { name: '其他文件', value: 435, color: '#64748b' }, + ]; +} + +function genDeptHeatmap(): HeatmapCell[] { + return [ + { dept: '研发部', high: 5, medium: 12, low: 47 }, + { dept: '销售部', high: 9, medium: 14, low: 35 }, + { dept: '财务部', high: 3, medium: 6, low: 18 }, + { dept: '市场部', high: 2, medium: 9, low: 29 }, + { dept: '运营部', high: 1, medium: 4, low: 23 }, + { dept: '人力资源', high: 1, medium: 3, low: 16 }, + { dept: '法务部', high: 0, medium: 1, low: 10 }, + { dept: '管理层', high: 2, medium: 7, low: 14 }, + ]; +} + +function genTopEmployees(): BarItem[] { + return [ + { name: '张*明', dept: '研发部', value: 47, risk: 'high' }, + { name: '李*华', dept: '销售部', value: 38, risk: 'high' }, + { name: '王*芳', dept: '财务部', value: 31, risk: 'medium' }, + { name: '陈*强', dept: '研发部', value: 28, risk: 'high' }, + { name: '刘*婷', dept: '市场部', value: 24, risk: 'medium' }, + { name: '赵*刚', dept: '销售部', value: 22, risk: 'high' }, + { name: '周*杰', dept: '研发部', value: 19, risk: 'medium' }, + { name: '吴*丽', dept: '财务部', value: 16, risk: 'low' }, + { name: '孙*强', dept: '管理层', value: 14, risk: 'high' }, + { name: '郑*敏', dept: '运营部', value: 12, risk: 'low' }, + ]; +} + +function genHourlyTrend() { + const hours = Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, '0')}:00`); + const counts = [12,8,5,3,2,4,8,18,35,52,68,75,82,78,65,72,58,45,38,25,18,14,10,8]; + const ratios = [8,5,3,2,1,2,5,10,18,22,25,28,26,24,20,25,22,18,15,12,10,8,6,5]; + return { hours, counts, ratios }; +} + +function genDailyTrend() { + const days = ['06/25','06/26','06/27','06/28','06/29','06/30','07/01']; + const counts = [1180,1245,1320,1100,980,1150,1247]; + const ratios = [22,25,28,20,18,24,23]; + return { days, counts, ratios }; +} + +function genTimeline(): TimelineItem[] { + return [ + { time: '14:32:15', user: '李*华', dept: '销售部', action: '外发客户名单至 personal@163.com', risk: 'high', status: '已处置' }, + { time: '14:18:40', user: '张*明', dept: '研发部', action: '上传源代码压缩包至个人云盘', risk: 'high', status: '处置中' }, + { time: '14:05:22', user: '王*芳', dept: '财务部', action: '发送财务报表至外部审计邮箱', risk: 'medium', status: '待复核' }, + { time: '13:48:10', user: '陈*强', dept: '研发部', action: '通过IM外发API密钥文档', risk: 'high', status: '已处置' }, + { time: '13:30:05', user: '赵*刚', dept: '销售部', action: '外发合同扫描件至合作方', risk: 'high', status: '已处置' }, + { time: '13:15:33', user: '刘*婷', dept: '市场部', action: '上传营销方案至企业云盘个人区', risk: 'medium', status: '已处置' }, + { time: '12:58:18', user: '郑*敏', dept: '运营部', action: '邮件外发运营数据报表', risk: 'low', status: '自动忽略' }, + { time: '12:42:50', user: '孙*强', dept: '管理层', action: 'USB拷贝战略规划文档', risk: 'high', status: '已处置' }, + { time: '12:20:11', user: '吴*丽', dept: '财务部', action: '发送工资明细至HR系统', risk: 'low', status: '自动忽略' }, + { time: '11:55:40', user: '周*杰', dept: '研发部', action: '上传技术方案至GitHub私有仓库', risk: 'medium', status: '已处置' }, + { time: '11:30:08', user: '张*明', dept: '研发部', action: '外发离职交接文档至竞对公司域名', risk: 'high', status: '已处置' }, + { time: '11:05:19', user: '李*华', dept: '销售部', action: '发送报价单至未授权供应商', risk: 'medium', status: '待复核' }, + ]; +} + +/* ====== Main Page ====== */ +export default function Page() { + const [time, setTime] = useState(new Date()); + const [trendMode, setTrendMode] = useState<'24h' | '7d'>('24h'); + const [kpi, setKpi] = useState({ total: 1247, highRisk: 23, disposed: 18, pending: 5, autoRate: 78 }); + + /* Simulated live refresh */ + useEffect(() => { + const t = setInterval(() => setTime(new Date()), 1000); + return () => clearInterval(t); + }, []); + + useEffect(() => { + const t = setInterval(() => { + setKpi((prev) => ({ + total: prev.total + Math.floor(Math.random() * 3), + highRisk: Math.max(0, prev.highRisk + (Math.random() > 0.7 ? 1 : 0)), + disposed: prev.disposed + (Math.random() > 0.6 ? 1 : 0), + pending: Math.max(0, prev.pending + (Math.random() > 0.5 ? 1 : -1)), + autoRate: Math.min(100, Math.max(60, prev.autoRate + (Math.random() - 0.5) * 2)), + })); + }, 8000); + return () => clearInterval(t); + }, []); + + /* Static data */ + const [channelData] = useState(genChannelData); + const [fileTypeData] = useState(genFileTypeData); + const [deptHeatmap] = useState(genDeptHeatmap); + const [topEmployees] = useState(genTopEmployees); + const [timeline] = useState(genTimeline); + const hourlyTrend = genHourlyTrend(); + const dailyTrend = genDailyTrend(); + + const gaugeValue = 32; + + const fmt = (d: Date) => + `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`; + + return ( +
+ {/* ===== Header ===== */} +
+
+
+

DLP 员工外发合规态势

+ | + 云枢 DLP · 智能审计 +
+
+
+ + 实时监控中 +
+ {fmt(time)} +
+
+ + {/* ===== Content ===== */} +
+ + {/* KPI Row */} +
+ + + + 5 ? C.orange : C.yellow} sub="三级·人工必审" /> + +
+ + {/* Row: 外发渠道 | 外发文件类型 | 外发趋势 — 三列横排 */} +
+ + + + + + + + + + 外发趋势 +
+ {(['24h', '7d'] as const).map((m) => ( + + ))} +
+
+ }> + + +
+ + {/* Row: Heatmap + Top10 等宽两列 */} +
+ + + + + + +
+ + {/* Row: 误报噪声占比 | 实时处置流 — 横向排列 */} +
+
+ +
+ +
+ 优秀 + 关注 + 警告 +
+
+
+
+
+ + + +
+
+ + {/* Footer */} +
+ 数据来源:云枢 DLP · Flocks yunshu_dlp_audit Workflow · 模拟数据 +
+
+
+ ); +} diff --git a/.flocks/plugins/contracts/webui/dlp-audit-dashboard/src/charts.tsx b/.flocks/plugins/contracts/webui/dlp-audit-dashboard/src/charts.tsx new file mode 100644 index 00000000..ea161242 --- /dev/null +++ b/.flocks/plugins/contracts/webui/dlp-audit-dashboard/src/charts.tsx @@ -0,0 +1,533 @@ +import React from 'react'; + +/* ====== Color Palette ====== */ +const COLORS = { + green: '#10b981', + yellow: '#f59e0b', + orange: '#f97316', + red: '#ef4444', + blue: '#3b82f6', + purple: '#8b5cf6', + cyan: '#06b6d4', + gray: '#64748b', + bg: '#0a0e27', +}; + +/* ====== DonutChart ====== */ +interface DonutItem { + name: string; + value: number; + color: string; +} + +export function DonutChart({ data, size = 200, thickness = 28, label }: { + data: DonutItem[]; + size?: number; + thickness?: number; + label?: string; +}) { + const cx = size / 2; + const cy = size / 2; + const r = (size - thickness) / 2; + const circumference = 2 * Math.PI * r; + const total = data.reduce((s, d) => s + d.value, 0); + let offset = 0; + + if (total === 0) return null; + + return ( +
+ + {data.map((item, i) => { + const pct = item.value / total; + const dash = pct * circumference; + const seg = ( + + ); + offset += dash; + return seg; + })} + {label && ( + <> + + {label} + + + {total.toLocaleString()} + + + )} + +
+ {data.map((item, i) => ( +
+ + {item.name} + {((item.value / total) * 100).toFixed(1)}% +
+ ))} +
+
+ ); +} + +/* ====== PieChart ====== */ +interface PieItem { + name: string; + value: number; + color: string; +} + +export function PieChart({ data, size = 200, label }: { + data: PieItem[]; + size?: number; + label?: string; +}) { + const cx = size / 2; + const cy = size / 2; + const r = size / 2 - 4; + const total = data.reduce((s, d) => s + d.value, 0); + if (total === 0) return null; + + let cumAngle = -Math.PI / 2; + + function polarToCartesian(cx2: number, cy2: number, r2: number, angle: number) { + return { x: cx2 + r2 * Math.cos(angle), y: cy2 + r2 * Math.sin(angle) }; + } + + function describeArc(startAngle: number, endAngle: number) { + const start = polarToCartesian(cx, cy, r, endAngle); + const end = polarToCartesian(cx, cy, r, startAngle); + const large = endAngle - startAngle > Math.PI ? 1 : 0; + return `M ${cx} ${cy} L ${start.x} ${start.y} A ${r} ${r} 0 ${large} 0 ${end.x} ${end.y} Z`; + } + + return ( +
+ + {data.map((item, i) => { + const pct = item.value / total; + const angle = pct * Math.PI * 2; + const startAngle = cumAngle; + const endAngle = cumAngle + angle; + cumAngle = endAngle; + return ( + + ); + })} + {label && ( + + {label} + + )} + +
+ {data.map((item, i) => ( +
+ + {item.name} + {item.value} +
+ ))} +
+
+ ); +} + +/* ====== LineChart ====== */ +interface LineSeries { + key: string; + data: number[]; + color: string; + label: string; + yAxis?: 'left' | 'right'; +} + +export function LineChart({ labels, series, width = 700, height = 260, padLeft = 50, padRight = 50, padTop = 20, padBottom = 32 }: { + labels: string[]; + series: LineSeries[]; + width?: number; + height?: number; + padLeft?: number; + padRight?: number; + padTop?: number; + padBottom?: number; +}) { + const plotW = width - padLeft - padRight; + const plotH = height - padTop - padBottom; + + const leftSeries = series.filter((s) => s.yAxis !== 'right'); + const rightSeries = series.filter((s) => s.yAxis === 'right'); + + const allLeft = leftSeries.flatMap((s) => s.data); + const allRight = rightSeries.flatMap((s) => s.data); + + const leftMin = Math.min(0, ...allLeft); + const leftMax = Math.max(...allLeft, 1); + const rightMin = rightSeries.length > 0 ? Math.min(0, ...allRight) : 0; + const rightMax = rightSeries.length > 0 ? Math.max(...allRight, 1) : 1; + + function xPos(i: number) { + return padLeft + (i / Math.max(labels.length - 1, 1)) * plotW; + } + + function yPosLeft(v: number) { + const range = leftMax - leftMin || 1; + return padTop + plotH - ((v - leftMin) / range) * plotH; + } + + function yPosRight(v: number) { + const range = rightMax - rightMin || 1; + return padTop + plotH - ((v - rightMin) / range) * plotH; + } + + const gridLines = 4; + const gridYs = Array.from({ length: gridLines + 1 }, (_, i) => padTop + (plotH * i) / gridLines); + + return ( +
+ + {/* Grid */} + {gridYs.map((gy, i) => ( + + ))} + + {/* Left-axis series */} + {leftSeries.map((s) => { + const points = s.data.map((v, i) => `${xPos(i)},${yPosLeft(v)}`).join(' '); + return ( + + + {s.data.map((v, i) => ( + + ))} + + ); + })} + + {/* Right-axis series */} + {rightSeries.map((s) => { + const points = s.data.map((v, i) => `${xPos(i)},${yPosRight(v)}`).join(' '); + return ( + + + {s.data.map((v, i) => ( + + ))} + + ); + })} + + {/* Left Y-axis labels */} + {gridYs.map((gy, i) => { + const val = leftMax - ((leftMax - leftMin) * i) / gridLines; + return ( + + {val >= 1000 ? (val / 1000).toFixed(0) + 'k' : Math.round(val)} + + ); + })} + + {/* Right Y-axis labels */} + {rightSeries.length > 0 && gridYs.map((gy, i) => { + const val = rightMax - ((rightMax - rightMin) * i) / gridLines; + return ( + + {Math.round(val)}% + + ); + })} + + {/* X-axis labels (show every Nth) */} + {labels.map((l, i) => { + const step = Math.max(1, Math.floor(labels.length / 12)); + if (i % step !== 0) return null; + return ( + + {l} + + ); + })} + + + {/* Legend */} +
+ {series.map((s) => ( +
+ + {s.label} +
+ ))} +
+
+ ); +} + +/* ====== HorizontalBar ====== */ +interface BarItem { + name: string; + dept: string; + value: number; + risk: 'high' | 'medium' | 'low'; + color?: string; +} + +export function HorizontalBar({ data, maxValue }: { data: BarItem[]; maxValue?: number }) { + const max = maxValue ?? Math.max(...data.map((d) => d.value), 1); + const riskColor = { high: COLORS.red, medium: COLORS.orange, low: COLORS.yellow }; + + return ( +
+ {data.map((item, i) => ( +
+ {i + 1} + {item.name} + {item.dept} +
+
+
+ {item.value} + + {item.risk === 'high' ? '高' : item.risk === 'medium' ? '中' : '低'} + +
+ ))} +
+ ); +} + +/* ====== HeatmapGrid ====== */ +interface HeatmapCell { + dept: string; + high: number; + medium: number; + low: number; +} + +export function HeatmapGrid({ data }: { data: HeatmapCell[] }) { + const maxVal = Math.max(...data.flatMap((d) => [d.high, d.medium, d.low]), 1); + + function intensity(v: number, base: string) { + const alpha = 0.15 + (v / maxVal) * 0.7; + return base + Math.round(alpha * 255).toString(16).padStart(2, '0'); + } + + const cols = ['高风险', '中风险', '低风险']; + const baseColors = [COLORS.red, COLORS.orange, COLORS.green]; + + return ( +
+ + + + + {cols.map((c) => ( + + ))} + + + + {data.map((row, ri) => ( + + + {[row.high, row.medium, row.low].map((v, ci) => ( + + ))} + + ))} + +
部门{c}
{row.dept} 0 ? baseColors[ci] : 'rgba(255,255,255,0.2)', + borderLeft: v > 0 ? `2px solid ${baseColors[ci]}` : '2px solid transparent', + }} + > + {v} +
+
+ ); +} + +/* ====== Gauge ====== */ +export function Gauge({ value, max = 100, label, zones }: { + value: number; + max?: number; + label?: string; + zones?: { from: number; to: number; color: string }[]; +}) { + const size = 180; + const cx = size / 2; + const cy = size * 0.65; + const r = size * 0.42; + const strokeW = 18; + const angleRange = 240; // degrees + const startAngle = -120; // degrees from top + + const zones2 = zones ?? [ + { from: 0, to: 30, color: COLORS.green }, + { from: 30, to: 60, color: COLORS.yellow }, + { from: 60, to: max, color: COLORS.red }, + ]; + + function angleToRad(deg: number) { + return (deg * Math.PI) / 180; + } + + function polarToCartesian(angle: number, radius: number) { + const rad = angleToRad(angle); + return { x: cx + radius * Math.cos(rad), y: cy + radius * Math.sin(rad) }; + } + + function makeArc(from: number, to: number) { + const s = polarToCartesian(startAngle + (from / max) * angleRange, r); + const e = polarToCartesian(startAngle + (to / max) * angleRange, r); + const large = (to - from) / max * angleRange > 180 ? 1 : 0; + return `M ${s.x} ${s.y} A ${r} ${r} 0 ${large} 1 ${e.x} ${e.y}`; + } + + const pct = Math.min(value / max, 1); + const needleAngle = startAngle + pct * angleRange; + const needleLen = r - strokeW / 2 - 6; + const needle = polarToCartesian(needleAngle, needleLen); + const needleBack = polarToCartesian(needleAngle, -10); + + let zoneColor = COLORS.green; + if (zones2) { + for (const z of zones2) { + if (value >= z.from && value <= z.to) { zoneColor = z.color; break; } + } + } + + return ( +
+ + {/* Zone arcs */} + {zones2.map((z, i) => ( + + ))} + + {/* Value arc */} + + + {/* Needle */} + + + + + {/* Value text */} + + {value}% + + {label && ( + + {label} + + )} + +
+ ); +} + +/* ====== Timeline ====== */ +interface TimelineItem { + time: string; + user: string; + dept: string; + action: string; + risk: 'high' | 'medium' | 'low'; + status: string; +} + +export function Timeline({ items }: { items: TimelineItem[] }) { + const riskColor = { high: COLORS.red, medium: COLORS.orange, low: COLORS.yellow }; + const riskLabel = { high: '高', medium: '中', low: '低' }; + + return ( +
+ {items.map((item, i) => ( +
+ {item.time} + +
+
+ {item.user} + {item.dept} + + {riskLabel[item.risk]} + +
+

{item.action}

+
+ {item.status} +
+ ))} +
+ ); +} + +/* ====== KpiCard ====== */ +export function KpiCard({ label, value, unit, color, sub }: { + label: string; + value: string | number; + unit?: string; + color: string; + sub?: string; +}) { + return ( +
+
{label}
+
+ + {typeof value === 'number' ? value.toLocaleString() : value} + + {unit && {unit}} +
+ {sub &&
{sub}
} +
+ ); +} diff --git a/.flocks/plugins/contracts/webui/dlp-audit-dashboard/src/index.tsx b/.flocks/plugins/contracts/webui/dlp-audit-dashboard/src/index.tsx new file mode 100644 index 00000000..dd7caa63 --- /dev/null +++ b/.flocks/plugins/contracts/webui/dlp-audit-dashboard/src/index.tsx @@ -0,0 +1,2 @@ +import Page from './Page'; +export default Page; diff --git a/.flocks/plugins/contracts/webui/threat-detection-response/manifest.json b/.flocks/plugins/contracts/webui/threat-detection-response/manifest.json new file mode 100644 index 00000000..1c2b1eb4 --- /dev/null +++ b/.flocks/plugins/contracts/webui/threat-detection-response/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "threat-detection-response", + "title": "威胁检测响应态势", + "route": "/contracts/webui/threat-detection-response", + "icon": "ShieldAlert", + "order": 100, + "enabled": true, + "placement": "home.after", + "entry": "src/index.tsx", + "updatedAt": 0 +} diff --git a/.flocks/plugins/contracts/webui/threat-detection-response/src/Page.tsx b/.flocks/plugins/contracts/webui/threat-detection-response/src/Page.tsx new file mode 100644 index 00000000..1bf1cc43 --- /dev/null +++ b/.flocks/plugins/contracts/webui/threat-detection-response/src/Page.tsx @@ -0,0 +1,519 @@ +import { useEffect, useState, useRef, useCallback, memo } from 'react'; +import { Card } from '@flocks/webui-contract-sdk'; + +const COLORS = { + bg: '#0a0e27', + cardBg: 'rgba(16, 24, 64, 0.75)', + cardBorder: 'rgba(24, 144, 255, 0.15)', + red: '#ff4d4f', + orange: '#fa8c16', + yellow: '#fadb14', + blue: '#1890ff', + green: '#52c41a', + cyan: '#13c2c2', + white: '#e8e8e8', + gray: '#8c8c8c', + gridLine: 'rgba(255,255,255,0.04)', +}; + +const CATEGORY_LABELS = ['真实威胁-已成功', '真实威胁-已阻止', '攻击尝试-待观察', '未知', '安全/误报']; +const CATEGORY_COLORS = [COLORS.red, COLORS.orange, COLORS.yellow, COLORS.blue, COLORS.green]; +const DEVICES = ['SIP', 'EDR', 'NDR']; + +const ATTACK_TACTICS = [ + { id: 'TA0001', name: '初始访问' }, + { id: 'TA0002', name: '执行' }, + { id: 'TA0003', name: '持久化' }, + { id: 'TA0004', name: '提权' }, + { id: 'TA0005', name: '防御规避' }, + { id: 'TA0006', name: '凭证访问' }, + { id: 'TA0007', name: '发现' }, + { id: 'TA0008', name: '横向移动' }, + { id: 'TA0009', name: '收集' }, + { id: 'TA0010', name: '数据渗出' }, + { id: 'TA0011', name: 'C2' }, + { id: 'TA0040', name: '影响' }, +]; + +const TECHNIQUES = ['T1059', 'T1566', 'T1053', 'T1003', 'T1021', 'T1041', 'T1071', 'T1486']; + +function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } +function pick(arr) { return arr[rand(0, arr.length - 1)]; } + +function generateDeviceData() { + return DEVICES.map((name) => { + const today = rand(10, 200); + const counts = [rand(0, 5), rand(2, 15), rand(5, 30), rand(1, 8), rand(20, 80)]; + const total = counts.reduce((a, b) => a + b, 0); + const normalized = counts.map((c) => Math.round((c / total) * today)); + return { name, today, categories: normalized }; + }); +} + +function generateAttckHeatmap() { + return ATTACK_TACTICS.map((tactic) => ({ + tactic, + values: TECHNIQUES.map(() => rand(0, 15)), + })); +} + +function generateAssetRisk() { + const assets = [ + 'srv-web-01', 'db-mysql-03', 'jumpserver', 'pc-zhang-san', 'srv-app-02', + 'vpn-gateway', 'file-server', 'dc-ad-01', 'pc-li-si', 'srv-redis-01' + ]; + const levels = [COLORS.red, COLORS.orange, COLORS.yellow, COLORS.green]; + return assets.map((name, i) => ({ + name, + count: rand(5, 120), + level: levels[Math.min(i < 2 ? 0 : i < 4 ? 1 : i < 6 ? 2 : 3, 3)], + levelLabel: ['高危', '中危', '低危', '正常'][Math.min(i < 2 ? 0 : i < 4 ? 1 : i < 6 ? 2 : 3, 3)], + })).sort((a, b) => b.count - a.count); +} + +function generateAlerts() { + const alertTypes = [ + { name: 'Webshell上传检测', device: 'SIP', category: 0, status: '已隔离' }, + { name: '可疑进程启动', device: 'EDR', category: 2, status: '监控中' }, + { name: '暴力破解扫描', device: 'NDR', category: 4, status: '已忽略' }, + { name: 'C2通信检测', device: 'SIP', category: 1, status: '已阻断' }, + { name: '横向移动检测', device: 'EDR', category: 0, status: '已隔离' }, + { name: 'DNS隧道告警', device: 'NDR', category: 2, status: '监控中' }, + { name: 'Mimikatz执行', device: 'EDR', category: 0, status: '已处置' }, + { name: '端口扫描', device: 'NDR', category: 4, status: '已忽略' }, + { name: 'SQL注入检测', device: 'SIP', category: 1, status: '已阻断' }, + { name: '异常登录', device: 'SIP', category: 3, status: '人工研判' }, + { name: '恶意文件下载', device: 'EDR', category: 1, status: '已阻断' }, + { name: '目录遍历', device: 'NDR', category: 4, status: '已忽略' }, + { name: '计划任务创建', device: 'EDR', category: 2, status: '监控中' }, + { name: 'XSS攻击检测', device: 'SIP', category: 1, status: '已阻断' }, + { name: '数据渗出告警', device: 'NDR', category: 0, status: '已隔离' }, + ]; + const assets = ['srv-web-01', 'pc-zhang-san', '10.2.3.5', 'srv-app-02', 'db-mysql-03', 'pc-li-si', 'vpn-gateway', '10.1.8.22', 'srv-web-01', 'jumpserver', 'file-server', '10.3.1.7', 'dc-ad-01', 'srv-app-02', 'srv-web-01']; + return alertTypes.map((a, i) => ({ + ...a, + asset: assets[i % assets.length], + time: `${String(14 - Math.floor(i / 3)).padStart(2, '0')}:${String(rand(0, 59)).padStart(2, '0')}:${String(rand(0, 59)).padStart(2, '0')}`, + id: `alert-${Date.now()}-${i}`, + })); +} + +function generateIocBubbles() { + const sources = ['微步在线', 'VirusTotal', 'X-Force', 'AlienVault', '内部情报']; + const levels = ['核心', '重要', '一般']; + const bubbles = []; + for (let i = 0; i < 18; i++) { + bubbles.push({ + x: rand(10, 90), + y: rand(8, 88), + r: rand(6, 32), + source: pick(sources), + assetLevel: pick(levels), + hits: rand(3, 200), + }); + } + return bubbles; +} + +function KpiCard({ label, value, sub, color, width }: { label: string; value: number; sub: string; color: string; width: string }) { + return ( +
+
{label}
+
{value}
+
{sub}
+
+ ); +} + +function StackedBar({ data, width, height }: { data: { name: string; today: number; categories: number[] }; width: number; height: number }) { + const total = data.categories.reduce((a, b) => a + b, 0) || 1; + const barH = 18; + const gap = 22; + const y = 26; + let x = 0; + return ( +
+
{data.name}{data.today}
+ + {data.categories.map((v, i) => { + const w = (v / total) * width; + const rect = ; + x += w; + return rect; + })} + {CATEGORY_LABELS.map((label, i) => { + const segX = data.categories.slice(0, i).reduce((s, v) => s + (v / total) * width, 0); + const segW = (data.categories[i] / total) * width; + if (segW < 30) return null; + return ( + + {data.categories[i]} + + ); + })} + +
+ {CATEGORY_LABELS.map((l, i) => ( + ● {l} {data.categories[i]} + ))} +
+
+ ); +} + +function AttckHeatmap({ data }: { data: { tactic: { id: string; name: string }; values: number[] }[] }) { + const maxVal = Math.max(...data.flatMap((d) => d.values), 1); + const cellW = 48, cellH = 24, leftW = 72, topH = 22; + const w = leftW + TECHNIQUES.length * cellW; + const h = topH + data.length * cellH; + return ( +
+
MITRE ATT&CK 战术热力图
+ + {TECHNIQUES.map((t, i) => ( + {t} + ))} + {data.map((row, ri) => ( + {row.tactic.name} + ))} + {data.map((row, ri) => + row.values.map((v, ci) => { + const alpha = v / maxVal; + const r = Math.round(10 + alpha * 120); + const g = Math.round(22 + alpha * 50); + const b = Math.round(40 + alpha * 20); + return ; + }) + )} + +
+ ); +} + +function AssetRiskList({ data, width }: { data: { name: string; count: number; level: string; levelLabel: string }[]; width: number }) { + const maxCount = Math.max(...data.map((d) => d.count), 1); + return ( +
+
资产风险榜 Top10
+ {data.map((item, i) => ( +
+ {i + 1} + {item.name} +
+
+
+ {item.count} +
+ ))} +
+ ); +} + +const AlertStream = memo(({ data }: { data: ReturnType }) => { + const containerRef = useRef(null); + const [offset, setOffset] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setOffset((prev) => { + const container = containerRef.current; + if (!container) return prev; + const maxScroll = container.scrollHeight - container.clientHeight; + const next = prev + 1; + return next > maxScroll ? 0 : next; + }); + }, 80); + return () => clearInterval(interval); + }, [data]); + + useEffect(() => { + if (containerRef.current) { + containerRef.current.scrollTop = offset; + } + }, [offset]); + + return ( +
+
实时告警流 Top 50
+
+ {[...data, ...data].map((alert, i) => ( +
+ {alert.time} + {alert.device} + {alert.name} + {alert.asset} + + {CATEGORY_LABELS[alert.category]} + + {alert.status} +
+ ))} +
+
+ ); +}); + +function RingChart({ autoIgnore, autoAction, manual, size }: { autoIgnore: number; autoAction: number; manual: number; size: number }) { + const total = autoIgnore + autoAction + manual || 1; + const cx = size / 2, cy = size / 2, r = size / 2 - 12, strokeW = 16; + const circumference = 2 * Math.PI * r; + const segments = [ + { val: autoIgnore, color: COLORS.green }, + { val: autoAction, color: COLORS.orange }, + { val: manual, color: COLORS.blue }, + ]; + let offset = -Math.PI / 2; + return ( +
+
自动化覆盖率
+ + {segments.map((seg, i) => { + const pct = seg.val / total; + const dashLen = circumference * pct; + const dashGap = circumference - dashLen; + const rot = (offset / (2 * Math.PI)) * 360; + offset += 2 * Math.PI * pct; + return ; + })} + {Math.round((autoIgnore + autoAction) / total * 100)}% + 自动化率 + +
+ {[ + { label: '自动忽略', val: autoIgnore, color: COLORS.green }, + { label: '自动联动', val: autoAction, color: COLORS.orange }, + { label: '人工处置', val: manual, color: COLORS.blue }, + ].map((item) => ( +
+
{item.val}
+
{item.label}
+
+ ))} +
+
+ ); +} + +function IocBubbleChart({ data, width, height }: { data: ReturnType; width: number; height: number }) { + const sourceColors: Record = { + '微步在线': COLORS.cyan, + 'VirusTotal': COLORS.orange, + 'X-Force': COLORS.blue, + 'AlienVault': COLORS.green, + '内部情报': COLORS.red, + }; + const opacityByLevel: Record = { '核心': 0.9, '重要': 0.65, '一般': 0.4 }; + return ( +
+
IOC 命中分布
+ + {data.map((b, i) => ( + + {b.source} | {b.assetLevel}资产 | 命中{b.hits}次 + + ))} + +
+ {Object.entries(sourceColors).map(([name, color]) => ( + ● {name} + ))} +
+
+ ); +} + +function GaugeChart({ value, max, label, color }: { value: number; max: number; color: string }) { + const pct = Math.min(value / max, 1); + const angle = pct * 180; + const r = 48, cx = 50, cy = 52; + const rad = (angle - 180) * (Math.PI / 180); + const needleX = cx + r * Math.cos(rad); + const needleY = cy + r * Math.sin(rad); + return ( +
+ + + + + + +
{value}min
+
{label}
+
+ ); +} + +export default function Page() { + const [ready, setReady] = useState(false); + const [deviceData, setDeviceData] = useState(generateDeviceData); + const [attckData, setAttckData] = useState(generateAttckHeatmap); + const [assetRisk, setAssetRisk] = useState(generateAssetRisk); + const [alerts, setAlerts] = useState(generateAlerts); + const [iocBubbles, setIocBubbles] = useState(generateIocBubbles); + const [now, setNow] = useState(new Date()); + const [blinking, setBlinking] = useState(true); + + const refresh = useCallback(() => { + setDeviceData(generateDeviceData()); + setAttckData(generateAttckHeatmap()); + setAssetRisk(generateAssetRisk()); + setAlerts(generateAlerts()); + setIocBubbles(generateIocBubbles()); + setNow(new Date()); + }, []); + + useEffect(() => { + setReady(true); + const timer = setInterval(refresh, 30000); + const blink = setInterval(() => setBlinking((b) => !b), 1000); + return () => { clearInterval(timer); clearInterval(blink); }; + }, [refresh]); + + const totalEvents = deviceData.reduce((s, d) => s + d.today, 0); + const totalCategories = deviceData.reduce((acc, d) => { + d.categories.forEach((v, i) => { acc[i] = (acc[i] || 0) + v; }); + return acc; + }, [0, 0, 0, 0, 0]); + const autoIgnore = Math.round(totalEvents * 0.58); + const autoAction = Math.round(totalEvents * 0.14); + const manual = totalEvents - autoIgnore - autoAction; + + const timeStr = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`; + + const totalThreats = totalCategories[0] + totalCategories[1]; + + if (!ready) return
加载中...
; + + return ( +
+
+
+ 🛡 + 威胁检测响应态势 +
+
+
+ + 运行中 +
+ {timeStr} + 每30s刷新 +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+
+
今日总事件
+
{totalEvents}
+
+ 真实威胁 {totalThreats} + | + 待处置 {manual} +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
攻击源地理分布
+
+ + + 中国地图 + 省份热力 + 海外飞线 + +
+
Top 攻击源: 美国 43% | 俄罗斯 18% | 越南 12% | 印度 9%
+
+ +
+
+ ); +} + +const styles: Record = { + wrapper: { + minHeight: '100vh', + background: COLORS.bg, + padding: 16, + fontFamily: '-apple-system, BlinkMacSystemFont, "Microsoft YaHei", "Segoe UI", sans-serif', + color: COLORS.white, + }, + header: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '12px 20px', + marginBottom: 14, + background: COLORS.cardBg, + borderRadius: 8, + border: `1px solid ${COLORS.cardBorder}`, + backdropFilter: 'blur(8px)', + }, + blinkDot: { + width: 8, + height: 8, + borderRadius: '50%', + background: COLORS.green, + boxShadow: `0 0 6px ${COLORS.green}`, + transition: 'opacity 0.4s', + }, + grid: { + display: 'grid', + gridTemplateColumns: 'repeat(5, 1fr)', + gap: 12, + }, + module: { + background: COLORS.cardBg, + borderRadius: 8, + border: `1px solid ${COLORS.cardBorder}`, + padding: 14, + backdropFilter: 'blur(8px)', + minHeight: 120, + }, + kpiCard: { + background: COLORS.cardBg, + borderRadius: 8, + border: `1px solid ${COLORS.cardBorder}`, + padding: '12px 16px', + backdropFilter: 'blur(8px)', + }, +}; diff --git a/.flocks/plugins/contracts/webui/threat-detection-response/src/index.tsx b/.flocks/plugins/contracts/webui/threat-detection-response/src/index.tsx new file mode 100644 index 00000000..dd7caa63 --- /dev/null +++ b/.flocks/plugins/contracts/webui/threat-detection-response/src/index.tsx @@ -0,0 +1,2 @@ +import Page from './Page'; +export default Page; diff --git a/.flocks/plugins/contracts/webui/threat-intel-dashboard/manifest.json b/.flocks/plugins/contracts/webui/threat-intel-dashboard/manifest.json new file mode 100644 index 00000000..fe850c5d --- /dev/null +++ b/.flocks/plugins/contracts/webui/threat-intel-dashboard/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "threat-intel-dashboard", + "title": "情报封禁大屏", + "route": "/contracts/webui/threat-intel-dashboard", + "icon": "ShieldAlert", + "order": 100, + "enabled": true, + "placement": "home.after", + "entry": "src/index.tsx", + "updatedAt": 0 +} diff --git a/.flocks/plugins/contracts/webui/threat-intel-dashboard/src/Page.tsx b/.flocks/plugins/contracts/webui/threat-intel-dashboard/src/Page.tsx new file mode 100644 index 00000000..5051c970 --- /dev/null +++ b/.flocks/plugins/contracts/webui/threat-intel-dashboard/src/Page.tsx @@ -0,0 +1,468 @@ +import { useEffect, useState } from 'react'; +import { Card } from '@flocks/webui-contract-sdk'; + +const MOCK = { + kpi: { + totalPulled: 12847, + totalPulledDelta: 12.3, + internalHits: 1246, + internalHitsDelta: 8.7, + blocked: 892, + blockedDelta: 15.2, + pending: 354, + pendingDelta: -3.1, + autoBlockRate: 71.5, + }, + iocTypes: [ + { type: 'Domain', count: 4201, color: '#40c4ff' }, + { type: 'IP', count: 3102, color: '#00e676' }, + { type: 'URL', count: 2844, color: '#ffd740' }, + { type: 'Hash', count: 2700, color: '#ff9100' }, + ], + sourceRanking: [ + { name: '银狐 IOC', count: 3847, highConf: 2103 }, + { name: 'HRTI 态势情报', count: 3102, highConf: 1847 }, + { name: 'ThreatBook', count: 2547, highConf: 1203 }, + { name: 'VirusTotal', count: 1892, highConf: 674 }, + { name: '勒索软件监控', count: 1459, highConf: 523 }, + ], + confidence: { + high: 2847, + medium: 5103, + low: 4897, + }, + blockStream: [ + { time: '14:30:12', asset: '192.168.1.5', action: '防火墙封禁', type: 'block', target: '45.33.32.156' }, + { time: '14:30:08', asset: 'DNS 防火墙', action: '域名阻断', type: 'block', target: 'evil-c2.xyz' }, + { time: '14:30:03', asset: 'web-prod-01', action: '命中内部', type: 'hit', target: '103.224.182.253' }, + { time: '14:29:55', asset: 'EDR 终端', action: '进程Hash拦截', type: 'block', target: 'a3f2b9c1d4e5' }, + { time: '14:29:41', asset: 'db-master', action: '命中内部', type: 'hit', target: 'malware-cdn.net' }, + { time: '14:29:33', asset: '零信任网关', action: '身份封禁', type: 'block', target: 'user_suspect_042' }, + { time: '14:29:18', asset: 'app-node-03', action: '命中内部', type: 'hit', target: '198.51.100.42' }, + { time: '14:29:02', asset: '防火墙', action: 'URL封禁', type: 'block', target: 'phish.badactor.io' }, + { time: '14:28:47', asset: 'mail-gw-01', action: '命中内部', type: 'hit', target: 'spam-relay.cc' }, + { time: '14:28:31', asset: 'DNS 防火墙', action: '域名阻断', type: 'block', target: 'ransom-c2.onion' }, + { time: '14:28:15', asset: 'vpn-node-02', action: '命中内部', type: 'hit', target: '185.220.101.34' }, + { time: '14:28:00', asset: 'EDR 终端', action: '进程Hash拦截', type: 'block', target: 'f7e8d9c0b1a2' }, + ], + affectedAssets: [ + { name: 'web-prod-01', hits: 142, level: 'high' }, + { name: 'db-master', hits: 98, level: 'high' }, + { name: 'app-node-03', hits: 67, level: 'medium' }, + { name: 'mail-gw-01', hits: 54, level: 'medium' }, + { name: 'vpn-node-02', hits: 43, level: 'medium' }, + { name: 'file-srv-01', hits: 38, level: 'low' }, + { name: 'ad-dc-01', hits: 31, level: 'low' }, + { name: 'monitor-01', hits: 25, level: 'low' }, + { name: 'jump-srv-01', hits: 19, level: 'low' }, + { name: 'log-srv-02', hits: 15, level: 'low' }, + ], + decay: [100, 82, 61, 45, 32, 22, 15, 10, 7, 5, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + strategy: { + firewall: 412, + zeroTrust: 198, + dnsFirewall: 337, + edr: 245, + multiLayerRate: 94.3, + }, +}; + +const BG = '#0a0f1e'; +const CARD_BG = '#0d1326'; +const BORDER = '#1a2340'; +const TEXT_PRIMARY = '#e0e6f0'; +const TEXT_SECONDARY = '#8892a8'; +const GREEN = '#00e676'; +const YELLOW = '#ffd740'; +const ORANGE = '#ff9100'; +const RED = '#ff1744'; +const BLUE = '#40c4ff'; + +function DeltaBadge({ delta }: { delta: number }) { + const isUp = delta > 0; + return ( + 10 ? RED : ORANGE) : GREEN, fontSize: 13, marginLeft: 6 }}> + {isUp ? '↑' : '↓'}{Math.abs(delta)}% + + ); +} + +function levelColor(level: string) { + switch (level) { + case 'high': return RED; + case 'medium': return ORANGE; + case 'low': return YELLOW; + default: return TEXT_SECONDARY; + } +} + +function levelLabel(level: string) { + switch (level) { + case 'high': return '高'; + case 'medium': return '中'; + case 'low': return '低'; + default: return level; + } +} + +export default function Page() { + const [time, setTime] = useState(''); + const [streamIdx, setStreamIdx] = useState(0); + + useEffect(() => { + const tick = () => { + const now = new Date(); + setTime( + now.getFullYear() + '-' + + String(now.getMonth() + 1).padStart(2, '0') + '-' + + String(now.getDate()).padStart(2, '0') + ' ' + + String(now.getHours()).padStart(2, '0') + ':' + + String(now.getMinutes()).padStart(2, '0') + ':' + + String(now.getSeconds()).padStart(2, '0') + ); + }; + tick(); + const t = setInterval(tick, 1000); + return () => clearInterval(t); + }, []); + + useEffect(() => { + const interval = setInterval(() => { + setStreamIdx(prev => (prev + 1) % MOCK.blockStream.length); + }, 2000); + return () => clearInterval(interval); + }, []); + + const maxIoc = Math.max(...MOCK.iocTypes.map(i => i.count)); + const maxSource = Math.max(...MOCK.sourceRanking.map(s => s.count)); + const totalConf = MOCK.confidence.high + MOCK.confidence.medium + MOCK.confidence.low; + const maxAssetHits = MOCK.affectedAssets[0]?.hits ?? 1; + const maxStrat = Math.max(MOCK.strategy.firewall, MOCK.strategy.zeroTrust, MOCK.strategy.dnsFirewall, MOCK.strategy.edr); + + const cardStyle: React.CSSProperties = { + background: CARD_BG, + border: `1px solid ${BORDER}`, + borderRadius: 8, + padding: '16px 20px', + }; + + const sectionTitle: React.CSSProperties = { + color: TEXT_SECONDARY, + fontSize: 12, + textTransform: 'uppercase' as const, + letterSpacing: 1.5, + marginBottom: 12, + fontWeight: 600, + }; + + return ( +
+ + {/* ====== HEADER ====== */} +
+
+
+ 🛡 +
+ 威胁情报运营态势 +
+
+ {time} + + + 实时监控中 + +
+
+ + {/* ====== KPI ROW ====== */} +
+ {[ + { label: '今日拉取 IOC 总数', value: MOCK.kpi.totalPulled.toLocaleString(), delta: MOCK.kpi.totalPulledDelta, color: BLUE }, + { label: '命中内部资产', value: MOCK.kpi.internalHits.toLocaleString(), delta: MOCK.kpi.internalHitsDelta, color: ORANGE }, + { label: '已执行封禁', value: MOCK.kpi.blocked.toLocaleString(), delta: MOCK.kpi.blockedDelta, color: GREEN }, + { label: '待处置', value: MOCK.kpi.pending.toLocaleString(), delta: MOCK.kpi.pendingDelta, color: YELLOW }, + { label: '自动化封禁率', value: MOCK.kpi.autoBlockRate + '%', delta: 0, color: GREEN }, + ].map((kpi, i) => ( +
+
{kpi.label}
+
+ + {kpi.value} + + {kpi.delta !== 0 && } +
+
+
+ ))} +
+ + {/* ====== MAIN CONTENT GRID ====== */} +
+ + {/* IOC 类型分布 */} +
+
IOC 类型分布 (24h)
+ {MOCK.iocTypes.map((item, i) => ( +
+
+ {item.type} + {item.count.toLocaleString()} +
+
+
+
+
+ ))} +
+ + 合计 {MOCK.iocTypes.reduce((s, i) => s + i.count, 0).toLocaleString()} 条 IOC + +
+
+ + {/* 情报贡献排行 */} +
+
情报源贡献排行
+ {MOCK.sourceRanking.map((item, i) => ( +
+
+ {item.name} + + 高置信 {item.highConf.toLocaleString()} + +
+
+
+
+
+ ))} +
+ + {/* 置信度分布 */} +
+
情报置信度分布
+
+ {[ + { label: '高置信', value: MOCK.confidence.high, color: RED }, + { label: '中置信', value: MOCK.confidence.medium, color: ORANGE }, + { label: '低置信', value: MOCK.confidence.low, color: YELLOW }, + ].map((item, i) => ( +
+ + {item.value.toLocaleString()} + +
+ + {item.label} ({((item.value / totalConf) * 100).toFixed(1)}%) + +
+ ))} +
+
+
+ + {/* ====== SECOND ROW ====== */} +
+ + {/* 实时封禁流水 */} +
+
+ 实时封禁流水 + LIVE +
+
+ {MOCK.blockStream.map((item, i) => { + const isVisible = i >= streamIdx && i < streamIdx + 8; + const visibleIdx = i >= streamIdx ? i - streamIdx : -1; + return ( +
+ + {item.time} + + + {item.action} + + + {item.target} + + + {item.type === 'block' ? '已封禁' : '待处置'} + +
+ ); + })} +
+
+
+ + {/* 受影响资产 Top10 */} +
+
受影响资产 Top10
+ {MOCK.affectedAssets.map((item, i) => ( +
+
+
+ #{i + 1} + {item.name} +
+
+ + {item.hits} + + + {levelLabel(item.level)}危 + +
+
+
+
+
+
+ ))} +
+
+ + {/* ====== THIRD ROW: Decay Curve ====== */} +
+
+
封禁效果衰减曲线(封禁后 24h 命中次数)
+ + {/* Grid lines */} + {[0, 25, 50, 75, 100].map(y => ( + + ))} + {/* Y axis labels */} + {[100, 75, 50, 25, 0].map((v, i) => ( + + {v}% + + ))} + {/* Decay line */} + `${40 + (i / (MOCK.decay.length - 1)) * 740},${20 + 100 - v}`).join(' ')} + fill="none" + stroke={BLUE} + strokeWidth={2} + strokeLinecap="round" + strokeLinejoin="round" + /> + {/* Area fill */} + `${40 + (i / (MOCK.decay.length - 1)) * 740},${20 + 100 - v}`).join(' ')} 780,120`} + fill="url(#decayGrad)" + opacity={0.3} + /> + + + + + + + {/* X axis labels */} + {['T0', 'T4', 'T8', 'T12', 'T16', 'T20', 'T24'].map((label, i) => ( + + {label}h + + ))} + {/* Data points */} + {[0, 1, 2, 4, 6, 12, 24].map(t => { + const v = MOCK.decay[t] ?? 0; + return ( + + ); + })} + +
+
+ + {/* ====== BOTTOM: Strategy Status ====== */} +
+
+
封禁策略执行状态
+
+ {[ + { label: '防火墙封禁', value: MOCK.strategy.firewall, color: RED }, + { label: '零信任封禁', value: MOCK.strategy.zeroTrust, color: ORANGE }, + { label: 'DNS 防火墙', value: MOCK.strategy.dnsFirewall, color: BLUE }, + { label: 'EDR 拦截', value: MOCK.strategy.edr, color: GREEN }, + ].map((item, i) => ( +
+
+ {item.label} + + {item.value} + +
+
+
+
+
+ ))} +
+
+
+
+ {MOCK.strategy.multiLayerRate}% +
+
多层叠加率
+
+
+
+ <30s +
+
平均封禁延迟
+
+
+
+ 99.7% +
+
封禁成功率
+
+
+
+
+ +
+ ); +} diff --git a/.flocks/plugins/contracts/webui/threat-intel-dashboard/src/index.tsx b/.flocks/plugins/contracts/webui/threat-intel-dashboard/src/index.tsx new file mode 100644 index 00000000..dd7caa63 --- /dev/null +++ b/.flocks/plugins/contracts/webui/threat-intel-dashboard/src/index.tsx @@ -0,0 +1,2 @@ +import Page from './Page'; +export default Page;