29、工业网络威胁检测与响应 (IDS 模拟) - /安全与维护组件/industrial-network-ids
76个工业组件库示例汇总
工业网络威胁检测与响应 (IDS) - 自定义组件
概述
这是一个用于模拟工业控制系统 (ICS) / 操作技术 (OT) 网络中威胁检测与响应流程的组件。它通过可视化简化的网络拓扑、模拟实时事件流、高亮潜在威胁并提供响应选项,帮助用户理解工业网络安全监控的基本概念。
功能特性
- 网络拓扑可视化:
- 展示一个简化的 OT 网络图,包含 PLC、HMI、工程师站、历史数据库、交换机和防火墙等典型设备。
- 实时根据模拟事件更新网络设备的状态(正常、警告、受攻击、隔离、阻止),并使用不同的颜色和动画进行视觉提示。
- 事件模拟与检测:
- 定时生成混合的网络事件流,包括模拟的正常工控协议通信(如 Modbus/TCP, OPC)、正常操作(如程序下载)以及各种可疑或恶意活动(如端口扫描、异常协议、非法控制指令、可疑数据传输、登录尝试失败等)。
- 基于简单的规则将事件分类为信息(Info)、警告(Warning)或警报(Alert)。
- 实时事件日志:
- 滚动显示生成的网络事件日志,包含时间戳、严重性、事件描述。
- 根据事件严重性对日志条目进行着色。
- 提供日志过滤器(所有、信息、警告、警报)以便于查看。
- 威胁分析与详情:
- 当日志中的"警报"条目被点击时,在专门的面板中显示该警报的详细信息,包括:检测到的威胁类型、时间、严重性、模拟的来源/目标IP和设备、相关协议/动作、触发的检测规则ID、可能的安全影响以及系统建议的响应操作。
- 模拟响应操作:
- 当选中一个警报时,启用响应操作按钮。
- 提供模拟的响应功能按钮:
- 隔离设备:将被攻击的目标设备标记为"已隔离"状态(可视化)。
- 阻止 IP:将可疑来源 IP 标记为"已阻止"(可视化,如果来源是已知内部设备)。
- 分析流量:模拟启动深度流量分析的过程(仅记录日志)。
- 确认警报:将选中的警报从活动警报列表中移除,并可能将相关设备状态恢复为"正常"(如果无其他警报影响)。
- 所有响应操作都会被记录到事件日志中。
- 全局状态监控:
- 在头部显示当前网络的整体安全状态(正常、警告、高危)和活动(未确认)警报的数量。
- Appsmith 兼容性:
- 采用健壮的初始化逻辑和
querySelector
进行 DOM 操作,确保在 Appsmith 环境中稳定运行。 - 包含错误处理和用户友好的错误消息提示。
- 采用健壮的初始化逻辑和
文件结构
/安全与维护组件
└── /industrial-network-ids├── index.html # 组件的 HTML 结构├── styles.css # 组件的 CSS 样式 (苹果科技工业风格)├── script.js # 组件的核心 JavaScript 逻辑└── README.md # 本说明文件
使用方法
- 将
index.html
的内容复制到 HTML 编辑器中。 - 将
styles.css
的内容复制到 CSS 编辑器中。 - 将
script.js
的内容复制到 JavaScript 编辑器中。 - 调整组件大小以适应内容展示,建议宽度较宽,高度适中(例如 600px)。
交互流程:
- 组件加载后自动开始模拟事件生成。
- 观察网络拓扑图中设备状态的变化(正常、警告闪烁、警报闪烁)。
- 查看实时事件日志面板中滚动的事件流。
- 使用日志过滤器筛选感兴趣的事件。
- 当出现"警报"级别的日志时,点击该条目。
- 威胁分析面板将显示该警报的详细信息。
- 响应操作面板的按钮将被启用。
- 点击相应的响应按钮(如"隔离设备"或"阻止 IP"),观察网络拓扑图和事件日志中的变化。
- 点击"确认警报"将清除该警报,并可能恢复设备状态。
技术栈
- HTML5
- CSS3 (Flexbox, Grid, Keyframes for animations)
- JavaScript (ES6+)
- Font Awesome (用于图标)
模拟逻辑说明
- 事件生成: 按固定时间间隔随机生成事件,大部分为预定义的正常流量,小部分为预定义的异常/可疑事件。
- 状态更新: 当生成"警报"或"警告"事件时,会更新涉及的源/目标设备在
state.deviceStatus
中的状态。renderNetworkTopology
函数根据此状态更新设备元素的 CSS 类。 - 警报管理: "警报"事件会添加到
state.activeAlerts
对象中。点击日志中的警报条目会更新state.selectedAlertId
。 - 响应模拟: 点击响应按钮会触发相应的处理函数,这些函数主要修改
state.deviceStatus
或从state.activeAlerts
中移除条目,并记录一个"信息"级别的操作日志。实际的网络操作(如防火墙规则修改)并未实现。
注意事项与限制
- 这是一个 高度简化 的模拟器,旨在演示概念,并非一个功能完整的工业 IDS/IPS 系统。
- 网络拓扑是静态的,在
index.html
中定义。 - 事件检测逻辑基于预定义的简单规则,没有真正的流量分析或机器学习能力。
- 响应操作仅在界面上模拟效果(改变状态、记录日志),不会执行任何实际的网络控制。
- 事件生成的随机性和频率可能与真实网络流量有较大差异。
效果展示
源码
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>工业网络威胁检测与响应 (IDS)</title><link rel="stylesheet" href="styles.css"><!-- Font Awesome for Icons --><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</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>工业网络威胁检测与响应</h1></div><div class="global-status"><span>网络状态: <strong id="network-status" class="status-normal">正常</strong></span><span>活动警报: <strong id="active-alerts-count">0</strong></span></div></header><!-- Main Content Grid --><main class="app-content"><!-- Network Topology Panel --><section class="panel network-panel"><div class="panel-header"><h2><i class="fas fa-project-diagram"></i> 网络拓扑与状态</h2></div><div id="network-topology" class="panel-content network-map"><!-- Network devices will be rendered here by JS --><div class="network-device plc" id="dev-plc-01" data-device-id="PLC-01" title="PLC-01 (生产线A)"><i class="fas fa-memory"></i><span>PLC-01</span></div><div class="network-device hmi" id="dev-hmi-01" data-device-id="HMI-01" title="HMI-01 (操作站A)"><i class="fas fa-desktop"></i><span>HMI-01</span></div><div class="network-device eng-ws" id="dev-eng-ws" data-device-id="ENG-WS" title="工程师站"><i class="fas fa-laptop-code"></i><span>ENG-WS</span></div><div class="network-device historian" id="dev-historian" data-device-id="HIST" title="历史数据库"><i class="fas fa-database"></i><span>HIST</span></div><div class="network-device switch" id="dev-switch-01" data-device-id="SW-01" title="OT 交换机"><i class="fas fa-network-wired"></i><span>SW-01</span></div><div class="network-device firewall" id="dev-firewall" data-device-id="FW-01" title="IT/OT 防火墙"><i class="fas fa-fire-alt"></i><span>FW-01</span></div><!-- Connections will be added/styled via CSS/JS --><div class="network-connection" data-from="dev-plc-01" data-to="dev-switch-01"></div><div class="network-connection" data-from="dev-hmi-01" data-to="dev-switch-01"></div><div class="network-connection" data-from="dev-eng-ws" data-to="dev-switch-01"></div><div class="network-connection" data-from="dev-historian" data-to="dev-switch-01"></div><div class="network-connection" data-from="dev-switch-01" data-to="dev-firewall"></div></div></section><!-- Event Log Panel --><section class="panel event-log-panel"><div class="panel-header"><h2><i class="fas fa-stream"></i> 实时事件日志</h2><select id="log-filter" class="header-filter"><option value="all">所有事件</option><option value="info">信息</option><option value="warning">警告</option><option value="alert">警报</option></select></div><div id="event-log" class="panel-content log-content scrollable"><!-- Log entries will be added here by JS --><p class="placeholder">等待网络事件...</p></div></section><!-- Threat Analysis Panel --><section class="panel analysis-panel"><div class="panel-header"><h2><i class="fas fa-search"></i> 威胁分析与详情</h2></div><div id="threat-details" class="panel-content"><p class="placeholder">选中警报或等待高危事件...</p><!-- Threat details will be loaded here --><!-- Example Structure:<div class="threat-info"><h4>警报: 非法 Modbus 功能码</h4><p><strong>时间:</strong> <span class="threat-time">...</span></p><p><strong>严重性:</strong> <span class="threat-severity high">高</span></p><p><strong>来源 IP:</strong> <span class="threat-source">192.168.1.150</span></p><p><strong>目标设备:</strong> <span class="threat-target">PLC-01 (192.168.1.10)</span></p><p><strong>检测规则:</strong> Modbus Anomaly Detection Rule #3</p><p><strong>可能影响:</strong> 设备被控、生产中断</p><p><strong>建议操作:</strong> 立即隔离来源IP,检查PLC程序</p></div>--></div></section><!-- Response Actions Panel --><section class="panel response-panel"><div class="panel-header"><h2><i class="fas fa-tasks"></i> 响应操作</h2></div><div id="response-actions" class="panel-content response-buttons"><button id="resp-isolate" class="btn btn-warning" disabled><i class="fas fa-plug"></i> 隔离设备</button><button id="resp-block" class="btn btn-danger" disabled><i class="fas fa-ban"></i> 阻止 IP</button><button id="resp-analyze" class="btn btn-info" disabled><i class="fas fa-microscope"></i> 分析流量</button><button id="resp-acknowledge" class="btn btn-secondary" disabled><i class="fas fa-check-circle"></i> 确认警报</button><p class="placeholder">请先选择警报或威胁</p></div></section></main></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: #007aff; /* Apple Blue for info */--alert-high-color: #ff3b30;--alert-medium-color: #ff9500;--alert-low-color: #ffcc00; /* Apple Yellow */--log-info-bg: #eef6ff;--log-warning-bg: #fff8e6;--log-alert-bg: #ffebee;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--border-radius: 8px;--panel-padding: 15px;--panel-header-height: 45px;--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; /* Slightly smaller base font */overflow-x: hidden;
}.app-container {display: flex;flex-direction: column;height: 100vh; /* Fallback */height: 100dvh;max-height: 600px; /* Limit 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);
}.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;
}.global-status {display: flex;align-items: center;gap: 20px;font-size: 0.9em;
}.global-status strong {font-weight: 600;padding: 3px 6px;border-radius: 4px;color: #fff;
}.global-status .status-normal {background-color: var(--success-color);
}
.global-status .status-warning {background-color: var(--warning-color);
}
.global-status .status-alert {background-color: var(--error-color);animation: pulse 1s infinite;
}@keyframes pulse {0% { box-shadow: 0 0 0 0 rgba(255, 59, 48, 0.7); }70% { box-shadow: 0 0 0 8px rgba(255, 59, 48, 0); }100% { box-shadow: 0 0 0 0 rgba(255, 59, 48, 0); }
}/* Main Content Grid */
.app-content {display: grid;grid-template-columns: 1.5fr 2fr; /* Network | Log */grid-template-rows: 2fr 1fr; /* Top Row | Bottom Row */grid-template-areas:"network eventlog""analysis response";gap: var(--panel-padding);padding: var(--panel-padding);flex-grow: 1;overflow: hidden;
}/* 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;
}.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 {padding: var(--panel-padding);flex-grow: 1;overflow-y: auto;
}.scrollable {overflow-y: auto;
}/* Specific Panel Assignments */
.network-panel { grid-area: network; }
.event-log-panel { grid-area: eventlog; }
.analysis-panel { grid-area: analysis; }
.response-panel { grid-area: response; }/* Network Topology Styles */
.network-map {position: relative;background-color: #fafafa;padding: 20px;display: grid; /* Use grid for positioning, adjust as needed */grid-template-columns: repeat(3, 1fr);grid-template-rows: repeat(2, 1fr);gap: 40px;align-items: center; /* Center items vertically */justify-items: center; /* Center items horizontally */overflow: hidden; /* Hide connections outside */
}.network-device {display: flex;flex-direction: column;align-items: center;text-align: center;cursor: pointer;transition: transform var(--animation-duration) ease;padding: 10px;border: 2px solid transparent;border-radius: var(--border-radius);
}.network-device:hover {transform: scale(1.05);background-color: rgba(0, 122, 255, 0.05);
}.network-device i {font-size: 2.2em;margin-bottom: 5px;color: var(--text-color-secondary);
}.network-device span {font-size: 0.8em;font-weight: 500;
}/* Device specific colors/icons */
.network-device.plc i { color: #34c759; } /* Green */
.network-device.hmi i { color: #5856d6; } /* Purple */
.network-device.eng-ws i { color: #007aff; } /* Blue */
.network-device.historian i { color: #ff9500; } /* Orange */
.network-device.switch i { color: #8e8e93; } /* Gray */
.network-device.firewall i { color: #ff3b30; } /* Red *//* Device Status Styles */
.network-device.status-warning {border-color: var(--warning-color);animation: pulse-warning 1.5s infinite;
}.network-device.status-alert {border-color: var(--error-color);background-color: rgba(255, 59, 48, 0.1);animation: pulse-alert 1s infinite;
}@keyframes pulse-warning {0% { box-shadow: 0 0 0 0 rgba(255, 149, 0, 0.5); }70% { box-shadow: 0 0 0 6px rgba(255, 149, 0, 0); }100% { box-shadow: 0 0 0 0 rgba(255, 149, 0, 0); }
}
@keyframes pulse-alert {0% { box-shadow: 0 0 0 0 rgba(255, 59, 48, 0.7); }70% { box-shadow: 0 0 0 6px rgba(255, 59, 48, 0); }100% { box-shadow: 0 0 0 0 rgba(255, 59, 48, 0); }
}/* Connections (Basic placeholders, JS might draw lines later) */
.network-connection {display: none; /* Hide placeholder divs */
}/* Event Log Styles */
.log-content {font-family: monospace;font-size: 0.85em;line-height: 1.6;padding: 10px;
}.log-entry {padding: 4px 8px;margin-bottom: 4px;border-radius: 4px;display: flex;justify-content: space-between;flex-wrap: wrap;border: 1px solid transparent;transition: background-color var(--animation-duration) ease;
}.log-entry.selected {background-color: rgba(0, 122, 255, 0.1);border-color: var(--primary-color);
}.log-entry span {margin-right: 10px;white-space: nowrap;
}.log-timestamp {color: var(--text-color-secondary);font-weight: 500;
}.log-severity {font-weight: bold;text-transform: uppercase;min-width: 60px;text-align: center;
}.log-entry.info { background-color: var(--log-info-bg); }
.log-entry.info .log-severity { color: var(--info-color); }.log-entry.warning { background-color: var(--log-warning-bg); }
.log-entry.warning .log-severity { color: var(--warning-color); }.log-entry.alert { background-color: var(--log-alert-bg); cursor: pointer; }
.log-entry.alert .log-severity { color: var(--error-color); }.log-message {flex-grow: 1;white-space: normal;
}.header-filter {padding: 4px 8px;border: 1px solid var(--border-color);border-radius: 6px;font-size: 0.9em;background-color: #fff;
}/* Threat Analysis Styles */
.analysis-panel .panel-content {padding-top: 10px;
}.threat-info h4 {font-size: 1.1em;font-weight: 600;margin-bottom: 10px;color: var(--error-color);
}.threat-info p {margin-bottom: 6px;font-size: 0.9em;line-height: 1.4;
}.threat-info strong {font-weight: 600;color: var(--text-color);margin-right: 5px;
}.threat-severity.high { color: var(--alert-high-color); font-weight: bold; }
.threat-severity.medium { color: var(--alert-medium-color); font-weight: bold; }
.threat-severity.low { color: var(--alert-low-color); font-weight: bold; }/* Response Actions Styles */
.response-buttons {display: flex;flex-wrap: wrap;gap: 10px;align-items: flex-start;
}.response-buttons .btn {flex-grow: 1; /* Allow buttons to grow */min-width: 100px; /* Ensure minimum width */
}.response-buttons .btn-warning { background-color: var(--warning-color); color: #fff; }
.response-buttons .btn-warning:hover { background-color: #d97e00; }
.response-buttons .btn-danger { background-color: var(--error-color); color: #fff; }
.response-buttons .btn-danger:hover { background-color: #d13026; }
.response-buttons .btn-info { background-color: var(--info-color); color: #fff; }
.response-buttons .btn-info:hover { background-color: #005ec4; }
.response-buttons .btn-secondary { background-color: var(--secondary-color); color: #fff; }
.response-buttons .btn-secondary:hover { background-color: #6c6c70; }.response-buttons .btn:disabled {background-color: #e0e0e0;cursor: not-allowed;color: #a0a0a0;
}.response-buttons .placeholder {width: 100%;text-align: center;margin-top: 10px;
}/* Placeholder */
.placeholder {text-align: center;color: var(--text-color-secondary);font-style: italic;padding: 20px;width: 100%;
}/* Responsive Adjustments */
@media (max-width: 900px) {.app-content {grid-template-columns: 1fr; /* Stack all panels */grid-template-rows: auto auto auto auto; /* One row per panel */grid-template-areas:"network""eventlog""analysis""response";overflow-y: auto;}.app-container {max-height: none;margin: 0;border-radius: 0;}.panel-content {min-height: 150px;}.network-map {grid-template-columns: repeat(2, 1fr); /* Adjust grid for smaller screens */padding: 15px;gap: 20px;}.response-buttons {flex-direction: column; /* Stack buttons vertically */}.response-buttons .btn {width: 100%;}
}
script.js
/* =============================================================================Industrial Network Intrusion Detection System (IDS) - Script============================================================================= */// --- Configuration & Constants ---
const EVENT_INTERVAL_MS = 1500; // Interval for generating new events
const MAX_LOG_ENTRIES = 100; // Max number of log entries to keep// --- Simulated Network Data ---
const DEVICES = {"PLC-01": { id: "PLC-01", name: "PLC-01 (生产线A)", type: "plc", ip: "192.168.1.10", status: "normal" },"HMI-01": { id: "HMI-01", name: "HMI-01 (操作站A)", type: "hmi", ip: "192.168.1.20", status: "normal" },"ENG-WS": { id: "ENG-WS", name: "工程师站", type: "eng-ws", ip: "192.168.1.50", status: "normal" },"HIST": { id: "HIST", name: "历史数据库", type: "historian", ip: "192.168.1.30", status: "normal" },"SW-01": { id: "SW-01", name: "OT 交换机", type: "switch", ip: "192.168.1.1", status: "normal" },"FW-01": { id: "FW-01", name: "IT/OT 防火墙", type: "firewall", ip: "192.168.1.254", status: "normal" },"UNKNOWN":{ id: "UNKNOWN", name: "未知设备", type: "unknown", ip: "10.10.10.100", status: "normal" } // Represent external/unknown source
};const NORMAL_TRAFFIC = [{ src: "PLC-01", dst: "HMI-01", proto: "Modbus/TCP", action: "Read Coils", freq: 0.4 },{ src: "HMI-01", dst: "PLC-01", proto: "Modbus/TCP", action: "Write Coil", freq: 0.1 },{ src: "PLC-01", dst: "HIST", proto: "OPC DA", action: "Data Update", freq: 0.2 },{ src: "ENG-WS", dst: "PLC-01", proto: "Vendor/P", action: "Program Download", freq: 0.05 },{ src: "ENG-WS", dst: "PLC-01", proto: "Vendor/P", action: "Status Check", freq: 0.1 },{ src: "HMI-01", dst: "HIST", proto: "SQL", action: "Query Data", freq: 0.05 },
];const SUSPICIOUS_EVENTS = [{ type: "Port Scan", src: "UNKNOWN", dst: "PLC-01", severity: "warning", rule: "IDS-SCAN-01", impact: "侦察活动", suggestion: "监控来源IP,检查防火墙规则" },{ type: "Unknown Protocol", src: "ENG-WS", dst: "HMI-01", proto: "XYZ/123", severity: "warning", rule: "IDS-PROTO-03", impact: "可能使用非标或恶意软件", suggestion: "检查工程师站进程" },{ type: "Excessive Login Attempts", src: "UNKNOWN", dst: "ENG-WS", proto: "SSH", severity: "alert", rule: "IDS-AUTH-05", impact: "暴力破解尝试", suggestion: "阻止来源IP,检查账户安全" },{ type: "Anomalous Modbus Function", src: "HMI-01", dst: "PLC-01", proto: "Modbus/TCP", action: "Func Code 99", severity: "alert", rule: "IDS-MODBUS-02", impact: "非法设备控制尝试", suggestion: "检查HMI活动,分析流量" },{ type: "Large Data Transfer", src: "PLC-01", dst: "UNKNOWN", proto: "FTP", severity: "warning", rule: "IDS-DATA-01", impact: "可能数据泄露", suggestion: "确认是否有授权传输,检查防火墙出口规则" },{ type: "Firewall Policy Violation", src: "10.10.10.100", dst: "192.168.1.30", proto: "SMB", severity: "warning", rule: "FW-BLOCK-07", impact: "尝试访问禁止服务", suggestion: "确认防火墙日志" },{ type: "PLC Stop Command", src: "UNKNOWN", dst: "PLC-01", proto: "Vendor/P", action: "STOP CPU", severity: "alert", rule: "IDS-ICS-01", impact: "生产中断风险", suggestion: "立即隔离来源IP,检查PLC状态" },
];// --- Global State ---
let state = {logEntries: [], // { id, timestamp, severity, message, details? }deviceStatus: { ...DEVICES }, // Current status of each deviceactiveAlerts: {}, // { alertId: logEntry }selectedAlertId: null,simulationTimer: null,logFilter: 'all',networkStatus: 'normal', // normal, warning, alertactiveAlertCount: 0,
};// --- DOM Elements Cache ---
const elements = {};// =============================================================================
// Initialization (Appsmith Optimized)
// =============================================================================let initializationAttempts = 0;
const MAX_INITIALIZATION_ATTEMPTS = 10;
const INITIALIZATION_RETRY_DELAY = 500;function attemptInitialization() {console.log(`Attempting to initialize IDS app (Attempt ${initializationAttempts + 1}/${MAX_INITIALIZATION_ATTEMPTS})`);if (document.readyState === 'loading') {console.log("Document still loading, waiting for DOMContentLoaded.");document.addEventListener('DOMContentLoaded', () => setTimeout(attemptInitialization, 100));return;}const appContainer = document.querySelector('.app-container');if (!appContainer) {initializationAttempts++;if (initializationAttempts < MAX_INITIALIZATION_ATTEMPTS) {console.log(`App container not found, retrying in ${INITIALIZATION_RETRY_DELAY}ms...`);setTimeout(attemptInitialization, INITIALIZATION_RETRY_DELAY);} else {console.error(`Failed to find app container after ${MAX_INITIALIZATION_ATTEMPTS} attempts.`);showErrorMessage("应用容器加载失败,请刷新重试。");}return;}console.log("App container found, proceeding with initialization.");initializeApp(appContainer);
}function initializeApp(container) {console.log("Initializing IDS App...");try {if (!queryDOMElements(container)) {console.error("Initialization failed: Missing essential DOM elements.");showErrorMessage("界面元素加载不完整,请刷新重试。");return;}setupEventListeners();renderNetworkTopology(); // Initial renderrenderEventLog(); // Clear log initiallyrenderThreatDetails(); // Show placeholderrenderResponseActions(); // Disable buttonsupdateGlobalStatus();startSimulation(); // Start generating eventsconsole.log("IDS App Initialized and Simulation Started.");} catch (error) {console.error("Error during app initialization:", error);showErrorMessage(`初始化出错: ${error.message}`);}
}function queryDOMElements(container) {console.log("Querying DOM elements...");const ids = ['network-status','active-alerts-count','network-topology','event-log','log-filter','threat-details','response-actions','resp-isolate','resp-block','resp-analyze','resp-acknowledge'];let allFound = true;elements.container = container; // Store container referenceids.forEach(id => {elements[id] = container.querySelector(`#${id}`);if (!elements[id]) {console.warn(`DOM element not found: #${id}`);// Add fallbacks if specific elements are hard to findif (!elements[id]) {allFound = false;}}});// Query network devices separately if needed (though static for now)elements.networkDevices = {};container.querySelectorAll('.network-device').forEach(el => {const deviceId = el.dataset.deviceId;if (deviceId) {elements.networkDevices[deviceId] = el;} else {console.warn("Network device found without data-device-id:", el);}});if (Object.keys(elements.networkDevices).length === 0) {console.error("No network devices found in the topology map!");allFound = false;}if (!allFound) {console.error("关键 DOM 元素缺失。 Missing elements logged above.");return false;}console.log("DOM elements queried successfully.");return true;
}function setupEventListeners() {console.log("Setting up event listeners...");try {// Log filterif (elements['log-filter']) {elements['log-filter'].addEventListener('change', handleLogFilterChange);} else console.error("log-filter not found");// Response buttonsif (elements['resp-isolate']) {elements['resp-isolate'].addEventListener('click', () => handleResponseAction('isolate'));} else console.error("resp-isolate button not found");if (elements['resp-block']) {elements['resp-block'].addEventListener('click', () => handleResponseAction('block'));} else console.error("resp-block button not found");if (elements['resp-analyze']) {elements['resp-analyze'].addEventListener('click', () => handleResponseAction('analyze'));} else console.error("resp-analyze button not found");if (elements['resp-acknowledge']) {elements['resp-acknowledge'].addEventListener('click', () => handleResponseAction('acknowledge'));} else console.error("resp-acknowledge button not found");// Click on log entries (delegated to log container)if (elements['event-log']) {elements['event-log'].addEventListener('click', handleLogEntryClick);} else console.error("event-log container not found for click delegation");console.log("Event listeners set up.");} catch (error) {console.error("Error setting up event listeners:", error);}
}// =============================================================================
// Simulation Control & Event Generation
// =============================================================================function startSimulation() {if (state.simulationTimer) clearInterval(state.simulationTimer);state.simulationTimer = setInterval(generateEvent, EVENT_INTERVAL_MS);console.log(`Simulation started, generating events every ${EVENT_INTERVAL_MS}ms`);
}function stopSimulation() {if (state.simulationTimer) clearInterval(state.simulationTimer);state.simulationTimer = null;console.log("Simulation stopped.");
}function generateEvent() {try {let eventData;let severity = 'info';let message = '';let details = {};const randomChoice = Math.random();if (randomChoice < 0.75) { // 75% chance of normal trafficconst traffic = NORMAL_TRAFFIC[Math.floor(Math.random() * NORMAL_TRAFFIC.length)];if (Math.random() < traffic.freq * 5) { // Adjust frequency multipliermessage = `正常流量: ${traffic.src} -> ${traffic.dst} (${traffic.proto} ${traffic.action})`;details = { src: traffic.src, dst: traffic.dst, proto: traffic.proto, action: traffic.action };severity = 'info';// Occasionally make normal traffic look slightly suspiciousif (Math.random() < 0.02) {message += " (流量略高)";severity = 'info'; // Keep as info for now}} else {return; // Skip event generation if frequency check fails}} else { // 25% chance of suspicious eventconst threat = SUSPICIOUS_EVENTS[Math.floor(Math.random() * SUSPICIOUS_EVENTS.length)];const srcDevice = threat.src === 'UNKNOWN' ? DEVICES.UNKNOWN : state.deviceStatus[threat.src];const dstDevice = state.deviceStatus[threat.dst];if (!srcDevice || !dstDevice) {console.warn("Skipping threat generation due to missing device:", threat);return;}message = `检测到 ${threat.type}: ${srcDevice.name} (${srcDevice.ip}) -> ${dstDevice.name} (${dstDevice.ip})`;if (threat.action) message += ` (${threat.action})`;severity = threat.severity;details = { ...threat, srcIp: srcDevice.ip, dstIp: dstDevice.ip, srcId: srcDevice.id, dstId: dstDevice.id };// Trigger device status change for alertsif (severity === 'alert') {updateDeviceStatus(dstDevice.id, 'alert');if(srcDevice.id !== 'UNKNOWN') updateDeviceStatus(srcDevice.id, 'warning'); // Mark source as potentially compromisedaddActiveAlert(details);} else if (severity === 'warning') {if(dstDevice.status === 'normal') updateDeviceStatus(dstDevice.id, 'warning');if(srcDevice.id !== 'UNKNOWN' && srcDevice.status === 'normal') updateDeviceStatus(srcDevice.id, 'warning');}}addLogEntry(severity, message, details);} catch (error) {console.error("Error generating event:", error);}
}function addLogEntry(severity, message, details = {}) {const timestamp = new Date();const entryId = `log-${timestamp.getTime()}-${Math.random().toString(16).slice(2)}`;const newEntry = {id: entryId,timestamp,severity,message,details};state.logEntries.unshift(newEntry); // Add to the beginning// Limit log sizeif (state.logEntries.length > MAX_LOG_ENTRIES) {state.logEntries.pop(); // Remove the oldest entry}renderEventLog(); // Update the UI// If it's a new alert, maybe automatically select it?if (severity === 'alert' && !state.selectedAlertId) {// selectAlert(entryId);}
}function addActiveAlert(alertDetails) {const alertId = alertDetails.rule + '-' + Date.now(); // Simple unique IDstate.activeAlerts[alertId] = {id: alertId,timestamp: new Date(),severity: alertDetails.severity || 'alert',message: `警报: ${alertDetails.type} from ${alertDetails.srcIp} to ${alertDetails.dstIp}`,details: alertDetails};state.activeAlertCount = Object.keys(state.activeAlerts).length;updateGlobalStatus();
}function removeActiveAlert(alertId) {if (state.activeAlerts[alertId]) {delete state.activeAlerts[alertId];state.activeAlertCount = Object.keys(state.activeAlerts).length;// If the removed alert was selected, clear selectionif (state.selectedAlertId === alertId) {state.selectedAlertId = null;renderThreatDetails();renderResponseActions();}updateGlobalStatus();}
}// =============================================================================
// Event Handlers
// =============================================================================function handleLogFilterChange(event) {state.logFilter = event.target.value;console.log(`Log filter changed to: ${state.logFilter}`);renderEventLog(); // Re-render the log with the filter applied
}function handleResponseAction(actionType) {console.log(`Response action clicked: ${actionType}`);if (!state.selectedAlertId || !state.activeAlerts[state.selectedAlertId]) {console.warn("No active alert selected for response action.");return;}const alert = state.activeAlerts[state.selectedAlertId];const details = alert.details;let message = `模拟响应 [${actionType.toUpperCase()}] 针对警报 ${alert.id}:`;switch (actionType) {case 'isolate':message += ` 尝试隔离设备 ${details.dstId} (${details.dstIp})`;if (state.deviceStatus[details.dstId]) {updateDeviceStatus(details.dstId, 'isolated'); // Mark as isolated}// In a real system, this would trigger firewall/switch ACL changesbreak;case 'block':message += ` 尝试阻止来源 IP ${details.srcIp}`;// In a real system, this would update firewall rulesif (details.srcId !== 'UNKNOWN' && state.deviceStatus[details.srcId]) {updateDeviceStatus(details.srcId, 'blocked'); // Mark source as blocked (visual only)}break;case 'analyze':message += ` 启动对 ${details.srcIp} <-> ${details.dstIp} 流量的深度分析...`;// Could trigger a placeholder loading state or mock analysis resultbreak;case 'acknowledge':message += ` 确认警报 ${alert.id}。`;removeActiveAlert(state.selectedAlertId); // Remove from active alerts// Reset device status if no other alerts affect it?// Simple reset for demo:if (details.dstId && state.deviceStatus[details.dstId]?.status !== 'normal') {updateDeviceStatus(details.dstId, 'normal');}if (details.srcId && details.srcId !== 'UNKNOWN' && state.deviceStatus[details.srcId]?.status !== 'normal') {updateDeviceStatus(details.srcId, 'normal');}break;default:console.warn(`Unknown response action: ${actionType}`);return;}addLogEntry('info', message); // Log the simulated action// Disable buttons after action?renderResponseActions(); // Re-render to potentially disable
}function handleLogEntryClick(event) {const logEntryElement = event.target.closest('.log-entry.alert'); // Only allow selecting alertsif (logEntryElement && logEntryElement.dataset.entryId) {const entryId = logEntryElement.dataset.entryId;console.log(`Log entry clicked: ${entryId}`);selectAlert(entryId);}
}function selectAlert(logEntryId) {// Find the corresponding active alert if possibleconst logEntry = state.logEntries.find(e => e.id === logEntryId);if (!logEntry || logEntry.severity !== 'alert') {console.warn("Clicked log entry is not a selectable alert.");// If an alert was previously selected, deselect it visuallyif (state.selectedAlertId) {const previouslySelected = elements['event-log']?.querySelector(`.log-entry[data-entry-id="${state.selectedAlertId}"]`);previouslySelected?.classList.remove('selected');}state.selectedAlertId = null;renderThreatDetails();renderResponseActions();return;}// Find the active alert associated with this log entry (match based on details if needed)// Simple approach: Assume log entry ID can map to an active alert (needs refinement)// For demo, let's find the *latest* active alert matching the rule type if log ID doesn't directly maplet foundAlertId = null;if (logEntry.details?.rule) {const matchingAlerts = Object.values(state.activeAlerts).filter(a => a.details.rule === logEntry.details.rule);if (matchingAlerts.length > 0) {// Select the most recent onematchingAlerts.sort((a, b) => b.timestamp - a.timestamp);foundAlertId = matchingAlerts[0].id;}}if (!foundAlertId) {console.warn(`Could not find an active alert corresponding to log entry ${logEntryId}`);if (state.selectedAlertId) {const previouslySelected = elements['event-log']?.querySelector(`.log-entry[data-entry-id="${state.selectedAlertId}"]`);previouslySelected?.classList.remove('selected');}state.selectedAlertId = null; // Clear selection if no active alert found} else {console.log(`Selected active alert: ${foundAlertId}`);// Deselect previously selected log entry visuallyif (state.selectedAlertId) {const previouslySelected = elements['event-log']?.querySelector(`.log-entry[data-entry-id="${state.selectedAlertId}"]`);previouslySelected?.classList.remove('selected');}// Select the new onestate.selectedAlertId = foundAlertId;const currentSelected = elements['event-log']?.querySelector(`.log-entry[data-entry-id="${logEntryId}"]`);currentSelected?.classList.add('selected');}renderThreatDetails();renderResponseActions(); // Update button states based on selection
}// =============================================================================
// UI Rendering
// =============================================================================function renderNetworkTopology() {// console.log("Rendering network topology status...");try {for (const deviceId in state.deviceStatus) {const device = state.deviceStatus[deviceId];const element = elements.networkDevices[deviceId];if (element) {// Remove previous status classeselement.classList.remove('status-normal', 'status-warning', 'status-alert', 'status-isolated', 'status-blocked');// Add current status classelement.classList.add(`status-${device.status}`);element.title = `${device.name} (${device.ip}) - Status: ${device.status}`; // Update tooltip} else {// console.warn(`Network device element not found for rendering status: ${deviceId}`);}}} catch (error) {console.error("Error rendering network topology:", error);}
}function renderEventLog() {// console.log("Rendering event log...");try {const container = elements['event-log'];if (!container) {console.error("Event log container not found.");return;}const fragment = document.createDocumentFragment();let entriesRendered = 0;state.logEntries.forEach(entry => {if (state.logFilter === 'all' || state.logFilter === entry.severity) {const entryDiv = document.createElement('div');entryDiv.className = `log-entry ${entry.severity}`;entryDiv.dataset.entryId = entry.id;if (entry.severity === 'alert') {entryDiv.title = "点击查看详情并执行响应";}if (entry.id === state.selectedAlertId) { // Highlight selected log entryentryDiv.classList.add('selected');}entryDiv.innerHTML = `<span class="log-timestamp">${entry.timestamp.toLocaleTimeString()}</span><span class="log-severity">${entry.severity}</span><span class="log-message">${escapeHtml(entry.message)}</span>`;fragment.appendChild(entryDiv);entriesRendered++;}});container.innerHTML = ''; // Clear previous entriesif (entriesRendered > 0) {container.appendChild(fragment);} else {container.innerHTML = `<p class="placeholder">无符合条件的事件 (${state.logFilter})</p>`;}} catch (error) {console.error("Error rendering event log:", error);}
}function renderThreatDetails() {console.log("Rendering threat details...");try {const container = elements['threat-details'];if (!container) {console.error("Threat details container not found.");return;}if (state.selectedAlertId && state.activeAlerts[state.selectedAlertId]) {const alert = state.activeAlerts[state.selectedAlertId];const details = alert.details;const severityClass = details.severity || 'alert'; // Default to alert if missingcontainer.innerHTML = `<div class="threat-info"><h4>警报: ${escapeHtml(details.type || '未知类型')}</h4><p><strong>时间:</strong> <span class="threat-time">${alert.timestamp.toLocaleString()}</span></p><p><strong>严重性:</strong> <span class="threat-severity ${severityClass}">${escapeHtml(details.severity || '未知')}</span></p><p><strong>来源 IP:</strong> <span class="threat-source">${escapeHtml(details.srcIp)} (${escapeHtml(details.srcId === 'UNKNOWN' ? '外部' : details.srcId)})</span></p><p><strong>目标设备:</strong> <span class="threat-target">${escapeHtml(details.dstId)} (${escapeHtml(details.dstIp)})</span></p><p><strong>协议:</strong> <span class="threat-proto">${escapeHtml(details.proto || 'N/A')}</span></p>${details.action ? `<p><strong>动作:</strong> <span class="threat-action">${escapeHtml(details.action)}</span></p>` : ''}<p><strong>检测规则:</strong> <span class="threat-rule">${escapeHtml(details.rule || '未知')}</span></p><p><strong>可能影响:</strong> <span class="threat-impact">${escapeHtml(details.impact || '未知')}</span></p><p><strong>建议操作:</strong> <span class="threat-suggestion">${escapeHtml(details.suggestion || '手动分析')}</span></p></div>`;} else {container.innerHTML = '<p class="placeholder">选中日志中的警报条目以查看详情。</p>';}} catch (error) {console.error("Error rendering threat details:", error);}}function renderResponseActions() {console.log("Rendering response actions...");try {const container = elements['response-actions'];const placeholder = container?.querySelector('.placeholder');const buttons = [elements['resp-isolate'], elements['resp-block'],elements['resp-analyze'], elements['resp-acknowledge']];const alertSelected = state.selectedAlertId && state.activeAlerts[state.selectedAlertId];if (placeholder) {placeholder.style.display = alertSelected ? 'none' : 'block';}buttons.forEach(button => {if (button) {button.disabled = !alertSelected;// More specific logic? e.g., disable isolate if already isolated?if(alertSelected && button.id === 'resp-isolate') {const targetDevice = state.deviceStatus[state.activeAlerts[state.selectedAlertId].details.dstId];if(targetDevice?.status === 'isolated') button.disabled = true;}// Disable acknowledge if already removed?if(alertSelected && button.id === 'resp-acknowledge' && !state.activeAlerts[state.selectedAlertId]){button.disabled = true; // Should not happen if removeActiveAlert works}}});} catch (error) {console.error("Error rendering response actions:", error);}}function updateGlobalStatus() {console.log("Updating global status...");try {// Determine overall network statuslet currentNetworkStatus = 'normal';if (Object.values(state.deviceStatus).some(d => d.status === 'alert' || d.status === 'isolated' || d.status === 'blocked')) {currentNetworkStatus = 'alert';} else if (Object.values(state.deviceStatus).some(d => d.status === 'warning')) {currentNetworkStatus = 'warning';}state.networkStatus = currentNetworkStatus;if (elements['network-status']) {const statusEl = elements['network-status'];statusEl.textContent = state.networkStatus === 'alert' ? '高危' : (state.networkStatus === 'warning' ? '警告' : '正常');statusEl.className = `status-${state.networkStatus}`; // Update class for color/animation} else console.warn("network-status element not found");if (elements['active-alerts-count']) {elements['active-alerts-count'].textContent = state.activeAlertCount;elements['active-alerts-count'].style.backgroundColor = state.activeAlertCount > 0 ? 'var(--error-color)' : 'var(--success-color)';} else console.warn("active-alerts-count element not found");} catch (error) {console.error("Error updating global status:", error);}}function updateDeviceStatus(deviceId, newStatus) {if (state.deviceStatus[deviceId] && state.deviceStatus[deviceId].status !== newStatus) {console.log(`Updating device ${deviceId} status to ${newStatus}`);state.deviceStatus[deviceId].status = newStatus;renderNetworkTopology(); // Re-render the topology to reflect the changeupdateGlobalStatus(); // Update overall status indicator}}// =============================================================================
// Utility Functions
// =============================================================================function escapeHtml(input) {if (input === null || input === undefined) return '';const str = String(input);const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };return str.replace(/[&<>"']/g, (match) => map[match]);
}function showErrorMessage(message) {console.error("Displaying error message to user:", message);try {const container = elements.container || document.body;let errorDiv = container.querySelector('.app-error-message');if (!errorDiv) {errorDiv = document.createElement('div');errorDiv.className = 'app-error-message';errorDiv.style.cssText = `position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);background-color: #f8d7da; color: #721c24; padding: 15px 20px;border-radius: 5px; border: 1px solid #f5c6cb; box-shadow: 0 2px 4px rgba(0,0,0,0.1);max-width: 90%; text-align: center; z-index: 1000;`;// Ensure container allows absolute positioning if it's not bodyif (container !== document.body && getComputedStyle(container).position === 'static') {container.style.position = 'relative';}container.appendChild(errorDiv);}errorDiv.innerHTML = `<h4 style="margin-bottom:8px; color:#721c24;">错误</h4><p style="margin-bottom:12px; font-size:0.9em;">${escapeHtml(message)}</p><button onclick="this.parentElement.style.display='none'" style="padding: 5px 10px; background:#dc3545; color:white; border:none; border-radius:4px; cursor:pointer;">关闭</button>`;errorDiv.style.display = 'block';} catch (error) {console.error("无法显示错误消息:", error);alert("发生严重错误,且无法显示错误提示。请检查控制台。");}
}// --- Start Initialization Process ---
if (document.readyState === 'complete') {console.log("Document already complete, attempting initialization shortly.");setTimeout(attemptInitialization, 50);
} else {window.addEventListener('load', () => {console.log("Window load event fired, attempting initialization shortly.");setTimeout(attemptInitialization, 50);});document.addEventListener('DOMContentLoaded', () => {console.log("DOMContentLoaded event fired, attempting initialization shortly if not already done.");setTimeout(attemptInitialization, 100);});
}