28、工业网络资产漏洞扫描与风险评估 (模拟) - /安全与维护组件/industrial-network-scanner
76个工业组件库示例汇总
工业网络资产漏洞扫描与风险评估组件 (模拟)
概述
这是一个交互式 Web 组件,旨在模拟工业网络环境中的资产发现、漏洞扫描和风险评估过程。用户可以模拟发现网络中的 PLC、HMI、服务器等资产,对选定的资产发起模拟扫描以查找漏洞,并查看汇总的风险信息和生成模拟审计报告。
请注意:这是一个高度概念化的演示组件。所有资产信息、漏洞数据、扫描过程和风险评估均为模拟,不代表真实的网络扫描或安全评估工具。
主要功能
- 资产发现 (模拟):
- 点击按钮模拟网络扫描,发现预定义的工控资产(如 PLC, HMI, Server, Switch, RTU, Workstation)。
- 在左侧面板动态展示发现的资产列表,包含 IP 地址和设备类型。
- 资产选择与详情:
- 支持从列表中选择资产。
- 在中间面板显示选定资产的详细模拟信息(IP, MAC, 类型, 厂商, OS, 型号, 上次扫描时间, 风险评分)。
- 漏洞扫描 (模拟):
- 为选定的"未扫描"资产启动模拟漏洞扫描。
- 显示扫描进度条和百分比。
- 扫描完成后,根据预定义的漏洞库和资产属性(类型、厂商、OS 等)模拟发现相关漏洞。
- 在中间面板下方展示发现的漏洞列表,包含 CVE 编号、严重性(带颜色标识)和描述。
- 风险评估 (模拟):
- 根据模拟发现的漏洞严重性,计算每个已扫描资产的风险评分(0-100)。
- 在右侧风险仪表盘中汇总显示整体网络风险等级(基于已扫描资产的最高风险评分)、总资产数、已扫描资产数、高风险资产数(评分>=70)和严重漏洞总数。
- 安全审计报告 (模拟):
- 在扫描完成后,允许生成一个简单的文本格式模拟审计报告。
- 报告包含扫描摘要、高风险资产列表(含风险评分和关键漏洞)以及关键漏洞汇总和通用建议。
- 界面与风格:
- 采用苹果科技工业风格,界面专业、清晰。
- 三栏响应式布局(资产列表 | 扫描详情 | 风险仪表盘),适应不同屏幕尺寸。
如何使用
- 打开页面: 在浏览器中打开
index.html
。 - 发现资产: 点击左上角的"发现新资产 (模拟)"按钮。等待片刻,左侧列表将填充模拟的网络资产。
- 选择资产: 从左侧列表中点击一个资产。中间面板将显示该资产的详细信息。此时"扫描选中资产"按钮应变为可用状态(如果该资产未被扫描过)。
- 扫描资产: 点击"扫描选中资产"按钮。左侧面板下方将显示扫描进度条,按钮暂时禁用。
- 查看结果: 扫描完成后(约 3 秒),进度条消失。
- 中间面板下方将列出模拟发现的漏洞(按严重性排序)。
- 资产的风险评分将在详情区域更新。
- 右侧的风险仪表盘数据将更新。
- 左侧列表中该资产的状态将更新为"已扫描"。
- 扫描其他资产: 选择另一个未扫描的资产并重复步骤 4-5。
- 生成报告: 当至少有一个资产被扫描后,右下角的"生成报告 (模拟)"按钮将启用。点击可查看模拟的审计报告摘要。
文件结构
/安全与维护组件/industrial-network-scanner/
├── index.html # 组件的 HTML 结构
├── styles.css # 组件的 CSS 样式
├── script.js # 组件的 JavaScript 逻辑(模拟、交互)
└── README.md # 当前说明文件
技术栈
- HTML5
- CSS3 (Flexbox 布局, Grid 布局, CSS Variables)
- JavaScript (ES6+)
- DOM 操作
Map
数据结构setTimeout
/setInterval
for simulation- 模拟数据处理与匹配
重要说明
- 完全模拟: 这是一个前端模拟演示,没有实际的网络扫描或漏洞验证能力。所有数据和行为都是在
script.js
中预先定义的。 - 无外部依赖: 组件不依赖任何外部 JavaScript 库。
- 简化逻辑: 漏洞匹配和风险评分逻辑非常简化,仅用于概念展示。
效果展示
源码
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>工业网络资产漏洞扫描与风险评估</title><link rel="stylesheet" href="styles.css">
</head>
<body><div class="scanner-container"><aside class="asset-list-panel"><h2>网络资产列表</h2><div class="scan-controls"><button id="discoverAssetsBtn" class="action-button secondary-button">发现新资产</button><button id="scanSelectedBtn" class="action-button" disabled>扫描选中资产</button></div><div class="asset-list-wrapper"><ul id="assetList" class="asset-list"><!-- 资产项将由 JS 动态填充 --><li class="placeholder">等待发现资产...</li></ul></div><div id="scanProgress" class="scan-progress" style="display: none;"><div class="progress-label">扫描中...</div><div class="progress-bar-container"><div id="scanProgressBar" class="progress-bar"></div></div><div id="scanProgressText" class="progress-text">0%</div></div></aside><main class="scan-details-panel"><h2>资产详情与漏洞</h2><div id="assetDetails" class="asset-details-content"><p class="placeholder">请在左侧选择一个资产查看详情</p><!-- 资产详情将动态填充 --><!--<div class="detail-item"><strong>IP 地址:</strong> <span id="detailIp">--</span></div><div class="detail-item"><strong>MAC 地址:</strong> <span id="detailMac">--</span></div><div class="detail-item"><strong>设备类型:</strong> <span id="detailType">--</span></div><div class="detail-item"><strong>厂商:</strong> <span id="detailVendor">--</span></div><div class="detail-item"><strong>风险评分:</strong> <span id="detailRiskScore" class="risk-score">--</span></div>--></div><div class="vulnerability-section"><h3>发现的漏洞</h3><ul id="vulnerabilityList" class="vulnerability-list"><li class="placeholder">选择资产并扫描以查看漏洞</li><!-- 漏洞项将动态填充 --><!--<li class="vulnerability-item severity-critical"><span class="vuln-id">CVE-2023-XXXX</span><span class="vuln-severity">严重</span><p class="vuln-description">远程代码执行漏洞...</p></li>--></ul></div></main><aside class="risk-dashboard-panel"><h2>风险仪表盘</h2><div class="dashboard-summary"><div class="summary-item"><span class="summary-label">网络风险等级</span><span id="networkRiskLevel" class="summary-value risk-level-unknown">未知</span></div><div class="summary-item"><span class="summary-label">总资产数量</span><span id="totalAssets" class="summary-value">0</span></div><div class="summary-item"><span class="summary-label">已扫描资产</span><span id="scannedAssets" class="summary-value">0</span></div><div class="summary-item"><span class="summary-label">高风险资产</span><span id="highRiskAssets" class="summary-value">0</span></div><div class="summary-item"><span class="summary-label">严重漏洞总数</span><span id="criticalVulnCount" class="summary-value">0</span></div></div><section class="reporting-section"><h2>安全审计报告</h2><button id="generateReportBtn" class="action-button secondary-button" disabled>生成报告</button><div id="reportOutput" class="report-output" style="display: none;"><h4>报告摘要:</h4><pre id="reportContent"></pre></div></section></aside></div><script src="script.js"></script>
</body>
</html>
styles.css
:root {--bg-color: #f8f8fa;--panel-bg: #ffffff;--border-color: #e0e0e6;--text-primary: #1d1d1f;--text-secondary: #6e6e73;--text-placeholder: #a0a0a5;--accent-blue: #007aff;--accent-blue-hover: #005ecb;--status-good: #34c759;--status-warning: #ff9500;--status-critical: #ff3b30;--status-medium: #ffcc00; /* Yellow for medium risk/vuln */--status-low: #007aff; /* Blue for low risk/vuln */--status-info: #5ac8fa; /* Light blue for info */--status-unknown: #8e8e93;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--border-radius: 8px;--medium-border-radius: 6px;--small-border-radius: 4px;--panel-padding: 15px;--section-spacing: 20px;--box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);--progress-bar-bg: #e5e5ea;
}body {margin: 0;padding: 0;font-family: var(--font-family);background-color: var(--bg-color);color: var(--text-primary);line-height: 1.5;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;height: 100vh;display: flex;justify-content: center;align-items: center;overflow: hidden;
}.scanner-container {display: flex;width: 96%;max-width: 1500px;height: 88vh;max-height: 750px;background-color: var(--panel-bg);border: 1px solid var(--border-color);border-radius: var(--border-radius);box-shadow: var(--box-shadow);overflow: hidden;
}/* Layout Panels */
.asset-list-panel {flex: 0 0 300px; /* Slightly wider list */border-right: 1px solid var(--border-color);padding: var(--panel-padding);display: flex;flex-direction: column;background-color: #f2f2f7;overflow: hidden; /* Manage scroll internally */
}.scan-details-panel {flex: 1 1 auto; /* Flexible width */display: flex;flex-direction: column;padding: var(--panel-padding);overflow-y: auto; /* Allow details and vulns to scroll */
}.risk-dashboard-panel {flex: 0 0 340px; /* Wider dashboard */border-left: 1px solid var(--border-color);padding: var(--panel-padding);display: flex;flex-direction: column;overflow-y: auto;
}/* Typography & Common Elements */
h2 {font-size: 1.1rem;font-weight: 600;color: var(--text-primary);margin-top: 0;margin-bottom: 15px;border-bottom: 1px solid var(--border-color);padding-bottom: 8px;
}h3 {font-size: 1rem; /* Slightly larger h3 */font-weight: 600;color: var(--text-primary);margin-top: var(--section-spacing);margin-bottom: 10px;
}h4 {font-size: 0.9rem;font-weight: 600;color: var(--text-secondary);margin-top: 15px;margin-bottom: 5px;
}.placeholder {color: var(--text-placeholder);font-style: italic;font-size: 0.9rem;text-align: center;padding: 20px 10px;
}.action-button {display: inline-block; /* Allow side-by-side */padding: 8px 15px;font-size: 0.9rem;font-weight: 500;color: #fff;background-color: var(--accent-blue);border: none;border-radius: var(--medium-border-radius);cursor: pointer;transition: background-color 0.2s ease;text-align: center;margin-right: 10px;
}.action-button:last-child {margin-right: 0;
}.action-button:hover:not(:disabled) {background-color: var(--accent-blue-hover);
}.action-button:disabled {background-color: #c7c7cc;cursor: not-allowed;opacity: 0.7;
}.action-button.secondary-button {background-color: #e5e5ea;color: var(--accent-blue);
}.action-button.secondary-button:hover:not(:disabled) {background-color: #dcdce0;
}/* Asset List Panel */
.scan-controls {margin-bottom: 15px;display: flex; /* Use flex for button layout */justify-content: space-between; /* Space out buttons */
}.scan-controls .action-button {flex: 1; /* Make buttons take equal space */margin-right: 5px; /* Adjust spacing */
}
.scan-controls .action-button:last-child {margin-right: 0;
}.asset-list-wrapper {flex-grow: 1; /* Allow list to take remaining space */overflow-y: auto; /* Enable scrolling for the list */border: 1px solid var(--border-color);border-radius: var(--medium-border-radius);background-color: var(--panel-bg);
}.asset-list {list-style: none;padding: 5px;margin: 0;
}.asset-list-item {padding: 10px 12px;margin-bottom: 5px;border-radius: var(--small-border-radius);cursor: pointer;transition: background-color 0.15s ease;display: flex;flex-direction: column; /* Stack info vertically */border: 1px solid transparent; /* For active state */
}.asset-list-item:hover {background-color: #eef1f5;
}.asset-list-item.active {background-color: var(--accent-blue);color: #fff;border-color: var(--accent-blue-hover);
}
.asset-list-item.active .asset-ip,
.asset-list-item.active .asset-type,
.asset-list-item.active .scan-status {color: #fff;
}.asset-info {display: flex;justify-content: space-between;align-items: center;margin-bottom: 4px;
}.asset-ip {font-weight: 600;font-size: 0.95rem;
}.asset-type {font-size: 0.8rem;color: var(--text-secondary);background-color: #e5e5ea;padding: 2px 5px;border-radius: var(--small-border-radius);
}.scan-status {font-size: 0.8rem;font-style: italic;color: var(--text-secondary);
}.scan-status.scanned { color: var(--status-good); font-style: normal; }
.scan-status.error { color: var(--status-critical); font-style: normal; }
.scan-status.scanning { color: var(--accent-blue); font-style: italic; }.scan-progress {margin-top: 15px;padding-top: 10px;border-top: 1px solid var(--border-color);
}.progress-label {font-size: 0.85rem;color: var(--text-secondary);margin-bottom: 5px;
}.progress-bar-container {width: 100%;height: 8px;background-color: var(--progress-bar-bg);border-radius: 4px;overflow: hidden;margin-bottom: 5px;
}.progress-bar {height: 100%;width: 0%;background-color: var(--accent-blue);transition: width 0.1s linear; /* Smooth progress */
}.progress-text {font-size: 0.8rem;text-align: right;color: var(--text-secondary);
}/* Scan Details Panel */
.asset-details-content {background-color: #fdfdff;border: 1px solid var(--border-color);border-radius: var(--medium-border-radius);padding: var(--panel-padding);margin-bottom: var(--section-spacing);
}.asset-details-content .placeholder {padding: 10px 0;
}.detail-item {margin-bottom: 8px;font-size: 0.9rem;
}.detail-item strong {display: inline-block;width: 80px; /* Align labels */color: var(--text-secondary);
}.risk-score {font-weight: 600;padding: 2px 6px;border-radius: var(--small-border-radius);color: #fff;
}/* Vulnerability List */
.vulnerability-section h3 {margin-top: 0;
}.vulnerability-list {list-style: none;padding: 0;margin: 0;
}.vulnerability-item {background-color: #fdfdff;border: 1px solid var(--border-color);border-radius: var(--medium-border-radius);padding: 10px 15px;margin-bottom: 10px;border-left-width: 4px; /* Severity indicator */
}.vulnerability-item.severity-critical { border-left-color: var(--status-critical); }
.vulnerability-item.severity-high { border-left-color: var(--status-warning); }
.vulnerability-item.severity-medium { border-left-color: var(--status-medium); }
.vulnerability-item.severity-low { border-left-color: var(--status-low); }
.vulnerability-item.severity-info { border-left-color: var(--status-info); }.vuln-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 5px;
}.vuln-id {font-weight: 600;font-size: 0.9rem;color: var(--text-primary);
}.vuln-severity {font-size: 0.8rem;font-weight: 500;padding: 2px 6px;border-radius: var(--small-border-radius);color: #fff;
}.severity-critical .vuln-severity { background-color: var(--status-critical); }
.severity-high .vuln-severity { background-color: var(--status-warning); }
.severity-medium .vuln-severity { background-color: var(--status-medium); }
.severity-low .vuln-severity { background-color: var(--status-low); }
.severity-info .vuln-severity { background-color: var(--status-info); color: var(--text-primary); }.vuln-description {font-size: 0.85rem;color: var(--text-secondary);margin: 0;
}/* Risk Dashboard Panel */
.dashboard-summary {display: grid;grid-template-columns: 1fr 1fr; /* Two columns */gap: 15px;margin-bottom: var(--section-spacing);
}.summary-item {background-color: #f2f2f7;padding: 10px 12px;border-radius: var(--medium-border-radius);text-align: center;
}.summary-label {display: block;font-size: 0.8rem;color: var(--text-secondary);margin-bottom: 5px;
}.summary-value {display: block;font-size: 1.3rem;font-weight: 600;color: var(--text-primary);
}.risk-level-critical { color: var(--status-critical); }
.risk-level-high { color: var(--status-warning); }
.risk-level-medium { color: var(--status-medium); }
.risk-level-low { color: var(--status-low); }
.risk-level-info { color: var(--status-info); }
.risk-level-unknown { color: var(--status-unknown); }.reporting-section {margin-top: auto; /* Push report section to bottom */padding-top: var(--section-spacing);border-top: 1px solid var(--border-color);
}.reporting-section h2 {border-bottom: none; padding-bottom: 0; margin-bottom: 10px;
}.report-output {background-color: #f8f8fa;border: 1px solid var(--border-color);border-radius: var(--medium-border-radius);padding: 10px 15px;margin-top: 10px;max-height: 250px; /* Limit report preview height */overflow-y: auto;
}.report-output pre {white-space: pre-wrap; /* Wrap long lines */word-wrap: break-word;font-size: 0.8rem;color: var(--text-secondary);margin: 0;font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}/* Responsive Design */
@media (max-width: 1100px) {.scanner-container {flex-direction: column;height: auto; /* Allow content height */max-height: none;width: 100%;border: none;border-radius: 0;box-shadow: none;}.asset-list-panel,.risk-dashboard-panel {flex: 0 0 auto; /* Reset flex basis */width: 100%;border-right: none;border-left: none;border-bottom: 1px solid var(--border-color);max-height: 300px; /* Limit height on mobile */overflow-y: auto;}.risk-dashboard-panel {border-bottom: none; /* No bottom border for the last panel */max-height: none;}.scan-details-panel {order: 0; /* Keep details in middle on mobile */border-bottom: 1px solid var(--border-color);overflow-y: visible; /* Allow content to determine height */}.dashboard-summary {grid-template-columns: 1fr 1fr 1fr; /* Three columns on smaller screens */}
}@media (max-width: 600px) {.dashboard-summary {grid-template-columns: 1fr 1fr; /* Back to two columns on very small screens */}.scan-controls {flex-direction: column; /* Stack buttons */}.scan-controls .action-button {margin-right: 0;margin-bottom: 10px; /* Add space between stacked buttons */}.scan-controls .action-button:last-child {margin-bottom: 0;}
}
script.js
document.addEventListener('DOMContentLoaded', () => {// --- DOM Elements ---const discoverAssetsBtn = document.getElementById('discoverAssetsBtn');const scanSelectedBtn = document.getElementById('scanSelectedBtn');const assetListElement = document.getElementById('assetList');const assetDetailsElement = document.getElementById('assetDetails');const vulnerabilityListElement = document.getElementById('vulnerabilityList');const scanProgressElement = document.getElementById('scanProgress');const scanProgressBarElement = document.getElementById('scanProgressBar');const scanProgressTextElement = document.getElementById('scanProgressText');const networkRiskLevelElement = document.getElementById('networkRiskLevel');const totalAssetsElement = document.getElementById('totalAssets');const scannedAssetsElement = document.getElementById('scannedAssets');const highRiskAssetsElement = document.getElementById('highRiskAssets');const criticalVulnCountElement = document.getElementById('criticalVulnCount');const generateReportBtn = document.getElementById('generateReportBtn');const reportOutputElement = document.getElementById('reportOutput');const reportContentElement = document.getElementById('reportContent');// --- State ---let discoveredAssets = new Map(); // Use Map for easier access by IDlet selectedAssetId = null;let isDiscovering = false;let isScanning = false;let scanProgressInterval = null;let currentScanTargetId = null;// --- Configuration ---const DISCOVERY_TIME_MS = 1500;const SCAN_TIME_PER_ASSET_MS = 3000;const PROGRESS_UPDATE_INTERVAL_MS = 50;// --- Mock Data ---const mockAssetsData = [{ id: "plc-001", ip: "192.168.1.10", mac: "00:1B:C0:A1:B2:C3", type: "PLC", vendor: "Siemens", os: "S7-1500 Firmware v2.8", model: "Simatic S7-1500" },{ id: "hmi-001", ip: "192.168.1.20", mac: "00:A0:DE:F1:G2:H3", type: "HMI", vendor: "Rockwell", os: "PanelView Plus 7 v11", model: "PanelView Plus 7" },{ id: "srv-001", ip: "192.168.1.5", mac: "00:50:56:A4:B5:C6", type: "Server", vendor: "Dell", os: "Windows Server 2019", model: "PowerEdge R740" },{ id: "sw-001", ip: "192.168.1.1", mac: "A0:B1:C2:D3:E4:F5", type: "Switch", vendor: "Cisco", os: "IOS 15.2", model: "Catalyst 2960" },{ id: "plc-002", ip: "192.168.1.11", mac: "00:1B:C0:A1:B2:D4", type: "PLC", vendor: "Siemens", os: "S7-1200 Firmware v4.2", model: "Simatic S7-1200" },{ id: "rtu-001", ip: "192.168.1.30", mac: "B0:C1:D2:E3:F4:05", type: "RTU", vendor: "Schneider", os: "Modicon M340 Firmware", model: "Modicon M340" },{ id: "eng-ws-001", ip: "192.168.1.50", mac: "00:50:56:B5:C6:D7", type: "Workstation", vendor: "HP", os: "Windows 10 Enterprise", model: "ZBook Fury" },{ id: "srv-002", ip: "192.168.1.6", mac: "00:50:56:A4:B5:C7", type: "Server", vendor: "Generic", os: "Ubuntu Server 20.04", model: "Custom Build" }];const mockVulnerabilitiesData = [{ cve: "CVE-2023-1001", description: "Siemens S7-1500 Remote Code Execution via Profinet.", severity: "critical", cvssScore: 9.8, affectedVendors: ["Siemens"], affectedTypes: ["PLC"], affectedModels: ["S7-1500"] },{ cve: "CVE-2023-1002", description: "Rockwell PanelView Plus 7 Denial of Service.", severity: "high", cvssScore: 7.5, affectedVendors: ["Rockwell"], affectedTypes: ["HMI"] },{ cve: "CVE-2023-1003", description: "Windows Server SMBv3 Remote Code Execution (Patch available).", severity: "high", cvssScore: 8.8, affectedOs: ["Windows Server 2019", "Windows 10"] },{ cve: "CVE-2023-1004", description: "Cisco IOS Default SNMP Community String.", severity: "medium", cvssScore: 5.3, affectedVendors: ["Cisco"], affectedTypes: ["Switch"] },{ cve: "CVE-2023-1005", description: "Modicon M340 Unauthenticated Access.", severity: "high", cvssScore: 8.2, affectedVendors: ["Schneider"], affectedTypes: ["RTU"] },{ cve: "CVE-2023-1006", description: "Outdated Apache Struts library found on server.", severity: "critical", cvssScore: 9.5, affectedTypes: ["Server"] },{ cve: "CVE-2023-1007", description: "Weak SSH Ciphers enabled on Linux server.", severity: "medium", cvssScore: 4.8, affectedOs: ["Ubuntu Server 20.04"] },{ cve: "CVE-2023-1008", description: "Siemens S7-1200 Information Disclosure.", severity: "medium", cvssScore: 6.5, affectedVendors: ["Siemens"], affectedModels: ["S7-1200"] },{ cve: "CVE-2023-1009", description: "Default password found on HMI interface.", severity: "high", cvssScore: 7.1, affectedTypes: ["HMI"] },{ cve: "CVE-2023-1010", description: "Missing security patches for Windows 10.", severity: "medium", cvssScore: 6.8, affectedOs: ["Windows 10 Enterprise"] },];// --- Core Functions ---function discoverAssets() {if (isDiscovering || isScanning) return;isDiscovering = true;discoverAssetsBtn.disabled = true;discoverAssetsBtn.textContent = "发现中...";assetListElement.innerHTML = '<li class="placeholder">正在发现网络资产...</li>';clearAssetDetails();clearVulnerabilityList();resetDashboard();discoveredAssets.clear();selectedAssetId = null;// Simulate discovery timesetTimeout(() => {mockAssetsData.forEach(assetData => {const assetId = assetData.id;discoveredAssets.set(assetId, {...assetData, // Spread the mock datastatus: 'unscanned', // Initial statusvulnerabilities: [],riskScore: 0,lastSeen: new Date().toLocaleString()});});populateAssetList();updateDashboard();discoverAssetsBtn.disabled = false;discoverAssetsBtn.textContent = "发现新资产 (模拟)";isDiscovering = false;}, DISCOVERY_TIME_MS);}function populateAssetList() {assetListElement.innerHTML = ''; // Clear existing list or placeholderif (discoveredAssets.size === 0) {assetListElement.innerHTML = '<li class="placeholder">未发现资产。</li>';return;}// Sort assets by IP for consistency (optional)const sortedAssets = [...discoveredAssets.values()].sort((a, b) => {const ipA = a.ip.split('.').map(Number);const ipB = b.ip.split('.').map(Number);for (let i = 0; i < 4; i++) {if (ipA[i] !== ipB[i]) return ipA[i] - ipB[i];}return 0;});sortedAssets.forEach(asset => {const listItem = document.createElement('li');listItem.className = 'asset-list-item';listItem.dataset.assetId = asset.id;if (asset.id === selectedAssetId) {listItem.classList.add('active');}listItem.innerHTML = `<div class="asset-info"><span class="asset-ip">${asset.ip}</span><span class="asset-type">${asset.type}</span></div><div class="scan-status ${asset.status}">${getScanStatusText(asset.status)}</div>`;listItem.addEventListener('click', () => handleAssetSelect(asset.id));assetListElement.appendChild(listItem);});}function getScanStatusText(status) {switch (status) {case 'unscanned': return '未扫描';case 'scanning': return '扫描中...';case 'scanned': return '已扫描';case 'error': return '扫描错误';default: return '未知';}}function handleAssetSelect(assetId) {if (isScanning && currentScanTargetId === assetId) return; // Don't change selection if the selected is scanningselectedAssetId = assetId;// Update active class on list itemsdocument.querySelectorAll('.asset-list-item').forEach(item => {item.classList.toggle('active', item.dataset.assetId === assetId);});displayAssetDetails(assetId);displayVulnerabilities(assetId);const asset = discoveredAssets.get(assetId);scanSelectedBtn.disabled = !asset || asset.status === 'scanning' || asset.status === 'error' || asset.status === 'scanned'; // Disable if scanning, error or already scanned}function displayAssetDetails(assetId) {const asset = discoveredAssets.get(assetId);if (!asset) {clearAssetDetails();return;}const riskLevel = getRiskLevel(asset.riskScore);assetDetailsElement.innerHTML = `<div class="detail-item"><strong>IP 地址:</strong> <span id="detailIp">${asset.ip}</span></div><div class="detail-item"><strong>MAC 地址:</strong> <span id="detailMac">${asset.mac}</span></div><div class="detail-item"><strong>设备类型:</strong> <span id="detailType">${asset.type}</span></div><div class="detail-item"><strong>厂商:</strong> <span id="detailVendor">${asset.vendor}</span></div><div class="detail-item"><strong>操作系统:</strong> <span id="detailOs">${asset.os || '--'}</span></div><div class="detail-item"><strong>型号:</strong> <span id="detailModel">${asset.model || '--'}</span></div><div class="detail-item"><strong>上次扫描:</strong> <span id="detailLastScan">${asset.scanTime || '--'}</span></div><div class="detail-item"><strong>风险评分:</strong> <span id="detailRiskScore" class="risk-score ${riskLevel.class}">${asset.riskScore.toFixed(0)}</span></div>`;}function clearAssetDetails() {assetDetailsElement.innerHTML = '<p class="placeholder">请在左侧选择一个资产查看详情</p>';}function displayVulnerabilities(assetId) {const asset = discoveredAssets.get(assetId);vulnerabilityListElement.innerHTML = ''; // Clear existing listif (!asset || asset.status === 'unscanned' || asset.status === 'scanning') {vulnerabilityListElement.innerHTML = `<li class="placeholder">${asset && asset.status === 'scanning' ? '扫描完成后显示漏洞...' : '选择资产并扫描以查看漏洞'}</li>`;return;}if (asset.status === 'error') {vulnerabilityListElement.innerHTML = '<li class="placeholder error">扫描资产时出错。</li>';return;}if (asset.vulnerabilities.length === 0) {vulnerabilityListElement.innerHTML = '<li class="placeholder">未发现漏洞。</li>';return;}// Sort vulnerabilities by severity (critical first)asset.vulnerabilities.sort((a, b) => scoreSeverity(b.severity) - scoreSeverity(a.severity));asset.vulnerabilities.forEach(vuln => {const listItem = document.createElement('li');listItem.className = `vulnerability-item severity-${vuln.severity}`;listItem.innerHTML = `<div class="vuln-header"><span class="vuln-id">${vuln.cve}</span><span class="vuln-severity">${vuln.severity.charAt(0).toUpperCase() + vuln.severity.slice(1)}</span></div><p class="vuln-description">${vuln.description}</p>`;vulnerabilityListElement.appendChild(listItem);});}function clearVulnerabilityList() {vulnerabilityListElement.innerHTML = '<li class="placeholder">选择资产并扫描以查看漏洞</li>';}function scoreSeverity(severity) {switch (severity) {case 'critical': return 4;case 'high': return 3;case 'medium': return 2;case 'low': return 1;case 'info': return 0;default: return -1;}}function scanSelectedAsset() {if (!selectedAssetId || isScanning) return;const asset = discoveredAssets.get(selectedAssetId);if (!asset || asset.status === 'scanning' || asset.status === 'scanned' || asset.status === 'error') {console.warn(`Asset ${selectedAssetId} cannot be scanned (status: ${asset?.status})`);return;}// Start scanning processisScanning = true;currentScanTargetId = selectedAssetId;scanSelectedBtn.disabled = true;discoverAssetsBtn.disabled = true; // Disable discovery during scan// Update UI for the target assetasset.status = 'scanning';updateAssetListItemStatus(currentScanTargetId, 'scanning');if(selectedAssetId === currentScanTargetId) { // Update details if currently selected// Keep details visible but maybe add a scanning indicator?// displayAssetDetails(currentScanTargetId); // Re-displaying might clear risk score, maybe not neededvulnerabilityListElement.innerHTML = '<li class="placeholder">正在扫描漏洞...</li>'; // Update vuln list placeholder}// Show and start progress bar animationlet progress = 0;scanProgressElement.style.display = 'block';scanProgressBarElement.style.width = '0%';scanProgressTextElement.textContent = '0%';const totalSteps = SCAN_TIME_PER_ASSET_MS / PROGRESS_UPDATE_INTERVAL_MS;let currentStep = 0;scanProgressInterval = setInterval(() => {currentStep++;progress = Math.min(100, Math.round((currentStep / totalSteps) * 100));scanProgressBarElement.style.width = `${progress}%`;scanProgressTextElement.textContent = `${progress}%`;if (progress >= 100) {clearInterval(scanProgressInterval);scanProgressInterval = null;// --- Finish Scan ---const targetAsset = discoveredAssets.get(currentScanTargetId);if (targetAsset) {// Simulate finding vulnerabilitiesfindVulnerabilities(currentScanTargetId);// Calculate risk scorecalculateRiskScore(currentScanTargetId);targetAsset.status = 'scanned';targetAsset.scanTime = new Date().toLocaleString(); // Record scan timeupdateAssetListItemStatus(currentScanTargetId, 'scanned');// Update details if the scanned asset is still selectedif (selectedAssetId === currentScanTargetId) {displayAssetDetails(currentScanTargetId);displayVulnerabilities(currentScanTargetId);scanSelectedBtn.disabled = true; // Already scanned}} else {// Should not happen ideally, but handle error caseupdateAssetListItemStatus(currentScanTargetId, 'error');if (selectedAssetId === currentScanTargetId) {clearVulnerabilityList();vulnerabilityListElement.innerHTML = '<li class="placeholder error">扫描资产时出错。</li>';scanSelectedBtn.disabled = true;}}// --- Cleanup ---scanProgressElement.style.display = 'none';updateDashboard(); // Update overall statsisScanning = false;currentScanTargetId = null;discoverAssetsBtn.disabled = false; // Re-enable discovery// Re-enable scan button only if a different unscanned asset is selectedif(selectedAssetId) {const currentSelectedAsset = discoveredAssets.get(selectedAssetId);scanSelectedBtn.disabled = !currentSelectedAsset || currentSelectedAsset.status !== 'unscanned';} else {scanSelectedBtn.disabled = true;}}}, PROGRESS_UPDATE_INTERVAL_MS);}function updateAssetListItemStatus(assetId, status) {const listItem = assetListElement.querySelector(`[data-asset-id="${assetId}"] .scan-status`);if (listItem) {listItem.className = `scan-status ${status}`;listItem.textContent = getScanStatusText(status);}}function findVulnerabilities(assetId) {const asset = discoveredAssets.get(assetId);if (!asset) return;asset.vulnerabilities = []; // Clear previous results for rescan scenarioconst potentialVulns = mockVulnerabilitiesData.filter(vuln => {// Match by typeif (vuln.affectedTypes && !vuln.affectedTypes.includes(asset.type)) {return false;}// Match by vendorif (vuln.affectedVendors && !vuln.affectedVendors.includes(asset.vendor)) {return false;}// Match by OSif (vuln.affectedOs && (!asset.os || !vuln.affectedOs.some(vo => asset.os.includes(vo)))) {return false;}// Match by Modelif (vuln.affectedModels && (!asset.model || !vuln.affectedModels.includes(asset.model))) {return false;}// If no specific filters match, check if it's a general type vulnerabilityif (!vuln.affectedVendors && !vuln.affectedOs && !vuln.affectedModels && vuln.affectedTypes && vuln.affectedTypes.includes(asset.type)) {return true;}// If multiple filters exist, they likely act as AND, so if we passed all, return true.// If no filters exist for a category, it's assumed to apply broadly unless another filter excludes it.return true; // Default allow if not filtered out by specifics});// Randomly "find" a subset of potential vulnerabilitiesconst maxVulnsToFind = 5;// Ensure at least one critical/high vuln for risky assets if possible, for demo purposeslet numVulnsFound = 0;if (asset.type === 'PLC' || asset.type === 'Server') {numVulnsFound = Math.floor(Math.random() * (Math.min(maxVulnsToFind, potentialVulns.length))) + 1; // Find at least 1 for these} else {numVulnsFound = Math.floor(Math.random() * (Math.min(maxVulnsToFind, potentialVulns.length) + 1));}numVulnsFound = Math.min(numVulnsFound, potentialVulns.length); // Cannot find more than exist// Shuffle potential vulns to get random onesconst shuffledVulns = potentialVulns.sort(() => 0.5 - Math.random());asset.vulnerabilities = shuffledVulns.slice(0, numVulnsFound);}function calculateRiskScore(assetId) {const asset = discoveredAssets.get(assetId);if (!asset || !asset.vulnerabilities) {if(asset) asset.riskScore = 0;return;}let score = 0;asset.vulnerabilities.forEach(vuln => {switch (vuln.severity) {case 'critical': score += 25; break; // Higher weight for criticalcase 'high': score += 15; break;case 'medium': score += 5; break;case 'low': score += 1; break;// info doesn't add score}});asset.riskScore = Math.min(100, score); // Cap score at 100}function updateDashboard() {let total = discoveredAssets.size;let scanned = 0;let highRisk = 0;let criticalVulnCount = 0;let maxRiskScore = 0;discoveredAssets.forEach(asset => {if (asset.status === 'scanned') {scanned++;if (asset.riskScore >= 70) { // Define high risk thresholdhighRisk++;}asset.vulnerabilities.forEach(vuln => {if (vuln.severity === 'critical') {criticalVulnCount++;}});maxRiskScore = Math.max(maxRiskScore, asset.riskScore);}if (asset.status === 'error') {// Optionally count errors or handle differently}});totalAssetsElement.textContent = total;scannedAssetsElement.textContent = scanned;highRiskAssetsElement.textContent = highRisk;criticalVulnCountElement.textContent = criticalVulnCount;// Update overall network risk level based on the highest score found among scanned assetsconst networkRisk = getRiskLevel(maxRiskScore);networkRiskLevelElement.textContent = (scanned > 0) ? networkRisk.level : '未知';networkRiskLevelElement.className = `summary-value ${(scanned > 0) ? networkRisk.class : 'risk-level-unknown'}`;// Enable report generation if any assets are scannedgenerateReportBtn.disabled = scanned === 0;}function resetDashboard() {totalAssetsElement.textContent = '0';scannedAssetsElement.textContent = '0';highRiskAssetsElement.textContent = '0';criticalVulnCountElement.textContent = '0';networkRiskLevelElement.textContent = '未知';networkRiskLevelElement.className = 'summary-value risk-level-unknown';generateReportBtn.disabled = true;reportOutputElement.style.display = 'none'; // Hide report areareportContentElement.textContent = '';}function getRiskLevel(score) {if (score >= 90) return { level: '严重', class: 'risk-level-critical' };if (score >= 70) return { level: '高危', class: 'risk-level-high' };if (score >= 40) return { level: '中危', class: 'risk-level-medium' };if (score >= 1) return { level: '低危', class: 'risk-level-low' };// If score is 0 after scanning, consider it Info/Goodconst scannedCount = Array.from(discoveredAssets.values()).filter(a => a.status === 'scanned').length;if (score === 0 && scannedCount > 0) return { level: '信息', class: 'risk-level-info' };return { level: '未知', class: 'risk-level-unknown' }; // Default or before scan}function generateReport() {if (isScanning) return;const scannedList = Array.from(discoveredAssets.values()).filter(a => a.status === 'scanned');if (scannedList.length === 0) {reportContentElement.textContent = "没有已扫描的资产可生成报告。";reportOutputElement.style.display = 'block';return;}let report = `工业网络安全审计报告 (模拟)
`;report += `生成时间: ${new Date().toLocaleString()}
`;report += `===================================`;report += `资产概览:
`;report += `- 总发现资产: ${discoveredAssets.size}
`;report += `- 已扫描资产: ${scannedList.length}
`;const maxScore = Math.max(0, ...scannedList.map(a => a.riskScore));report += `- 最高风险评分: ${maxScore.toFixed(0)}
`;report += `- 整体网络风险评估: ${getRiskLevel(maxScore).level}`;report += `高风险资产 (评分 >= 70):
`;const highRiskList = scannedList.filter(a => a.riskScore >= 70).sort((a, b) => b.riskScore - a.riskScore);if (highRiskList.length > 0) {highRiskList.forEach(asset => {report += `- ${asset.ip} (${asset.type}, ${asset.vendor}) - 风险评分: ${asset.riskScore.toFixed(0)}
`;asset.vulnerabilities.filter(v => v.severity === 'critical' || v.severity === 'high').slice(0, 2).forEach(v => { // Show top 2 critical/highreport += ` - [${v.severity.toUpperCase()}] ${v.cve}: ${v.description.substring(0, 50)}...
`;});});} else {report += `- 无
`;}report += `
关键漏洞汇总 (严重):
`;let criticalVulnsFound = false;scannedList.forEach(asset => {asset.vulnerabilities.filter(v => v.severity === 'critical').forEach(v => {criticalVulnsFound = true;report += `- ${v.cve} on ${asset.ip} (${asset.type}): ${v.description}
`;});});if (!criticalVulnsFound) {report += `- 未发现严重漏洞
`;}report += `
建议:
- 优先处理高风险资产上的严重和高危漏洞。
- 对所有资产应用最新的安全补丁。
- 定期进行漏洞扫描和风险评估。`;report += `--- 报告结束 ---`;reportContentElement.textContent = report;reportOutputElement.style.display = 'block';}// --- Initial Setup ---assetListElement.innerHTML = '<li class="placeholder">点击 "发现新资产" 开始</li>';clearAssetDetails();clearVulnerabilityList();resetDashboard();// --- Event Listeners ---discoverAssetsBtn.addEventListener('click', discoverAssets);scanSelectedBtn.addEventListener('click', scanSelectedAsset);generateReportBtn.addEventListener('click', generateReport);});