31、工业网络异常行为检测与OT协议深度分析 (核电站DCS模拟) - /安全与维护组件/network-anomaly-detection-nuclear
76个工业组件库示例汇总
工业网络异常行为检测与OT协议深度分析 (核电站DCS模拟)
1. 组件概述
network-anomaly-detection-nuclear
组件是一个高级的前端模拟界面,旨在演示工业网络安全监控系统如何检测异常行为,特别是针对核电站集散控制系统 (DCS) 的潜在攻击,并结合操作技术 (OT) 协议的深度包检测 (DPI) 功能。
该组件模拟了一个多区域网络环境(IT, DMZ, PCN, SIS),并动态生成混合了正常和异常(包括模拟的零日攻击)的网络流量。系统会对流量进行分析,识别异常模式和违反安全策略的行为,并进行告警。
2. 核心功能
- 网络拓扑可视化: 使用简单的块状图展示模拟的核电站网络分区(IT, DMZ, PCN, SIS, Field等)及关键资产(防火墙, 服务器, HMI, PLC, 安全PLC等)。
- 点击节点可查看该资产的详细信息(名称, IP, 类型, 区域, 状态, 描述)。
- 节点边框颜色会根据资产状态(正常/警告/严重)变化。
- 实时流量监控: 动态滚动显示模拟产生的网络流量包概要信息。
- 包含源/目的 IP、协议类型、简要信息和告警级别。
- 流量条目根据是否为 OT 协议、是否异常、告警级别进行视觉区分。
- 提供流量过滤器(所有、仅 OT、仅异常、仅严重告警)。
- OT 协议深度分析 (模拟): 点击流量条目后,在下方区域展示对该数据包(特别是 Modbus/TCP 和 DNP3)的模拟深度解析结果。
- 展示关键协议字段(如功能码、地址、值、DNP3对象等)。
- 如果检测到异常,会高亮显示可疑字段,并附带异常描述。
- 异常行为检测 (规则引擎模拟): 内置一套简化的规则引擎,用于检测多种异常情况:
- 跨区域违规通信: 如 IT 区直接访问 PCN/SIS 区,非授权设备访问 SIS 区等。
- 非法 OT 操作: 如向安全 PLC 发送写指令、使用无效功能码等。
- 扫描行为: 简单的 ICMP Ping 扫描和 TCP SYN 端口扫描检测。
- 模拟零日攻击场景: 随机触发特定的攻击模式,如向安全PLC发送无效Modbus功能码、对关键地址写入危险值、来自非预期源的DNP3非请求响应、来自DMZ对OT设备的端口扫描等。这些攻击会被标记为严重告警,并附带特殊说明。
- 分级告警系统: 将检测到的异常根据严重程度分为信息 (Info)、警告 (Warning)、严重 (Critical) 三级,并在告警面板和流量日志中用不同颜色标识。
- 系统状态指示: 根据当前活动告警的最高级别,动态更新顶部的系统状态(正常/警告/严重告警)。
- 模拟控制: 提供开始/停止模拟、调整模拟速度的功能。
- 苹果科技工业风格 & 响应式设计: 界面简洁、专业,并能适应不同屏幕尺寸。
3. 文件结构
安全与维护组件/
└── network-anomaly-detection-nuclear/├── index.html # 组件的 HTML 结构├── styles.css # CSS 样式文件├── script.js # JavaScript 文件,包含模拟核心逻辑└── README.md # 组件说明文档 (本文件)
4. 使用方法
- 在浏览器中打开
安全与维护组件/network-anomaly-detection-nuclear/index.html
文件。 - 点击"开始"按钮启动模拟。
- 观察实时流量日志和告警面板。
- 使用流量过滤器查看特定类型的流量。
- 点击流量条目,在"协议深度分析"区域查看详情。
- 点击网络拓扑图中的节点,在左下面板查看资产详情。
- 使用滑块调整模拟速度。
- 点击"清除"按钮清除告警列表。
5. 模拟说明与限制
- 简化模型: 网络拓扑、资产细节、协议解析和异常检测规则均为高度简化,旨在演示概念,并非真实世界的精确复现。
- 协议解析: OT 协议(Modbus, DNP3)的解析是模拟的,仅展示部分关键字段。
- 异常检测: 检测规则基于简单的逻辑判断,并非复杂的机器学习或行为分析模型。
- 零日攻击: 模拟的零日攻击是预设的几种特定场景,用于演示系统识别未知威胁的能力概念。
- 性能: 大量 DOM 操作可能导致在高模拟速度下界面响应变慢,实际应用需要优化渲染逻辑。
- 数据来源: 所有网络流量和事件均为脚本内部随机生成。
6. 未来扩展方向
- 真实拓扑导入: 支持加载自定义的网络拓扑结构。
- 集成 PCAP 数据: 支持读取真实的 PCAP 文件进行分析。
- 更完善的协议解析: 引入更专业的协议解析库。
- 高级异常检测算法: 集成基于统计或机器学习的异常检测模型。
- 可视化增强: 使用 SVG 或 Canvas 库(如 D3.js, Konva.js)绘制更丰富的网络拓扑图和流量动画。
- 与 SIEM 集成: 提供接口将告警发送到 SIEM 系统。
效果展示
源码
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>工业网络异常检测与OT协议分析 (核电站)</title><link rel="stylesheet" href="styles.css"><!-- Font Awesome for icons --><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body><div class="app-container"><!-- Header --><header class="app-header"><div class="header-title"><i class="fas fa-shield-alt header-icon"></i><h1>工业网络异常检测与OT协议分析 (核电)</h1></div><div class="header-status"><span>系统状态: <strong id="system-status" class="status-ok">正常</strong></span><span>活动告警: <strong id="active-alerts-count">0</strong></span><span>已处理流量: <strong id="processed-packets-count">0</strong> pkts</span></div></header><!-- Main Content Area --><main class="app-content"><!-- Left Panel: Network Topology & Assets --><section class="panel network-panel"><div class="panel-header"><h2><i class="fas fa-project-diagram"></i> 网络拓扑与资产</h2></div><div class="panel-content scrollable" id="network-topology"><!-- Topology will be rendered here by JS --><p class="placeholder">正在加载网络拓扑...</p></div><div class="asset-details" id="asset-details"><h3>资产详情</h3><div id="asset-info-content"><p class="placeholder">点击拓扑图中的节点查看详情</p></div></div></section><!-- Center Panel: Live Traffic & Protocol Analysis --><section class="panel traffic-panel"><div class="panel-header"><h2><i class="fas fa-network-wired"></i> 实时流量 & 协议分析</h2><select id="traffic-filter" class="header-filter"><option value="all">所有流量</option><option value="ot">仅 OT 协议</option><option value="anomalous">仅异常</option><option value="critical">仅严重告警</option></select></div><div class="panel-content scrollable" id="live-traffic-log"><!-- Live traffic entries will be added here by JS --><p class="placeholder">等待网络流量...</p></div><div class="protocol-analysis" id="protocol-analysis"><h3>协议深度分析 (选中流量)</h3><div id="analysis-content" class="scrollable"><p class="placeholder">点击流量条目查看协议详情</p></div></div></section><!-- Right Panel: Anomaly Alerts --><section class="panel alerts-panel"><div class="panel-header"><h2><i class="fas fa-bell"></i> 异常行为告警</h2><button id="clear-alerts-btn" title="清除所有告警"><i class="fas fa-broom"></i> 清除</button></div><div class="panel-content scrollable" id="alerts-log"><!-- Alert entries will be added here by JS --><p class="placeholder">暂无告警</p></div></section></main><!-- Simulation Controls --><footer class="app-footer"><div class="simulation-controls"><button id="start-sim-btn" title="开始"><i class="fas fa-play"></i> 开始</button><button id="stop-sim-btn" title="停止" disabled><i class="fas fa-stop"></i> 停止</button><label for="speed-slider">速度:</label><input type="range" id="speed-slider" min="1" max="10" value="3" title="调整速度"><span id="speed-value">x3</span></div><div class="simulation-info">时间: <span id="simulation-time">00:00:00</span></div></footer></div><script src="script.js"></script>
</body>
</html>
styles.css
:root {--primary-color: #007aff; /* Apple Blue */--secondary-color: #8e8e93; /* Apple Gray */--background-color: #f2f2f7;--panel-background-color: #ffffff;--text-color: #1c1c1e;--text-color-secondary: #636366;--border-color: #d1d1d6;--success-color: #34c759; /* Apple Green */--warning-color: #ff9500; /* Apple Orange */--error-color: #ff3b30; /* Apple Red */--info-color: #5ac8fa; /* Apple Light Blue */--ot-protocol-color: #5856d6; /* Apple Purple */--alert-critical-bg: rgba(255, 59, 48, 0.08);--alert-warning-bg: rgba(255, 149, 0, 0.08);--alert-info-bg: rgba(90, 200, 250, 0.1);--alert-critical-border: var(--error-color);--alert-warning-border: var(--warning-color);--alert-info-border: var(--info-color);--status-ok-color: var(--success-color);--status-warning-color: var(--warning-color);--status-critical-color: var(--error-color);--traffic-selected-bg: rgba(0, 122, 255, 0.1);--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--border-radius: 8px;--panel-padding: 15px;--panel-header-height: 45px;--footer-height: 50px;--animation-duration: 0.3s;
}* {box-sizing: border-box;margin: 0;padding: 0;
}body {font-family: var(--font-family);background-color: var(--background-color);color: var(--text-color);line-height: 1.5;font-size: 13px;overflow: hidden; /* Prevent body scroll */
}.app-container {display: flex;flex-direction: column;height: 100vh; /* Fallback */height: 100dvh;max-height: 600px; /* Limit overall height */overflow: hidden;background-color: var(--panel-background-color);border-radius: var(--border-radius);box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);margin: 10px;
}/* Header */
.app-header {display: flex;justify-content: space-between;align-items: center;padding: 10px var(--panel-padding);border-bottom: 1px solid var(--border-color);background-color: #f8f8f8;border-top-left-radius: var(--border-radius);border-top-right-radius: var(--border-radius);flex-shrink: 0;
}.header-title {display: flex;align-items: center;
}.header-icon {font-size: 1.6em;color: var(--primary-color);margin-right: 10px;
}.app-header h1 {font-size: 1.15em;font-weight: 600;
}.header-status {display: flex;align-items: center;gap: 20px;font-size: 0.9em;
}.header-status strong {font-weight: 600;
}/* Status Colors */
.status-ok { color: var(--status-ok-color); }
.status-warning { color: var(--status-warning-color); }
.status-critical { color: var(--status-critical-color); }/* Main Content Grid */
.app-content {display: grid;grid-template-columns: 1fr 1.8fr 1.2fr; /* Adjust column ratios */gap: var(--panel-padding);padding: var(--panel-padding);flex-grow: 1;overflow: hidden;height: calc(100% - var(--panel-header-height) - var(--footer-height) - 1px); /* Calculate height minus header/footer/border */
}/* Panels */
.panel {background-color: var(--panel-background-color);border: 1px solid var(--border-color);border-radius: var(--border-radius);display: flex;flex-direction: column;overflow: hidden; /* Crucial for containing scrolling content */height: 100%; /* Ensure panels take full grid cell height */
}.panel-header {display: flex;justify-content: space-between;align-items: center;padding: 8px var(--panel-padding);border-bottom: 1px solid var(--border-color);background-color: #f8f8f8;height: var(--panel-header-height);flex-shrink: 0;
}.panel-header h2 {font-size: 0.95em;font-weight: 600;display: flex;align-items: center;gap: 8px;
}.panel-header h2 i {color: var(--text-color-secondary);
}.panel-content {flex-grow: 1;overflow: hidden; /* Base overflow setting *//* padding needs to be applied carefully or removed if scrollable handles padding */
}.scrollable {overflow-y: auto;height: 100%; /* Make sure scrollable area takes available height */padding: var(--panel-padding); /* Apply padding inside scrollable */
}/* --- Specific Panel Styles --- *//* Network Panel */
.network-panel {/* Contains topology and asset details */
}#network-topology {height: 65%; /* Allocate space for topology */border-bottom: 1px solid var(--border-color);position: relative; /* For potential absolute positioning inside */padding: 0; /* Remove padding if SVG/Canvas handles its own spacing */display: flex; /* Center placeholder */align-items: center;justify-content: center;background-color: #fafafa; /* Slight background tint */
}.network-node {/* Example style for nodes rendered by JS (e.g., SVG circle) */cursor: pointer;transition: transform 0.2s ease, fill 0.2s ease;
}
.network-node:hover {transform: scale(1.1);
}
.network-node.active {stroke: var(--primary-color);stroke-width: 3px;
}.asset-details {height: 35%; /* Remaining space */display: flex;flex-direction: column;
}.asset-details h3 {font-size: 0.9em;font-weight: 600;padding: 10px var(--panel-padding) 5px;border-bottom: 1px solid var(--border-color);flex-shrink: 0;background-color: #f8f8f8;
}#asset-info-content {flex-grow: 1;overflow-y: auto;padding: var(--panel-padding);font-size: 0.9em;line-height: 1.6;
}
#asset-info-content p strong {font-weight: 600;color: var(--text-color);margin-right: 5px;display: inline-block;min-width: 60px;
}/* Traffic Panel */
.traffic-panel {/* Contains live log and protocol analysis */
}.traffic-panel .panel-content {padding: 0; /* Remove padding from main content, apply to sub-sections */display: flex;flex-direction: column;height: calc(100% - var(--panel-header-height)); /* Full height minus header */
}#live-traffic-log {height: 60%; /* Allocate space for traffic log */border-bottom: 1px solid var(--border-color);overflow-y: auto;padding: 5px; /* Minimal padding for log entries */
}.traffic-entry {display: flex;justify-content: space-between;align-items: center;padding: 6px 10px;margin-bottom: 3px;border-radius: 4px;font-size: 0.85em;cursor: pointer;border: 1px solid transparent;border-left: 3px solid var(--border-color);transition: background-color var(--animation-duration) ease, border-color var(--animation-duration) ease;background-color: #fff;
}.traffic-entry:hover {background-color: #f0f0f0;
}.traffic-entry.selected {background-color: var(--traffic-selected-bg);border-color: var(--primary-color);
}.traffic-entry span {white-space: nowrap;overflow: hidden;text-overflow: ellipsis;padding: 0 5px;
}.traffic-src-dest {flex-basis: 40%;display: flex;align-items: center;gap: 5px;
}
.traffic-protocol {flex-basis: 15%;font-weight: 500;
}
.traffic-info {flex-basis: 35%;color: var(--text-color-secondary);text-align: right;
}
.traffic-alert-level {flex-basis: 10%;text-align: center;font-weight: bold;
}/* Traffic Entry Alert Levels & Protocol Type */
.traffic-entry[data-alert-level="critical"] { border-left-color: var(--alert-critical-border); background-color: var(--alert-critical-bg); }
.traffic-entry[data-alert-level="warning"] { border-left-color: var(--alert-warning-border); background-color: var(--alert-warning-bg); }
.traffic-entry[data-alert-level="info"] { border-left-color: var(--alert-info-border); background-color: var(--alert-info-bg); }
.traffic-entry[data-protocol-type="ot"] .traffic-protocol { color: var(--ot-protocol-color); }
.traffic-alert-level.critical { color: var(--error-color); }
.traffic-alert-level.warning { color: var(--warning-color); }
.traffic-alert-level.info { color: var(--info-color); }.protocol-analysis {height: 40%; /* Remaining space */display: flex;flex-direction: column;
}.protocol-analysis h3 {font-size: 0.9em;font-weight: 600;padding: 10px var(--panel-padding) 5px;border-bottom: 1px solid var(--border-color);flex-shrink: 0;background-color: #f8f8f8;
}#analysis-content {flex-grow: 1;overflow-y: auto;padding: var(--panel-padding);font-family: monospace; /* Good for protocol fields */font-size: 0.85em;line-height: 1.7;
}.analysis-field {margin-bottom: 4px;
}.analysis-key {display: inline-block;width: 150px;font-weight: 600;color: var(--text-color-secondary);white-space: nowrap;
}.analysis-value {color: var(--text-color);
}
.analysis-value.highlight { /* For suspicious values */color: var(--error-color);font-weight: bold;background-color: var(--alert-critical-bg);padding: 0 3px;border-radius: 3px;
}/* Alerts Panel */
.alerts-panel .panel-header button {padding: 4px 10px;font-size: 0.85em;background-color: #fff;border: 1px solid var(--border-color);border-radius: 6px;cursor: pointer;transition: background-color var(--animation-duration) ease;
}
.alerts-panel .panel-header button:hover {background-color: #eee;
}
.alerts-panel .panel-header button i {margin-right: 5px;
}#alerts-log {padding: 5px; /* Minimal padding for alert entries */
}.alert-entry {padding: 8px 12px;margin-bottom: 6px;border-radius: 6px;border-left: 4px solid;font-size: 0.9em;
}.alert-entry strong {font-weight: 600;
}.alert-entry .alert-time {font-size: 0.9em;color: var(--text-color-secondary);margin-right: 10px;
}/* Alert Severity Styles */
.alert-entry.critical {background-color: var(--alert-critical-bg);border-left-color: var(--alert-critical-border);
}
.alert-entry.warning {background-color: var(--alert-warning-bg);border-left-color: var(--alert-warning-border);
}
.alert-entry.info {background-color: var(--alert-info-bg);border-left-color: var(--alert-info-border);
}/* Footer */
.app-footer {display: flex;justify-content: space-between;align-items: center;padding: 0 var(--panel-padding);border-top: 1px solid var(--border-color);height: var(--footer-height);flex-shrink: 0;background-color: #f8f8f8;border-bottom-left-radius: var(--border-radius);border-bottom-right-radius: var(--border-radius);
}.simulation-controls {display: flex;align-items: center;gap: 10px;
}.simulation-controls button {padding: 6px 12px;font-size: 0.9em;border: 1px solid var(--border-color);border-radius: 6px;background-color: #fff;cursor: pointer;transition: background-color var(--animation-duration) ease;
}
.simulation-controls button:hover:not(:disabled) {background-color: #eee;
}
.simulation-controls button:disabled {opacity: 0.5;cursor: not-allowed;
}
.simulation-controls button i {margin-right: 5px;
}.simulation-controls label {font-size: 0.9em;color: var(--text-color-secondary);
}#speed-slider {cursor: pointer;
}.simulation-info {font-size: 0.9em;color: var(--text-color-secondary);
}/* General Helpers */
.placeholder {text-align: center;color: var(--text-color-secondary);font-style: italic;padding: 20px;width: 100%;
}.header-filter {padding: 4px 8px;border: 1px solid var(--border-color);border-radius: 6px;font-size: 0.9em;background-color: #fff;
}/* Responsive Adjustments */
@media (max-width: 1100px) {.app-content {grid-template-columns: 1fr; /* Stack all panels */grid-template-rows: repeat(3, minmax(200px, auto)); /* Auto height rows */height: calc(100% - var(--panel-header-height) - var(--footer-height) - 1px); /* Adjust height calc */overflow-y: auto; /* Allow content area to scroll if needed */}.panel {height: auto; /* Allow panels to size based on content */}#network-topology {min-height: 250px;height: 250px; /* Fixed height for topology on smaller screens */}.asset-details {height: auto;min-height: 150px;}.traffic-panel .panel-content {height: auto;min-height: 300px;}#live-traffic-log {min-height: 200px;}.protocol-analysis {min-height: 150px;height: auto;}#alerts-log {min-height: 150px;}.app-container {max-height: none;margin: 0;border-radius: 0;}.app-header h1 { font-size: 1em; }.header-status { display: none; } /* Hide status on very small screens */
}@media (max-width: 600px) {.app-header {flex-direction: column;align-items: flex-start;gap: 5px;}.app-footer {flex-direction: column;height: auto;padding: 10px;gap: 10px;}.simulation-controls {width: 100%;justify-content: center;}.simulation-info {width: 100%;text-align: center;}
}
script.js
document.addEventListener('DOMContentLoaded', () => {console.log('Network Anomaly Detection Component Initializing...');// --- Configuration & Constants ---const MAX_LOG_ENTRIES = 100; // Max entries for traffic and alertsconst BASE_SIMULATION_INTERVAL_MS = 1000; // Base interval at speed x1const PROTOCOLS = {MODBUS_TCP: 'Modbus/TCP',DNP3_SIM: 'DNP3 (Sim)',TCP: 'TCP',UDP: 'UDP',ICMP: 'ICMP',HTTP: 'HTTP'};const ALERT_LEVELS = {INFO: 'info',WARNING: 'warning',CRITICAL: 'critical'};const ASSET_TYPES = {FIREWALL: 'Firewall',SERVER: 'Server',HMI: 'HMI',ENGINEERING_WS: 'Engineering Workstation',PLC: 'PLC',SAFETY_PLC: 'Safety PLC',RTU: 'RTU',SENSOR: 'Sensor',IT_WS: 'IT Workstation'};// --- DOM Elements ---const systemStatusElement = document.getElementById('system-status');const activeAlertsCountElement = document.getElementById('active-alerts-count');const processedPacketsCountElement = document.getElementById('processed-packets-count');const networkTopologyContainer = document.getElementById('network-topology');const assetInfoContentElement = document.getElementById('asset-info-content');const liveTrafficLogContainer = document.getElementById('live-traffic-log');const trafficFilterElement = document.getElementById('traffic-filter');const analysisContentElement = document.getElementById('analysis-content');const alertsLogContainer = document.getElementById('alerts-log');const clearAlertsBtn = document.getElementById('clear-alerts-btn');const startSimBtn = document.getElementById('start-sim-btn');const stopSimBtn = document.getElementById('stop-sim-btn');const speedSlider = document.getElementById('speed-slider');const speedValueElement = document.getElementById('speed-value');const simulationTimeElement = document.getElementById('simulation-time');// --- Application State ---let simulationIntervalId = null;let simulationSpeed = 3;let simulationRunning = false;let simulationTicks = 0;let processedPackets = 0;let activeAlerts = 0;let networkAssets = {};let networkLinks = [];let allTraffic = [];let allAlerts = [];let selectedTrafficEntry = null;let selectedAssetId = null;// --- Network Definition (Simplified Nuclear DCS) ---function defineNetwork() {networkAssets = {// IT Zone'it_ws_1': { id: 'it_ws_1', name: 'IT-WS-01', ip: '192.168.1.10', type: ASSET_TYPES.IT_WS, zone: 'IT', status: 'ok', details: '标准IT办公终端' },'it_server_1': { id: 'it_server_1', name: 'IT-SRV-01', ip: '192.168.1.5', type: ASSET_TYPES.SERVER, zone: 'IT', status: 'ok', details: '企业邮件服务器' },// DMZ'dmz_server_1': { id: 'dmz_server_1', name: 'DMZ-SRV-01', ip: '10.1.1.10', type: ASSET_TYPES.SERVER, zone: 'DMZ', status: 'ok', details: '数据Historian跳转服务器' },'fw_it_dmz': { id: 'fw_it_dmz', name: 'FW-IT-DMZ', ip: '192.168.1.1 / 10.1.1.1', type: ASSET_TYPES.FIREWALL, zone: 'Boundary', status: 'ok', details: 'IT/DMZ 边界防火墙' },// Plant Control Network (PCN)'pcn_hmi_1': { id: 'pcn_hmi_1', name: 'PCN-HMI-01', ip: '10.10.10.20', type: ASSET_TYPES.HMI, zone: 'PCN', status: 'ok', details: '主控制室操作员站' },'pcn_eng_ws_1': { id: 'pcn_eng_ws_1', name: 'PCN-ENG-01', ip: '10.10.10.25', type: ASSET_TYPES.ENGINEERING_WS, zone: 'PCN', status: 'ok', details: '工程师站' },'pcn_plc_1': { id: 'pcn_plc_1', name: 'PCN-PLC-01', ip: '10.10.20.30', type: ASSET_TYPES.PLC, zone: 'PCN', status: 'ok', details: '常规工艺回路控制器' },'pcn_plc_2': { id: 'pcn_plc_2', name: 'PCN-PLC-02', ip: '10.10.20.31', type: ASSET_TYPES.PLC, zone: 'PCN', status: 'ok', details: '辅助系统控制器' },'fw_dmz_pcn': { id: 'fw_dmz_pcn', name: 'FW-DMZ-PCN', ip: '10.1.1.2 / 10.10.1.1', type: ASSET_TYPES.FIREWALL, zone: 'Boundary', status: 'ok', details: 'DMZ/PCN 边界防火墙' },// Safety Instrumented System (SIS)'sis_hmi_1': { id: 'sis_hmi_1', name: 'SIS-HMI-01', ip: '10.20.10.20', type: ASSET_TYPES.HMI, zone: 'SIS', status: 'ok', details: '安全系统监控站' },'sis_plc_1': { id: 'sis_plc_1', name: 'SIS-PLC-01', ip: '10.20.20.40', type: ASSET_TYPES.SAFETY_PLC, zone: 'SIS', status: 'ok', details: '反应堆保护系统控制器 (A组)' },'sis_plc_2': { id: 'sis_plc_2', name: 'SIS-PLC-02', ip: '10.20.20.41', type: ASSET_TYPES.SAFETY_PLC, zone: 'SIS', status: 'ok', details: '反应堆保护系统控制器 (B组)' },'fw_pcn_sis': { id: 'fw_pcn_sis', name: 'FW-PCN-SIS', ip: '10.10.1.2 / 10.20.1.1', type: ASSET_TYPES.FIREWALL, zone: 'Boundary', status: 'ok', details: 'PCN/SIS 边界防火墙 (单向)' },// Field Devices (Conceptual)'field_sensor_1': { id: 'field_sensor_1', name: 'Sensor-Temp-R1', ip: '10.10.30.50', type: ASSET_TYPES.SENSOR, zone: 'Field', status: 'ok', details: '反应堆温度传感器 (连接 PCN-PLC-01)' },'field_safety_sensor_1': { id: 'field_safety_sensor_1', name: 'Sensor-Pressure-S1', ip: '10.20.30.60', type: ASSET_TYPES.SENSOR, zone: 'Field', status: 'ok', details: '安全壳压力传感器 (连接 SIS-PLC-01)' },// External Attacker (Conceptual)'external_attacker': { id: 'external_attacker', name: 'Ext-Attacker', ip: '203.0.113.100', type: 'Attacker', zone: 'External', status: 'malicious', details: '模拟外部攻击源' }};networkLinks = [{ source: 'it_ws_1', target: 'it_server_1' }, { source: 'it_ws_1', target: 'fw_it_dmz' },{ source: 'it_server_1', target: 'fw_it_dmz' },{ source: 'fw_it_dmz', target: 'dmz_server_1' },{ source: 'dmz_server_1', target: 'fw_dmz_pcn' },{ source: 'fw_dmz_pcn', target: 'pcn_hmi_1' }, { source: 'fw_dmz_pcn', target: 'pcn_eng_ws_1' },{ source: 'pcn_hmi_1', target: 'pcn_plc_1' }, { source: 'pcn_hmi_1', target: 'pcn_plc_2' },{ source: 'pcn_eng_ws_1', target: 'pcn_plc_1' }, { source: 'pcn_eng_ws_1', target: 'pcn_plc_2' },{ source: 'pcn_plc_1', target: 'field_sensor_1' }, // Simplified link{ source: 'fw_dmz_pcn', target: 'fw_pcn_sis' }, // Connection between FWs{ source: 'fw_pcn_sis', target: 'sis_hmi_1' }, // Typically one-way data diode or strict firewall{ source: 'sis_hmi_1', target: 'sis_plc_1' }, { source: 'sis_hmi_1', target: 'sis_plc_2' },{ source: 'sis_plc_1', target: 'field_safety_sensor_1' }, // Simplified link// Conceptual attacker links{ source: 'external_attacker', target: 'fw_it_dmz'}, // Initial probe/attack vector];}// --- Simulation Logic ---function runSimulationStep() {simulationTicks++;updateSimulationTime();// Generate multiple packets per tick based on speedconst packetsToGenerate = Math.ceil(Math.random() * simulationSpeed);for (let i = 0; i < packetsToGenerate; i++) {generateTrafficPacket();}// Sometimes trigger a specific attack scenarioif (Math.random() < 0.01 * simulationSpeed) { // Low probability, increases with speedtriggerZeroDayScenario();}}function generateTrafficPacket() {processedPackets++;const packet = { id: `pkt_${Date.now()}_${Math.random().toString(16).slice(2)}`, timestamp: new Date() };// Basic Traffic Type Distributionconst rand = Math.random();let srcAsset, dstAsset, commPattern;if (rand < 0.4) { // Normal PCN OT Traffic (HMI/ENG <-> PLC)commPattern = 'pcn_ot';srcAsset = Math.random() < 0.5 ? networkAssets['pcn_hmi_1'] : networkAssets['pcn_eng_ws_1'];dstAsset = Math.random() < 0.5 ? networkAssets['pcn_plc_1'] : networkAssets['pcn_plc_2'];if (Math.random() < 0.5) [srcAsset, dstAsset] = [dstAsset, srcAsset]; // Allow PLC responsespacket.protocol = PROTOCOLS.MODBUS_TCP;packet.protocolData = generateModbusData(srcAsset, dstAsset, false);} else if (rand < 0.6) { // Normal SIS OT Traffic (HMI -> PLC, restricted)commPattern = 'sis_ot';srcAsset = networkAssets['sis_hmi_1'];dstAsset = Math.random() < 0.5 ? networkAssets['sis_plc_1'] : networkAssets['sis_plc_2'];packet.protocol = PROTOCOLS.DNP3_SIM; // Use simulated DNP3 for varietypacket.protocolData = generateDnp3Data(srcAsset, dstAsset, false);} else if (rand < 0.75) { // IT Traffic (WS <-> Server, WS -> FW)commPattern = 'it_traffic';srcAsset = networkAssets['it_ws_1'];dstAsset = Math.random() < 0.5 ? networkAssets['it_server_1'] : networkAssets['fw_it_dmz'];packet.protocol = Math.random() < 0.7 ? PROTOCOLS.TCP : PROTOCOLS.HTTP;packet.protocolData = { port: packet.protocol === PROTOCOLS.HTTP ? 80 : Math.floor(1024 + Math.random() * 64511), size: Math.floor(64 + Math.random() * 1400) };} else if (rand < 0.85) { // DMZ Traffic (Historian related)commPattern = 'dmz_traffic';srcAsset = Math.random() < 0.5 ? networkAssets['fw_it_dmz'] : networkAssets['dmz_server_1'];dstAsset = srcAsset.id === 'dmz_server_1' ? networkAssets['fw_dmz_pcn'] : networkAssets['dmz_server_1'];packet.protocol = PROTOCOLS.TCP;packet.protocolData = { port: 1433, size: Math.floor(100 + Math.random() * 500) }; // Simulate SQL traffic} else { // Background noise / Scans (low probability)commPattern = 'background';const assetIds = Object.keys(networkAssets).filter(id => id !== 'external_attacker');srcAsset = networkAssets[assetIds[Math.floor(Math.random() * assetIds.length)]];dstAsset = networkAssets[assetIds[Math.floor(Math.random() * assetIds.length)]];// Avoid self-communicationwhile (dstAsset.id === srcAsset.id) {dstAsset = networkAssets[assetIds[Math.floor(Math.random() * assetIds.length)]];}packet.protocol = Math.random() < 0.5 ? PROTOCOLS.UDP : PROTOCOLS.ICMP;packet.protocolData = { type: packet.protocol === PROTOCOLS.ICMP ? (Math.random() < 0.5 ? 8 : 0) : 'N/A', size: Math.floor(32 + Math.random() * 32) };}if (!srcAsset || !dstAsset) {console.warn("Failed to determine src/dst asset for pattern:", commPattern);return; // Skip packet if assets are invalid}packet.srcIp = srcAsset.ip.split(' ')[0]; // Take first IP if multiplepacket.srcName = srcAsset.name;packet.dstIp = dstAsset.ip.split(' ')[0];packet.dstName = dstAsset.name;packet.isOT = packet.protocol === PROTOCOLS.MODBUS_TCP || packet.protocol === PROTOCOLS.DNP3_SIM;// Detect anomaliesconst anomaly = detectAnomalies(packet, srcAsset, dstAsset);packet.anomaly = anomaly;packet.alertLevel = anomaly ? anomaly.level : null;allTraffic.unshift(packet); // Add to beginning of arrayif (allTraffic.length > MAX_LOG_ENTRIES * 2) { // Keep buffer slightly larger than displayallTraffic.pop();}if (anomaly) {triggerAlert(packet, anomaly);}updateHeaderStatus();filterAndRenderTraffic();}function generateModbusData(src, dst, isAttack) {const data = {transactionId: Math.floor(Math.random() * 65535),unitId: 1};const isRead = Math.random() < 0.7;if (isAttack && isAttack.type === 'invalid_function_code') {data.functionCode = 129; // Example invalid codedata.description = "非法功能码读写尝试";data.address = Math.floor(Math.random() * 1000);data.count = Math.floor(Math.random() * 10);} else if (isAttack && isAttack.type === 'dangerous_write') {data.functionCode = 16; // Write Multiple Registersdata.address = isAttack.targetAddress || 40100; // Example target address (e.g., setpoint)data.values = [isAttack.dangerousValue || 9999]; // Example dangerous valuedata.description = `写入危险值 ${data.values[0]} 到寄存器 ${data.address}`;} else if (isRead) {data.functionCode = Math.random() < 0.8 ? 3 : 4; // Read Holding/Input Registersdata.address = Math.floor(Math.random() * 1000);data.count = Math.floor(1 + Math.random() * 16);data.description = `读取寄存器 ${data.address} (${data.count}个)`;} else { // Normal Writedata.functionCode = Math.random() < 0.8 ? 6 : 16; // Write Single/Multiple Registersdata.address = Math.floor(Math.random() * 1000);if (data.functionCode === 6) {data.value = Math.floor(Math.random() * 1000);data.description = `写入值 ${data.value} 到寄存器 ${data.address}`;} else {data.values = [Math.floor(Math.random() * 1000), Math.floor(Math.random() * 1000)];data.description = `写入 ${data.values.length} 个值到寄存器 ${data.address}`;}}return data;}function generateDnp3Data(src, dst, isAttack) {// Simplified DNP3-like dataconst data = {sourceAddr: src.ip.split('.')[3], // Use last octet as addressdestAddr: dst.ip.split('.')[3],};if (isAttack && isAttack.type === 'unsolicited_response') {data.functionCode = 'Unsolicited Response';data.objects = [{ type: 'Binary Input', index: 1, value: 1, flags: 'Online' }];data.description = "来自非预期设备的非请求响应";} else if (isAttack && isAttack.type === 'manipulated_control') {data.functionCode = 'Operate DO'; // Direct Operatedata.objects = [{ type: 'Control Relay Output Block', index: isAttack.targetIndex || 5, opType: 'Pulse ON', count: 1, onTime: 5000, offTime: 0 }];data.description = `向关键索引 ${data.objects[0].index} 发送异常操作指令`;} else {data.functionCode = Math.random() < 0.5 ? 'Read Class 1/2/3' : 'Write Time';data.objects = [{ type: 'Analog Input', index: Math.floor(Math.random()*5), value: (Math.random()*100).toFixed(2), flags: 'Online' }];data.description = data.functionCode === 'Write Time' ? '设备时间同步' : '读取常规数据';}return data;}function triggerZeroDayScenario() {console.log("Triggering simulated zero-day scenario...");const scenarioType = Math.floor(Math.random() * 4); // Choose one of the scenarioslet packet = { id: `pkt_${Date.now()}_${Math.random().toString(16).slice(2)}`, timestamp: new Date(), anomalyForced: true };let srcAsset, dstAsset, attackDetails = {};try {switch(scenarioType) {case 0: // Invalid Modbus Function Code to Safety PLCattackDetails = { type: 'invalid_function_code' };srcAsset = networkAssets['pcn_eng_ws_1']; // Assume compromised Eng WSdstAsset = networkAssets['sis_plc_1'];packet.protocol = PROTOCOLS.MODBUS_TCP;packet.protocolData = generateModbusData(srcAsset, dstAsset, attackDetails);packet.anomalyDescription = "[零日模拟] 检测到向安全PLC发送无效Modbus功能码";packet.alertLevel = ALERT_LEVELS.CRITICAL;break;case 1: // Dangerous Modbus Write to Normal PLCattackDetails = { type: 'dangerous_write', targetAddress: 40050, dangerousValue: 850 };srcAsset = networkAssets['pcn_hmi_1']; // Assume compromised HMIdstAsset = networkAssets['pcn_plc_1'];packet.protocol = PROTOCOLS.MODBUS_TCP;packet.protocolData = generateModbusData(srcAsset, dstAsset, attackDetails);packet.anomalyDescription = "[零日模拟] 检测到向关键地址写入潜在危险值";packet.alertLevel = ALERT_LEVELS.CRITICAL;break;case 2: // Unsolicited DNP3 Response from unexpected PLCattackDetails = { type: 'unsolicited_response' };srcAsset = networkAssets['pcn_plc_2']; // Unexpected sourcedstAsset = networkAssets['sis_hmi_1']; // Target is SIS HMIpacket.protocol = PROTOCOLS.DNP3_SIM;packet.protocolData = generateDnp3Data(srcAsset, dstAsset, attackDetails);packet.anomalyDescription = "[零日模拟] 检测到来自非预期源的DNP3非请求响应";packet.alertLevel = ALERT_LEVELS.WARNING;break;case 3: // OT Port Scan from DMZ Server (Should not happen)srcAsset = networkAssets['dmz_server_1']; // Compromised DMZ ServerdstAsset = networkAssets['pcn_plc_1'];packet.protocol = PROTOCOLS.TCP;packet.protocolData = { port: 502, flags: 'SYN', size: 40 }; // SYN packet to Modbus portpacket.anomalyDescription = "[零日模拟] 检测到来自DMZ服务器对OT设备的端口扫描";packet.alertLevel = ALERT_LEVELS.WARNING;break;}if (!srcAsset || !dstAsset) {console.error("Failed to generate zero-day packet, invalid assets");return;}packet.srcIp = srcAsset.ip.split(' ')[0];packet.srcName = srcAsset.name;packet.dstIp = dstAsset.ip.split(' ')[0];packet.dstName = dstAsset.name;packet.isOT = packet.protocol === PROTOCOLS.MODBUS_TCP || packet.protocol === PROTOCOLS.DNP3_SIM;packet.anomaly = { ruleId: `ZD-${scenarioType}`, description: packet.anomalyDescription, level: packet.alertLevel };processedPackets++;allTraffic.unshift(packet);if (allTraffic.length > MAX_LOG_ENTRIES * 2) { allTraffic.pop(); }triggerAlert(packet, packet.anomaly);updateHeaderStatus();filterAndRenderTraffic();} catch (error) {console.error("Error generating zero-day scenario:", error);}}// --- Anomaly Detection Logic (Simplified Rules) ---function detectAnomalies(packet, srcAsset, dstAsset) {if (packet.anomalyForced) return packet.anomaly; // Already determined by zero-day trigger// Rule 1: Communication across forbidden boundariesif (srcAsset.zone === 'IT' && (dstAsset.zone === 'PCN' || dstAsset.zone === 'SIS')) {return { ruleId: 'RULE001', description: `禁止的通信: IT区 (${srcAsset.name}) -> ${dstAsset.zone}区 (${dstAsset.name})`, level: ALERT_LEVELS.CRITICAL };}if ((srcAsset.zone === 'PCN' || srcAsset.zone === 'DMZ') && dstAsset.zone === 'SIS' && dstAsset.type !== ASSET_TYPES.FIREWALL && srcAsset.type !== ASSET_TYPES.FIREWALL) {// Allow only through FW or specific allowed hosts (not modeled here)if (packet.protocol !== PROTOCOLS.DNP3_SIM) { // Assume only DNP3 is allowed for SIS comms herereturn { ruleId: 'RULE002', description: `禁止的通信: ${srcAsset.zone}区 (${srcAsset.name}) -> SIS区 (${dstAsset.name}) 使用非预期协议 ${packet.protocol}`, level: ALERT_LEVELS.CRITICAL };}}if (srcAsset.zone === 'External' && dstAsset.zone !== 'DMZ' && dstAsset.type !== ASSET_TYPES.FIREWALL) {return { ruleId: 'RULE003', description: `禁止的通信: 外部 (${srcAsset.name}) -> 内部非DMZ区 (${dstAsset.name})`, level: ALERT_LEVELS.WARNING };}// Rule 2: Invalid OT operationsif (packet.isOT) {if (packet.protocolData.functionCode > 127) { // Standard Modbus codes are usually lowerreturn { ruleId: 'RULE101', description: `检测到无效/扩展功能码 (${packet.protocolData.functionCode}) from ${srcAsset.name} to ${dstAsset.name}`, level: ALERT_LEVELS.WARNING };}// Check for writes to safety PLCs from non-Eng stations (simplified)if (dstAsset.type === ASSET_TYPES.SAFETY_PLC && packet.protocolData.functionCode >= 5 && packet.protocolData.functionCode <= 16 && srcAsset.type !== ASSET_TYPES.ENGINEERING_WS && srcAsset.type !== ASSET_TYPES.HMI) { // Function codes 5, 6, 15, 16 are writesreturn { ruleId: 'RULE102', description: `非法写入尝试: ${srcAsset.type} (${srcAsset.name}) 尝试写入安全PLC (${dstAsset.name})`, level: ALERT_LEVELS.CRITICAL };}// Add more OT specific rules: e.g., unexpected register ranges, high frequency commands}// Rule 3: Basic Scan Detection (simple example)if (packet.protocol === PROTOCOLS.ICMP && packet.protocolData.type === 8) { // Echo request// Basic check: If many ICMP requests seen recently from same source - very naiveconst recentIcmp = allTraffic.slice(0, 50).filter(p => p.srcIp === packet.srcIp && p.protocol === PROTOCOLS.ICMP && p.protocolData.type === 8);if (recentIcmp.length > 5) {return { ruleId: 'RULE201', description: `潜在ICMP扫描行为: 检测到来自 ${packet.srcIp} 的大量 Echo 请求`, level: ALERT_LEVELS.INFO };}}if (packet.protocol === PROTOCOLS.TCP && packet.protocolData.flags === 'SYN') {const recentSyn = allTraffic.slice(0, 100).filter(p => p.srcIp === packet.srcIp && p.protocol === PROTOCOLS.TCP && p.protocolData.flags === 'SYN');const uniquePorts = new Set(recentSyn.map(p => p.protocolData.port));if (uniquePorts.size > 10) {return { ruleId: 'RULE202', description: `潜在TCP端口扫描行为: 检测到来自 ${packet.srcIp} 对多个端口的连接尝试`, level: ALERT_LEVELS.INFO };}}return null; // No anomaly detected}function triggerAlert(packet, anomaly) {const alert = {id: `alert_${Date.now()}_${Math.random().toString(16).slice(2)}`,timestamp: new Date(),level: anomaly.level,description: anomaly.description,ruleId: anomaly.ruleId,srcIp: packet.srcIp,dstIp: packet.dstIp,protocol: packet.protocol,packetId: packet.id};allAlerts.unshift(alert);if (allAlerts.length > MAX_LOG_ENTRIES * 2) {allAlerts.pop();}activeAlerts = allAlerts.filter(a => a.level === ALERT_LEVELS.CRITICAL || a.level === ALERT_LEVELS.WARNING).length; // Count active non-info alertsrenderAlerts();updateSystemStatusBasedOnAlerts();}// --- Rendering Functions ---function renderTopology() {// Simple div based rendering for topology - Replace with SVG/Canvas for real viznetworkTopologyContainer.innerHTML = '';const zones = { 'IT': [], 'DMZ': [], 'PCN': [], 'SIS': [], 'Field': [], 'Boundary': [], 'External': [] };Object.values(networkAssets).forEach(asset => {if (zones[asset.zone]) {zones[asset.zone].push(asset);} else {zones['Other'] = zones['Other'] || [];zones['Other'].push(asset);}});const zoneOrder = ['External', 'IT', 'Boundary', 'DMZ', 'Boundary', 'PCN', 'Boundary', 'SIS', 'Field'];const topologyDiv = document.createElement('div');topologyDiv.style.display = 'flex';topologyDiv.style.justifyContent = 'space-around';topologyDiv.style.alignItems = 'flex-start';topologyDiv.style.padding = '20px';topologyDiv.style.width = '100%';topologyDiv.style.height = '100%';zoneOrder.forEach(zoneName => {if (zones[zoneName] && zones[zoneName].length > 0) {const zoneDiv = document.createElement('div');zoneDiv.style.border = '1px dashed var(--secondary-color)';zoneDiv.style.borderRadius = 'var(--border-radius)';zoneDiv.style.padding = '10px';zoneDiv.style.margin = '5px';zoneDiv.style.textAlign = 'center';zoneDiv.style.minWidth = '100px';const title = document.createElement('h4');title.textContent = zoneName;title.style.fontSize = '0.8em';title.style.color = 'var(--text-color-secondary)';title.style.marginBottom = '10px';zoneDiv.appendChild(title);zones[zoneName].forEach(asset => {const nodeDiv = document.createElement('div');nodeDiv.textContent = asset.name;nodeDiv.title = asset.type;nodeDiv.dataset.assetId = asset.id;nodeDiv.classList.add('network-node'); // For potential future styling/interactionnodeDiv.style.backgroundColor = '#fff';nodeDiv.style.border = '1px solid var(--border-color)';nodeDiv.style.borderRadius = '4px';nodeDiv.style.padding = '4px 8px';nodeDiv.style.margin = '5px 0';nodeDiv.style.fontSize = '0.8em';nodeDiv.style.cursor = 'pointer';if (asset.status === 'warning') nodeDiv.style.borderColor = 'var(--warning-color)';if (asset.status === 'critical' || asset.status === 'malicious') nodeDiv.style.borderColor = 'var(--error-color)';if (asset.id === selectedAssetId) nodeDiv.style.boxShadow = '0 0 0 2px var(--primary-color)';nodeDiv.addEventListener('click', () => handleAssetClick(asset.id));zoneDiv.appendChild(nodeDiv);});topologyDiv.appendChild(zoneDiv);}});networkTopologyContainer.appendChild(topologyDiv);}function renderAssetDetails(assetId) {selectedAssetId = assetId;const asset = networkAssets[assetId];if (!asset) {assetInfoContentElement.innerHTML = '<p class="placeholder">无法找到资产信息。</p>';renderTopology(); // Re-render topology to remove selection highlightreturn;}assetInfoContentElement.innerHTML = `<p><strong>ID:</strong> ${asset.id}</p><p><strong>名称:</strong> ${asset.name}</p><p><strong>类型:</strong> ${asset.type}</p><p><strong>IP 地址:</strong> ${asset.ip}</p><p><strong>区域:</strong> ${asset.zone}</p><p><strong>状态:</strong> <span class="status-${asset.status}">${asset.status.toUpperCase()}</span></p><p><strong>详情:</strong> ${asset.details}</p>`;renderTopology(); // Re-render topology to apply selection highlight}function filterAndRenderTraffic() {const filterValue = trafficFilterElement.value;let filteredTraffic = allTraffic;if (filterValue === 'ot') {filteredTraffic = allTraffic.filter(p => p.isOT);} else if (filterValue === 'anomalous') {filteredTraffic = allTraffic.filter(p => p.anomaly);} else if (filterValue === 'critical') {filteredTraffic = allTraffic.filter(p => p.alertLevel === ALERT_LEVELS.CRITICAL);}renderTrafficLog(filteredTraffic.slice(0, MAX_LOG_ENTRIES)); // Render only max entries}function renderTrafficLog(trafficToRender) {liveTrafficLogContainer.innerHTML = ''; // Clear existingif (trafficToRender.length === 0) {liveTrafficLogContainer.innerHTML = '<p class="placeholder">无匹配流量数据。</p>';return;}trafficToRender.forEach(packet => {const entry = document.createElement('div');entry.classList.add('traffic-entry');entry.dataset.packetId = packet.id;if (packet.isOT) entry.dataset.protocolType = 'ot';if (packet.alertLevel) entry.dataset.alertLevel = packet.alertLevel;if (selectedTrafficEntry && selectedTrafficEntry.id === packet.id) {entry.classList.add('selected');}// Simplify info displaylet info = packet.protocolData ? packet.protocolData.description || `Size: ${packet.protocolData.size || 'N/A'}` : '';if (packet.anomaly) info = packet.anomaly.description.split(':')[0]; // Show first part of anomaly descentry.innerHTML = `<span class="traffic-src-dest" title="${packet.srcName} -> ${packet.dstName}"><i class="fas fa-arrow-right"></i><span>${packet.srcIp} → ${packet.dstIp}</span></span><span class="traffic-protocol">${packet.protocol}</span><span class="traffic-info" title="${info}">${info.length > 40 ? info.substring(0, 37) + '...' : info}</span><span class="traffic-alert-level ${packet.alertLevel || ''}">${packet.alertLevel ? packet.alertLevel.toUpperCase().slice(0, 4) : '-'}</span>`;entry.addEventListener('click', () => handleTrafficClick(packet.id));liveTrafficLogContainer.appendChild(entry);});}function renderProtocolAnalysis(packetId) {const packet = allTraffic.find(p => p.id === packetId);if (!packet) {analysisContentElement.innerHTML = '<p class="placeholder">无法找到流量详情。</p>';return;}let html = '';const data = packet.protocolData;function renderField(key, value, highlight = false) {const valueClass = highlight ? 'analysis-value highlight' : 'analysis-value';// Sanitize value slightly to prevent potential HTML injection if data source was realconst safeValue = String(value).replace(/</g, '<').replace(/>/g, '>');return `<div class="analysis-field"><span class="analysis-key">${key}:</span> <span class="${valueClass}">${safeValue}</span></div>`;}html += renderField('时间戳', packet.timestamp.toLocaleTimeString('sv-SE') + '.' + packet.timestamp.getMilliseconds());html += renderField('源 IP', packet.srcIp);html += renderField('目的 IP', packet.dstIp);html += renderField('协议', packet.protocol);if (packet.isOT && data) {html += '<hr style="margin: 5px 0; border-color: var(--border-color);">';if (packet.protocol === PROTOCOLS.MODBUS_TCP) {html += renderField('事务 ID', data.transactionId);html += renderField('单元 ID', data.unitId);html += renderField('功能码', data.functionCode, packet.anomaly?.ruleId === 'RULE101' || (packet.anomalyForced && packet.anomaly.description.includes('功能码')));html += renderField('地址', data.address);if (data.count !== undefined) html += renderField('数量', data.count);if (data.value !== undefined) html += renderField('写入值', data.value, packet.anomaly?.ruleId === 'RULE102' || (packet.anomalyForced && packet.anomaly.description.includes('危险值')));if (data.values !== undefined) html += renderField('写入值(多)', JSON.stringify(data.values), packet.anomaly?.ruleId === 'RULE102' || (packet.anomalyForced && packet.anomaly.description.includes('危险值')));} else if (packet.protocol === PROTOCOLS.DNP3_SIM) {html += renderField('源地址 (DNP3)', data.sourceAddr);html += renderField('目的地址 (DNP3)', data.destAddr);html += renderField('功能码 (DNP3)', data.functionCode, packet.anomalyForced && packet.anomaly.description.includes('非请求响应'));if (data.objects) {data.objects.forEach((obj, i) => {html += `<div style="margin-left: 10px; border-left: 1px solid #eee; padding-left: 5px;">`;html += renderField(`对象 ${i} 类型`, obj.type);if (obj.index !== undefined) html += renderField(`对象 ${i} 索引`, obj.index, packet.anomalyForced && packet.anomaly.description.includes('关键索引'));if (obj.value !== undefined) html += renderField(`对象 ${i} 值`, obj.value);if (obj.flags !== undefined) html += renderField(`对象 ${i} 标志`, obj.flags);if (obj.opType !== undefined) html += renderField(`对象 ${i} 操作类型`, obj.opType, packet.anomalyForced && packet.anomaly.description.includes('异常操作'));// Add other DNP3 fields if neededhtml += `</div>`;});}}} else if (data) {html += '<hr style="margin: 5px 0; border-color: var(--border-color);">';if (data.port !== undefined) html += renderField('端口', data.port);if (data.flags !== undefined) html += renderField('TCP 标志', data.flags, packet.anomaly?.ruleId === 'RULE202');if (data.type !== undefined) html += renderField('ICMP 类型', data.type === 8 ? 'Echo Request' : (data.type === 0 ? 'Echo Reply' : data.type), packet.anomaly?.ruleId === 'RULE201');if (data.size !== undefined) html += renderField('大小 (bytes)', data.size);}if (packet.anomaly) {html += '<hr style="margin: 10px 0; border-color: var(--error-color);">';html += renderField('检测到异常!', packet.anomaly.description, true);html += renderField('规则 ID', packet.anomaly.ruleId);html += renderField('严重级别', packet.anomaly.level.toUpperCase(), true);}analysisContentElement.innerHTML = html;}function renderAlerts() {alertsLogContainer.innerHTML = '';const alertsToRender = allAlerts.slice(0, MAX_LOG_ENTRIES);if (alertsToRender.length === 0) {alertsLogContainer.innerHTML = '<p class="placeholder">暂无告警</p>';return;}alertsToRender.forEach(alert => {const entry = document.createElement('div');entry.classList.add('alert-entry', alert.level);entry.innerHTML = `<span class="alert-time">${alert.timestamp.toLocaleTimeString()}</span><strong>[${alert.ruleId || 'N/A'}]</strong> ${alert.description}<small>(${alert.srcIp} -> ${alert.dstIp}, ${alert.protocol})</small>`;// Optional: Add click to highlight related packetentry.addEventListener('click', () => {handleTrafficClick(alert.packetId, true); // Find and highlight packet});alertsLogContainer.appendChild(entry);});}function updateHeaderStatus() {processedPacketsCountElement.textContent = processedPackets;activeAlertsCountElement.textContent = activeAlerts;}function updateSystemStatusBasedOnAlerts() {const criticalAlerts = allAlerts.some(a => a.level === ALERT_LEVELS.CRITICAL);const warningAlerts = allAlerts.some(a => a.level === ALERT_LEVELS.WARNING);if (criticalAlerts) {systemStatusElement.textContent = '严重告警';systemStatusElement.className = 'status-critical';} else if (warningAlerts) {systemStatusElement.textContent = '警告';systemStatusElement.className = 'status-warning';} else {systemStatusElement.textContent = '正常';systemStatusElement.className = 'status-ok';}}function updateSimulationTime() {const totalSeconds = simulationTicks * (BASE_SIMULATION_INTERVAL_MS / 1000);const hours = Math.floor(totalSeconds / 3600);const minutes = Math.floor((totalSeconds % 3600) / 60);const seconds = Math.floor(totalSeconds % 60);simulationTimeElement.textContent = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;}// --- Event Handlers ---function handleStartSimulation() {if (simulationRunning) return;simulationRunning = true;startSimBtn.disabled = true;stopSimBtn.disabled = false;const interval = BASE_SIMULATION_INTERVAL_MS / simulationSpeed;simulationIntervalId = setInterval(runSimulationStep, interval);console.log(`Simulation started with speed x${simulationSpeed}, interval ${interval}ms`);}function handleStopSimulation() {if (!simulationRunning) return;simulationRunning = false;clearInterval(simulationIntervalId);simulationIntervalId = null;startSimBtn.disabled = false;stopSimBtn.disabled = true;console.log('Simulation stopped.');}function handleSpeedChange(event) {simulationSpeed = parseInt(event.target.value, 10);speedValueElement.textContent = `x${simulationSpeed}`;if (simulationRunning) {// Adjust interval if runningclearInterval(simulationIntervalId);const interval = BASE_SIMULATION_INTERVAL_MS / simulationSpeed;simulationIntervalId = setInterval(runSimulationStep, interval);console.log(`Simulation speed changed to x${simulationSpeed}, interval ${interval}ms`);}}function handleTrafficFilterChange() {filterAndRenderTraffic();}function handleClearAlerts() {allAlerts = [];activeAlerts = 0;renderAlerts();updateHeaderStatus();updateSystemStatusBasedOnAlerts();console.log('Alerts cleared.');}function handleAssetClick(assetId) {console.log(`Asset clicked: ${assetId}`);renderAssetDetails(assetId);}function handleTrafficClick(packetId, scrollIntoView = false) {console.log(`Traffic clicked: ${packetId}`);selectedTrafficEntry = allTraffic.find(p => p.id === packetId);filterAndRenderTraffic(); // Re-render to show selectionrenderProtocolAnalysis(packetId);if (scrollIntoView) {const entryElement = liveTrafficLogContainer.querySelector(`[data-packet-id="${packetId}"]`);if (entryElement) {entryElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });}}}// --- Initialization ---function initializeApp() {console.log('Initializing application...');defineNetwork();renderTopology();renderAssetDetails(null); // Show placeholder initiallyfilterAndRenderTraffic();renderAlerts();updateHeaderStatus();updateSystemStatusBasedOnAlerts();updateSimulationTime();speedValueElement.textContent = `x${simulationSpeed}`;speedSlider.value = simulationSpeed;// Add event listenersstartSimBtn.addEventListener('click', handleStartSimulation);stopSimBtn.addEventListener('click', handleStopSimulation);speedSlider.addEventListener('input', handleSpeedChange);trafficFilterElement.addEventListener('change', handleTrafficFilterChange);clearAlertsBtn.addEventListener('click', handleClearAlerts);console.log('Network Anomaly Detection Component Ready.');}initializeApp();
});