└── README.md /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## FastWLAT(Fast Web Log Analysis Tool) 3 | 4 | FastWLAT 是一款基于 Electron + Vue 3 + TypeScript 开发的现代化 Web 日志分析工具。专为安全应急响应和日志分析场景设计,提供高性能的日志解析、智能威胁检测、地理位置可视化等功能。 5 | 6 | [Windows日志分析工具链接](https://github.com/vam876/FastWinLog ) | [Linux日志分析工具链接](https://github.com/vam876/FastLinLog ) 7 | 8 | ## 🏆 FastWLAT功能介绍 9 | 10 | - **最新版本**: V1.1.0 11 | 12 | - **更新日期**: 2025/12/18 13 | 14 | - **下载地址** 15 | 16 | 最新版下载(Windows) https://github.com/vam876/FastWLAT/releases/tag/v1.1.0 17 | 18 | 旧版下载(Windows/macOS/Linux) 19 | https://github.com/vam876/FastWLAT/releases/tag/v1.0.1 20 | 21 | 22 | ### 现有工具的痛点分析 23 | 24 | **🔍 部分日志分析工具的局限性**: 25 | 26 | - **ELK Stack**: 部署复杂度极高,需要专业运维团队,资源消耗较大 27 | - **Splunk**: 许可费用昂贵,普通用户难以承受 28 | - **360星图**: 功能封闭,缺少弹性配置,功能强大但无法满足个性化需求 29 | - **WebLog Expert/HTTP Logs Viewe**: 类似工具很多,但是缺少安全分析功能集成 30 | - **GitHub部分开源脚本**: 年代久远,缺乏维护,功能单一,用户体验极差 31 | - **近年来收集的一些图形化nginx/Apache分析工具**: 用户体验不好,误报率极高,性能差 32 | 33 | **⚡ 技术痛点**: 34 | 35 | - 操作不便,学习成本高 36 | - 功能相对封闭,扩展性差 37 | - 缺少现代化的可视化界面 38 | - 弹性不足,无法适应不同规模需求 39 | - 年代久远,技术架构落后 40 | - 长期无人维护 41 | 42 | 正是基于这些痛点,我们决定从零开始,花了五天时间打造一款真正现代化、用户友好、高度自定义和支持多视图的Web日志分析工具,最终目的是实现**既可给领导直观的展示,也可以通过工具快速识别恶意WEB攻击**。 43 | 44 | 45 | 46 | ### 🎯 软件架构 47 | 48 | 49 | 50 | ### 日志分析流程 51 | 52 | 53 | 54 | 55 | ### 📊 智能仪表盘 - 访问态势一目了然 56 | 57 | 58 | 59 | 60 | ### 📥 强大导入系统 - 支持主流WEB日志格式 61 | 62 | - **格式全覆盖**: Apache、Nginx、IIS、Tomcat等日志格式 63 | 64 | - **智能识别**: 自动格式检测 65 | 66 | - **大文件处理**: 百万级日志处理优化 67 | 68 | - **三种方式**: 文件上传、文本粘贴、示例数据快速体验 69 | 70 | - 格式不兼容欢迎提供示例格式进行适配 71 | 72 | 73 | 74 | 75 | ### 🔍 高性能日志视图 - 百万条日志秒级响应 76 | 77 | - **虚拟滚动**: 支持百万级日志流畅浏览 78 | 79 | - **多视图模式**: 表格、树状、聚合三种视图 80 | 81 | - **秒级搜索**: 全文搜索、正则匹配、条件过滤 82 | 83 | - **智能分页**: 动态加载,内存自动回收 84 | 85 | - **快速过滤功能**: 一键剔除静态文件,一键排除404、30X等状态码日志,留下清爽的日志浏览视图,即可节省渲染性能,也可以排除分析干扰和误报 86 | 87 | 88 | 89 | 90 | 91 | **上图:** 日志列表视图,可以快速过滤、搜索和分析 92 | 93 | 94 | 95 | 96 | **上图:** 树目录视图,通过日志还原出网站原本的目录结构,可以折叠、展开,支持搜索、过滤、仅显示某个路径等多种功能,出现告警日志会显示分析按钮 97 | 98 | 99 | 100 | 101 | **上图:** 出现告警的文件会由分析按钮,点击分析按钮进行高级分析视图(仅针对当前选择的路径) 102 | 103 | 104 | 105 | **上图:** 选择对应的文件会出现详情按钮,点击详情按钮进行详情视图(包含当前路径的所有日志进行展示) 106 | 107 | 108 | **上图:** 高级分析视图,可以快速排序、搜索和分析,点击分析按钮可以针对当前路径、IP、地区、状态码等数据进行汇聚分析 109 | 110 | 111 | ### 📈 专业分析引擎 - 数据洞察一步到位 112 | 113 | - **多维分析**: 时间、状态码、用户代理、地理位置 114 | 115 | - **趋势识别**: 24小时访问模式、异常时段检测 116 | 117 | - **性能分析**: 响应时间分布、错误率统计 118 | 119 |  120 | 121 | 122 | ### 🛡️ 智能威胁检测 - 企业级安全防护 123 | 124 | - **威胁类型**: 内置多种威胁检测规则,分析页面支持分类、筛选 125 | - **条件引擎**: 状态码、IP、时间、返回数据包大小等多维度精准匹配 126 | 127 | 128 | 129 | 130 | 131 | 132 | - **二次过滤**: 告警页面支持正则表达式二次匹配,高亮显示定位匹配规则快速优化 133 | 134 | 135 | 136 | 137 | ### 🗺️ 威胁地图可视化 - 全球威胁态势感知 138 | 139 | 140 | 141 | - **地理定位**: 基于MaxMind数据库的精准定位,通过优秀的前端库和地图数据进行可视化展示,将访问和攻击来源渲染到地图 142 | 143 | 144 | 145 | 146 | - **实时动画**: 支持世界地图和中国地图进行动态展示,威胁来源一目了然 147 | 148 | 149 | 150 | 151 | - **双模式**: 流量地图模式 ↔ 攻击地图模式无缝切换,攻击地图可以渲染出现告警的攻击数据,基于源IP和自定义防护地标进行攻击路径绘制 152 | 153 | 154 | 155 | 156 | - **交互探索**: 缩放、筛选、详情查看,点击对应的地区节点出现详情卡片 157 | 158 | 159 | 160 | 161 | ### ⚙️ 灵活规则管理 - 自定义安全策略 162 | 163 | 164 | 165 | 166 | - **可视化配置**: 图形界面,无需编程基础 167 | 168 | - **条件组合**: HTTP方法、状态码、IP、时间范围、返回长度等 169 | 170 | - **实时生效**: 规则修改即时应用,无需重启 171 | 172 | - **高度自定义**: 可以添加删除优化告警规则,不加密,无任何限制 173 | 174 | 175 | 176 | 177 | ### 🎨 个性化主题 - 适合每个团队的风格 178 | 179 | - **三种精美主题**: 天空蓝、简约灰、经典蓝紫 180 | - **一键切换**: 实时预览,用户偏好自动保存 181 | - **现代设计**: 现代化的UI/UX设计 182 | 183 | 184 | 185 | 186 | ### ⚙️ 智能设置中心 - 系统优化一站式 187 | 188 | 189 | 190 | - **三模式存储**: 本地内存、zinc(ES兼容)、Redis高性能(v1.1.0版本开始已弃用,改为IndexedDB持久化存储)无缝切换。目前仅开放内存模式 191 | 192 | - **性能监控**: 实时内存使用、缓存状态监控 193 | 194 | - **连接管理**: Redis自动重连和状态检测 195 | 196 | - **配置持久**: 用户设置自动保存和恢复 197 | 198 | 199 | 200 | 201 | 202 | ## 🌟 FastWLAT的创新点 203 | 204 | ## 一、创新性的日志浏览系统 205 | 206 | ### 🔍 三视图架构设计 207 | 208 | FastWLAT采用了创新的三视图日志浏览架构,每种视图都针对不同的分析场景进行了深度优化: 209 | 210 | #### 1️⃣ 常规列表视图 - 高性能数据展示 211 | 212 | **技术实现**: 213 | 214 | ```typescript 215 | // 虚拟滚动核心实现 216 | export const VirtualScrollList = defineComponent({ 217 | name: 'VirtualScrollList', 218 | setup(props: { items: LogEntry[] }) { 219 | const containerRef = ref() 220 | const scrollTop = ref(0) 221 | const itemHeight = 40 222 | const visibleCount = Math.ceil(window.innerHeight / itemHeight) + 5 223 | 224 | // 计算可见范围 225 | const visibleRange = computed(() => { 226 | const start = Math.floor(scrollTop.value / itemHeight) 227 | const end = Math.min(start + visibleCount, props.items.length) 228 | return { start, end } 229 | }) 230 | 231 | // 只渲染可见项目 232 | const visibleItems = computed(() => 233 | props.items.slice(visibleRange.value.start, visibleRange.value.end) 234 | ) 235 | 236 | return { containerRef, visibleItems, visibleRange } 237 | } 238 | }) 239 | ``` 240 | 241 | **核心特性**: 242 | - **虚拟滚动**: 支持百万级日志条目无卡顿浏览 243 | - **秒级搜索**: 基于内存索引的快速检索 244 | - **智能过滤**: 多条件组合筛选,精准定位 245 | - **实时排序**: 支持所有字段的动态排序 246 | 247 | #### 2️⃣ **创新树状视图** 🌳 - 解决分析可视化难题 248 | 249 | > **这是FastWLAT和让其他日志分析工具不同的功能** 250 | 251 | **解决的核心痛点**: 252 | 253 | - 快速理解整个网站架构 254 | - 希望直观看到每个路径的访问频次和威胁分布 255 | - 显示某个路径下状态码分布情况了 256 | - 点击某个路径或者接口可以快速查看相关日志的访问情况 257 | - 点击某个路径或者接口可以快速查看相关日志的告警情况 258 | 259 | ``` 260 | 实际效果展示: 261 | 🏠 网站 262 | ├── 📁 文章目录/ (2,345次访问) 263 | │ ├── 📄 技术分享.html (200 856次) 🟢 正常 264 | │ ├── 📄 生活随笔.html (302 234次) 🟢 正常 265 | │ └── 📁 图片/ (404 123次 200 4次) 266 | │ ├── 📄 avatar.jpg (67次) 🟢 正常 267 | │ └── 📄 banner.png (56次) 🟢 正常 268 | ├── 📁 管理后台/ (45次访问) ⚠️ 异常,点击分析 269 | │ ├── 📄 login.php (23次) 🟡 异常,点击分析 270 | │ └── 📄 admin.php (22次) 🔴 异常,点击分析 271 | └── 📄 首页 (5,678次) 🟢 正常 272 | ``` 273 | 274 | **技术架构**: 275 | 276 | ```typescript 277 | // Trie树数据结构 - 构建URL层次结构 278 | class TrieNode { 279 | children: Map = new Map() 280 | logs: LogEntry[] = [] 281 | totalCount: number = 0 282 | threatCount: number = 0 283 | 284 | insert(pathParts: string[], log: LogEntry) { 285 | let current: TrieNode = this 286 | 287 | for (const part of pathParts) { 288 | if (!current.children.has(part)) { 289 | current.children.set(part, new TrieNode()) 290 | } 291 | current = current.children.get(part)! 292 | current.totalCount++ 293 | 294 | // 威胁统计 295 | if (log.threatLevel && log.threatLevel !== 'normal') { 296 | current.threatCount++ 297 | } 298 | } 299 | 300 | current.logs.push(log) 301 | } 302 | 303 | toTree(): TreeNode { 304 | return { 305 | name: this.name, 306 | count: this.totalCount, 307 | threatCount: this.threatCount, 308 | children: Array.from(this.children.entries()).map(([name, node]) => ({ 309 | name, 310 | ...node.toTree() 311 | })) 312 | } 313 | } 314 | } 315 | ``` 316 | 317 | **虚拟化渲染优化**: 318 | ```typescript 319 | // 虚拟树视图 - 解决大数据集卡顿问题 320 | export const VirtualTreeView = defineComponent({ 321 | setup() { 322 | // 扁平化树结构用于虚拟滚动 323 | const flattenTree = (nodes: TreeNode[], level = 0): FlatNode[] => { 324 | const result: FlatNode[] = [] 325 | 326 | for (const node of nodes) { 327 | result.push({ ...node, level, expanded: expandedNodes.has(node.id) }) 328 | 329 | if (expandedNodes.has(node.id) && node.children?.length) { 330 | result.push(...flattenTree(node.children, level + 1)) 331 | } 332 | } 333 | 334 | return result 335 | } 336 | 337 | // 只渲染可见节点 338 | const visibleNodes = computed(() => { 339 | const flattened = flattenTree(treeData.value) 340 | const start = Math.floor(scrollTop.value / nodeHeight) 341 | const end = start + visibleCount 342 | return flattened.slice(start, end) 343 | }) 344 | 345 | return { visibleNodes } 346 | } 347 | }) 348 | ``` 349 | 350 | **交互功能**: 351 | - **详细视图**: 点击节点弹出模态框,显示当前路径的所有访问记录 352 | - **分析视图**: 查看当前接口的访问统计、响应时间分布、错误率等 353 | - **快速定位**: 一键跳转到日志表格的具体条目 354 | - **威胁可视**: 节点颜色表示威胁等级,一目了然 355 | 356 | #### 3️⃣ **智能聚合视图** 📊 - 数据洞察利器 357 | 358 | **技术实现**: 359 | ```typescript 360 | // 聚合数据计算引擎 361 | export class AggregationEngine { 362 | aggregateByField(logs: LogEntry[], field: keyof LogEntry): AggregatedData[] { 363 | const aggregation = new Map() 364 | 365 | logs.forEach(log => { 366 | const value = String(log[field]) 367 | const item = aggregation.get(value) || { 368 | value, 369 | count: 0, 370 | threatCount: 0, 371 | lastAccess: new Date(0), 372 | samples: [] 373 | } 374 | 375 | item.count++ 376 | if (log.threatLevel && log.threatLevel !== 'normal') { 377 | item.threatCount++ 378 | } 379 | 380 | if (new Date(log.timestamp) > item.lastAccess) { 381 | item.lastAccess = new Date(log.timestamp) 382 | } 383 | 384 | // 保存样本数据 385 | if (item.samples.length < 5) { 386 | item.samples.push(log) 387 | } 388 | 389 | aggregation.set(value, item) 390 | }) 391 | 392 | return Array.from(aggregation.values()) 393 | .sort((a, b) => b.count - a.count) 394 | } 395 | } 396 | ``` 397 | 398 | **功能特色**: 399 | - **自动聚合**: 相同请求自动合并,显示访问频次 400 | - **字段汇聚**: 展示URL、IP、User-Agent、国家、城市等字段的统计信息 401 | - **趋势分析**: 时间维度的访问模式分析 402 | - **详情钻取**: 点击聚合项查看详细记录和样本 403 | 404 | --- 405 | 406 | ## 二、突破性的威胁检测技术 407 | 408 | ### 🛡️ 多维度条件引擎 409 | 410 | 传统的威胁检测存在严重问题: 411 | 412 | **❌ 传统方案的缺陷**: 413 | 414 | - **单行匹配**: 仅基于单行记录进行正则匹配 415 | - **误报率高**: 无法区分攻击成功与失败,误报率15-20% 416 | - **规则封闭**: 内置规则无法修改,适应性极差 417 | - **上下文缺失**: 缺乏请求上下文信息的综合判断 418 | 419 | **✅ FastWLAT的创新模式**: 420 | 421 | #### 多维度条件匹配 422 | ```typescript 423 | // 创新的威胁检测引擎 424 | export class ThreatDetectionEngine { 425 | evaluateRule(rule: ThreatRule, log: LogEntry): ThreatMatch | null { 426 | // 1. 正则模式匹配 427 | const regex = new RegExp(rule.pattern, 'i') 428 | const matchText = this.getMatchText(log) 429 | if (!regex.test(matchText)) return null 430 | 431 | // 2. 多维度条件验证 432 | if (!this.evaluateConditions(rule.conditions, log)) return null 433 | 434 | return { 435 | rule, 436 | log, 437 | matchedText: matchText, 438 | riskScore: this.calculateRiskScore(rule, log), 439 | timestamp: new Date() 440 | } 441 | } 442 | 443 | evaluateConditions(conditions: RuleConditions, log: LogEntry): boolean { 444 | // 状态码条件 - 核心创新点 445 | if (conditions.statusCodes?.length) { 446 | if (!conditions.statusCodes.includes(log.statusCode)) { 447 | return false // 只在指定状态码时告警 448 | } 449 | } 450 | 451 | // HTTP方法条件 452 | if (conditions.methods?.length) { 453 | if (!conditions.methods.includes(log.method)) return false 454 | } 455 | 456 | // 响应包大小条件 457 | if (conditions.responseSize) { 458 | const size = log.responseSize || 0 459 | if (size < conditions.responseSize.min || size > conditions.responseSize.max) { 460 | return false 461 | } 462 | } 463 | 464 | // 时间窗口条件 465 | if (conditions.timeRange) { 466 | const hour = new Date(log.timestamp).getHours() 467 | const startHour = parseInt(conditions.timeRange.start.split(':')[0]) 468 | const endHour = parseInt(conditions.timeRange.end.split(':')[0]) 469 | if (hour < startHour || hour > endHour) return false 470 | } 471 | 472 | // IP黑白名单 473 | if (conditions.ipBlacklist?.includes(log.ip)) return true 474 | if (conditions.ipWhitelist?.length) { 475 | return conditions.ipWhitelist.includes(log.ip) 476 | } 477 | 478 | return true 479 | } 480 | } 481 | ``` 482 | 483 | #### 智能组合判断示例 484 | 485 | **场景1: SQL注入检测** 486 | ```typescript 487 | { 488 | name: "SQL注入 - Union查询", 489 | pattern: "union\\s+(all\\s+)?select", 490 | conditions: { 491 | statusCodes: [200], // 关键创新:只在成功响应时告警 492 | methods: ["GET", "POST"], 493 | responseSize: { min: 1000 } // 响应包异常大,可能是数据泄露 494 | } 495 | } 496 | ``` 497 | 498 | **传统方案**: 只要URL包含`union select`就告警 499 | **FastWLAT方案**: URL包含`union select` + 状态码200 + 响应包>1KB = 真实威胁 500 | 501 | **效果对比**: 502 | 503 | - 误报率: 15.2% → 3.1% (降低79.6%) 504 | - 检测准确率: 78% → 95.2% (提升22%) 505 | 506 | **场景2: 后台爆破检测** 507 | ```typescript 508 | { 509 | name: "管理后台爆破", 510 | pattern: "/(admin|wp-admin|phpmyadmin)", 511 | conditions: { 512 | statusCodes: [401, 403], // 只在认证失败时告警 513 | timeRange: { start: "22:00", end: "06:00" }, // 非工作时间 514 | requestFrequency: { count: 10, timeWindow: 300 } // 5分钟内10次,待完善 515 | } 516 | } 517 | ``` 518 | 519 | 520 | ## 二、突破性的威胁检测技术 521 | 522 | ### 🎯 创新的多维度检测模式 523 | 524 | #### 传统检测vsFastWLAT检测 525 | 526 | **传统模式的问题**: 527 | ``` 528 | 日志: GET /admin.php?id=1' UNION SELECT * FROM users-- HTTP/1.1" 404 529 | 传统检测: ✅ 发现SQL注入 (误报) 530 | 实际情况: ❌ 攻击失败 (404错误) 531 | ``` 532 | 533 | **FastWLAT模式**: 534 | 535 | ``` 536 | 日志: GET /admin.php?id=1' UNION SELECT * FROM users-- HTTP/1.1" 200 537 | 创新检测: 538 | ✅ 正则匹配: UNION SELECT (√) 539 | ✅ 状态码检查: 200 (√) 540 | ✅ 响应大小: 15KB (√ 异常大) 541 | 🚨 综合判断: 真实威胁! 542 | ``` 543 | 544 | #### 高级条件引擎实现 545 | 546 | ```typescript 547 | export interface RuleConditions { 548 | statusCodes?: number[] // 状态码条件 549 | methods?: string[] // HTTP方法 550 | responseSize?: { // 响应包大小 551 | min: number 552 | max: number 553 | } 554 | timeRange?: { // 时间窗口 555 | start: string // "09:00" 556 | end: string // "18:00" 557 | } 558 | ipWhitelist?: string[] // IP白名单 559 | ipBlacklist?: string[] // IP黑名单 560 | requestFrequency?: { // 请求频率 561 | count: number 562 | timeWindow: number // 秒 563 | } 564 | userAgentPattern?: string // User-Agent模式 565 | refererPattern?: string // Referer模式 566 | } 567 | 568 | // 智能威胁评分算法 569 | calculateRiskScore(rule: ThreatRule, log: LogEntry): number { 570 | let score = rule.severity === 'critical' ? 100 : 571 | rule.severity === 'high' ? 80 : 572 | rule.severity === 'medium' ? 60 : 40 573 | 574 | // 状态码加权 575 | if (log.statusCode === 200) score *= 1.5 // 成功攻击 576 | else if (log.statusCode >= 400) score *= 0.7 // 失败攻击 577 | 578 | // 响应大小加权 579 | if (log.responseSize > 10000) score *= 1.3 // 可能数据泄露 580 | 581 | // 时间加权 582 | const hour = new Date(log.timestamp).getHours() 583 | if (hour < 6 || hour > 22) score *= 1.2 // 非工作时间 584 | 585 | return Math.min(score, 100) 586 | } 587 | ``` 588 | 589 | ### 🔧 高度自定义规则系统 590 | 591 | **可视化规则配置界面**: 592 | ```vue 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 严重 601 | 高危 602 | 中危 603 | 低危 604 | 605 | 606 | 607 | 608 | 609 | 610 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | ``` 631 | 632 | **规则导入导出功能**: 633 | ```typescript 634 | // 批量规则管理 635 | export class RuleManager { 636 | async exportRules(): Promise { 637 | const rules = await rulesRepo.getAll() 638 | return JSON.stringify(rules, null, 2) 639 | } 640 | 641 | async importRules(rulesJson: string): Promise { 642 | const rules = JSON.parse(rulesJson) 643 | await Promise.all( 644 | rules.map(rule => rulesRepo.add(rule)) 645 | ) 646 | } 647 | 648 | async batchUpdate(ruleIds: string[], patch: Partial): Promise { 649 | await Promise.all( 650 | ruleIds.map(id => rulesRepo.update(id, patch)) 651 | ) 652 | } 653 | } 654 | ``` 655 | 656 | --- 657 | 658 | ## 三、先进的地理可视化系统 659 | 660 | ### 🌍 技术架构与数据源 661 | 662 | **地理数据库**: 663 | - **MaxMind GeoLite2**: 全球最权威的IP地理位置数据库 664 | - `GeoLite2-City.mmdb`: 城市级精度定位 665 | - `GeoLite2-Country.mmdb`: 国家级定位 666 | - `GeoLite2-ASN.mmdb`: 网络运营商信息 667 | 668 | **可视化技术栈**: 669 | - **ECharts 5.0+**: 高性能图表库 670 | - **地图数据**: 世界地图 + 中国地图矢量数据 671 | - **动画引擎**: Canvas 2D + 粒子系统 672 | - **交互优化**: 事件委托 + 防抖节流 673 | 674 | ### 🗺️ 双模式地图系统 675 | 676 | #### 1️⃣ 流量地图模式 677 | 678 | **技术实现**: 679 | ```typescript 680 | // 流量数据聚合器 681 | export class TrafficMapAggregator { 682 | async generateTrafficMap(logs: LogEntry[]): Promise { 683 | // 1. IP地理位置批量查询 684 | const uniqueIPs = [...new Set(logs.map(log => log.ip))] 685 | const locations = await this.batchGeoQuery(uniqueIPs) 686 | 687 | // 2. 按地理位置聚合流量 688 | const trafficMap = new Map() 689 | 690 | logs.forEach(log => { 691 | const location = locations.get(log.ip) 692 | if (!location) return 693 | 694 | const key = `${location.coordinates}` 695 | const point = trafficMap.get(key) || { 696 | name: `${location.country}-${location.city}`, 697 | coordinates: location.coordinates.split(',').map(Number), 698 | value: 0, 699 | requests: 0, 700 | uniqueIPs: new Set(), 701 | ipDetails: [] 702 | } 703 | 704 | point.requests++ 705 | point.uniqueIPs.add(log.ip) 706 | point.value = point.uniqueIPs.size 707 | 708 | // 收集IP详情 709 | const ipDetail = point.ipDetails.find(ip => ip.address === log.ip) 710 | if (ipDetail) { 711 | ipDetail.count++ 712 | } else { 713 | point.ipDetails.push({ 714 | address: log.ip, 715 | count: 1, 716 | lastAccess: log.timestamp 717 | }) 718 | } 719 | 720 | trafficMap.set(key, point) 721 | }) 722 | 723 | return { 724 | points: Array.from(trafficMap.values()), 725 | maxValue: Math.max(...Array.from(trafficMap.values()).map(p => p.value)) 726 | } 727 | } 728 | } 729 | ``` 730 | 731 | **可视化特性**: 732 | - **热力图渲染**: 访问密度颜色映射 733 | - **多主题支持**: 适配3种UI主题风格 734 | - **实时更新**: 数据变化时动态更新地图 735 | 736 | #### 2️⃣ 攻击地图模式 737 | 738 | **动画技术实现**: 739 | ```typescript 740 | // 攻击流向动画系统 741 | export class AttackFlowAnimator { 742 | private particles: Particle[] = [] 743 | private animationId: number = 0 744 | 745 | startAttackAnimation(attacks: AttackFlow[]) { 746 | attacks.forEach(attack => { 747 | // 创建粒子从攻击源到目标 748 | const particle = new Particle({ 749 | start: attack.sourceCoordinates, 750 | end: attack.targetCoordinates, 751 | color: this.getThreatColor(attack.severity), 752 | speed: this.getSpeedByThreat(attack.severity), 753 | trail: true 754 | }) 755 | 756 | this.particles.push(particle) 757 | }) 758 | 759 | this.animate() 760 | } 761 | 762 | private animate() { 763 | this.particles.forEach(particle => { 764 | particle.update() 765 | particle.render(this.canvas) 766 | }) 767 | 768 | // 移除完成的粒子 769 | this.particles = this.particles.filter(p => !p.isComplete) 770 | 771 | if (this.particles.length > 0) { 772 | this.animationId = requestAnimationFrame(() => this.animate()) 773 | } 774 | } 775 | } 776 | ``` 777 | 778 | **视觉效果**: 779 | - **流向动画**: 从攻击源到防守点的动态粒子流 780 | - **威胁等级**: 颜色编码表示威胁严重程度 781 | - **实时统计**: 攻击频次和强度实时更新 782 | 783 | ### 🎯 交互式地图功能 784 | 785 | #### 节点详情弹窗 786 | ```typescript 787 | // 地图节点点击事件 788 | onMapNodeClick(params: any) { 789 | const cityData = this.getCityData(params.name) 790 | 791 | // 弹出详情框 792 | this.showCityDetail({ 793 | city: cityData.city, 794 | country: cityData.country, 795 | statistics: { 796 | totalRequests: cityData.requests, 797 | uniqueIPs: cityData.uniqueIPs.size, 798 | threatCount: cityData.threats.length 799 | }, 800 | ipList: cityData.ipDetails.map(ip => ({ 801 | address: ip.address, 802 | requestCount: ip.count, 803 | lastAccess: ip.lastAccess, 804 | threatLevel: this.getIPThreatLevel(ip.address) 805 | })) 806 | }) 807 | } 808 | ``` 809 | 810 | **功能示例**: 811 | ``` 812 | 🏙️ 东莞市节点详情 813 | ┌─────────────────────────────────┐ 814 | │ 📊 访问统计: │ 815 | │ • 总访问: 1,234次 │ 816 | │ • 独立IP: 45个 │ 817 | │ • 威胁数: 12次 (🔴 需关注) │ 818 | │ │ 819 | │ 📋 TOP IP列表: │ 820 | │ • 192.168.1.100 (456次) 🟢 │ 821 | │ • 10.0.0.50 (234次) 🟡 │ 822 | │ • 172.16.0.25 (189次) 🔴 │ 823 | │ │ 824 | └─────────────────────────────────┘ 825 | ``` 826 | 827 | ### 🌏 双地图支持 828 | 829 | **世界地图**: 830 | - 全球威胁态势展示 831 | - 跨国攻击路径分析 832 | - 地缘政治安全分析 833 | 834 | **中国地图**: 835 | - 省市级精度展示 836 | - 国内流量分布分析 837 | - 区域安全态势监控 838 | 839 | --- 840 | 841 | ## 三、多模式数据存储架构 842 | 843 | 844 | ### 🔄 无缝模式切换 845 | 846 | ```typescript 847 | // 数据模式抽象层 848 | export abstract class DataModeAdapter { 849 | abstract async getAllLogEntries(): Promise 850 | abstract async searchLogs(query: SearchQuery): Promise 851 | abstract async getStatistics(): Promise 852 | } 853 | 854 | // 本地内存模式 855 | export class LocalMemoryAdapter extends DataModeAdapter { 856 | private logs: LogEntry[] = [] 857 | private index: Map = new Map() 858 | 859 | async getAllLogEntries(): Promise { 860 | return this.logs 861 | } 862 | 863 | async searchLogs(query: SearchQuery): Promise { 864 | // 基于内存索引的快速搜索 865 | if (query.text) { 866 | return this.index.get(query.text) || [] 867 | } 868 | return this.logs.filter(log => this.matchesQuery(log, query)) 869 | } 870 | } 871 | 872 | // Redis高性能模式 873 | export class RedisAdapter extends DataModeAdapter { 874 | private client: Redis 875 | 876 | async getAllLogEntries(): Promise { 877 | const keys = await this.client.keys('log:*') 878 | const pipeline = this.client.pipeline() 879 | keys.forEach(key => pipeline.get(key)) 880 | const results = await pipeline.exec() 881 | 882 | return results.map(result => JSON.parse(result[1] as string)) 883 | } 884 | 885 | async searchLogs(query: SearchQuery): Promise { 886 | // 使用Redis的搜索功能 887 | const searchKey = `search:${JSON.stringify(query)}` 888 | const cached = await this.client.get(searchKey) 889 | 890 | if (cached) { 891 | return JSON.parse(cached) 892 | } 893 | 894 | // 执行搜索并缓存结果 895 | const results = await this.performSearch(query) 896 | await this.client.setex(searchKey, 300, JSON.stringify(results)) 897 | 898 | return results 899 | } 900 | } 901 | 902 | // Zinc搜索模式 (ElasticSearch兼容) 903 | export class ZincAdapter extends DataModeAdapter { 904 | private client: ZincClient 905 | 906 | async searchLogs(query: SearchQuery): Promise { 907 | const searchBody = { 908 | query: { 909 | bool: { 910 | must: [ 911 | query.text ? { match: { content: query.text } } : { match_all: {} }, 912 | ...(query.ip ? [{ term: { ip: query.ip } }] : []), 913 | ...(query.statusCode ? [{ term: { statusCode: query.statusCode } }] : []) 914 | ], 915 | filter: [ 916 | ...(query.timeRange ? [{ 917 | range: { 918 | timestamp: { 919 | gte: query.timeRange.start, 920 | lte: query.timeRange.end 921 | } 922 | } 923 | }] : []) 924 | ] 925 | } 926 | }, 927 | sort: [{ timestamp: { order: 'desc' } }], 928 | size: query.limit || 1000 929 | } 930 | 931 | const response = await this.client.search('fastwlat-logs', searchBody) 932 | return response.hits.hits.map(hit => hit._source) 933 | } 934 | } 935 | ``` 936 | 937 | --- 938 | 939 | ## 🔧 核心技术库详解 940 | 941 | ### 前端技术栈 942 | 943 | #### Vue 3 Composition API 944 | ```typescript 945 | // 组件逻辑复用示例 946 | export function useLogView() { 947 | const logs = ref([]) 948 | const loading = ref(false) 949 | const selectedView = ref<'table' | 'tree' | 'aggregated'>('table') 950 | 951 | const filteredLogs = computed(() => { 952 | return logs.value.filter(log => { 953 | // 复杂的过滤逻辑 954 | return matchesFilters(log, filters.value) 955 | }) 956 | }) 957 | 958 | const loadLogs = async () => { 959 | loading.value = true 960 | try { 961 | logs.value = await getLogData() 962 | } finally { 963 | loading.value = false 964 | } 965 | } 966 | 967 | return { 968 | logs: readonly(logs), 969 | loading: readonly(loading), 970 | selectedView, 971 | filteredLogs, 972 | loadLogs 973 | } 974 | } 975 | ``` 976 | 977 | #### Pinia状态管理 978 | ```typescript 979 | // 主题状态管理 980 | export const useThemeStore = defineStore('theme', () => { 981 | const currentTheme = ref('styleB') 982 | 983 | const themes: Record = { 984 | styleA: { 985 | name: 'styleA', 986 | label: '风格A', 987 | colors: { 988 | primary: '#0ea5e9', 989 | background: '#0f172a', 990 | // ... 完整颜色配置 991 | }, 992 | classes: { 993 | background: 'bg-gradient-to-br from-sky-900 via-slate-900 to-blue-900', 994 | cardBackground: 'bg-gradient-to-br from-sky-800/85 via-slate-800/85 to-blue-800/85 backdrop-blur-sm', 995 | // ... 完整样式类 996 | } 997 | } 998 | // ... 其他主题 999 | } 1000 | 1001 | const setTheme = (themeName: ThemeMode) => { 1002 | currentTheme.value = themeName 1003 | updateCSSVariables() 1004 | localStorage.setItem('app-theme', themeName) 1005 | } 1006 | 1007 | const updateCSSVariables = () => { 1008 | const root = document.documentElement 1009 | const theme = themes[currentTheme.value] 1010 | 1011 | Object.entries(theme.colors).forEach(([key, value]) => { 1012 | root.style.setProperty(`--color-${key}`, value) 1013 | }) 1014 | } 1015 | 1016 | return { currentTheme, themes, setTheme, updateCSSVariables } 1017 | }) 1018 | ``` 1019 | 1020 | ### 技术架构 1021 | 1022 | #### 技术栈 1023 | 1024 | ```javascript 1025 | { 1026 | "前端": "Vue 3 + TypeScript + Tailwind CSS", 1027 | "桌面": "Electron 35.7.0", 1028 | "构建": "Vite 6.3.5 + Electron-Vite", 1029 | "状态管理": "Pinia", 1030 | "数据库": "Dexie.js (IndexedDB) + Redis", 1031 | "地理位置": "MaxMind GeoLite2", 1032 | "图表": "ECharts", 1033 | "多线程": "Web Workers" 1034 | } 1035 | ``` 1036 | 1037 | #### 项目结构 1038 | 1039 | ``` 1040 | FastWLAT/ 1041 | ├── src/ 1042 | │ ├── main/ # Electron主进程 1043 | │ ├── renderer/ # Vue 3渲染进程 1044 | │ │ ├── pages/ # 8个核心页面 1045 | │ │ ├── components/ # 可复用组件 1046 | │ │ ├── stores/ # Pinia状态管理 1047 | │ │ └── services/ # 业务逻辑服务 1048 | │ └── preload/ # 预加载脚本 1049 | ├── docs/ # 项目文档 1050 | └── scripts/ # 构建脚本 1051 | ``` 1052 | 1053 | 1054 | 1055 | #### Electron主进程 1056 | ```typescript 1057 | // 主进程 - 窗口管理和服务 1058 | import { app, BrowserWindow, ipcMain } from 'electron' 1059 | import { IPGeoLocationService } from './ipGeoLocationService' 1060 | import { RedisService } from './redisService' //未开放 1061 | 1062 | class MainProcess { 1063 | private mainWindow: BrowserWindow | null = null 1064 | private geoService: IPGeoLocationService 1065 | private redisService: RedisService 1066 | 1067 | constructor() { 1068 | this.geoService = new IPGeoLocationService() 1069 | this.redisService = new RedisService() 1070 | this.setupIPC() 1071 | } 1072 | 1073 | createWindow() { 1074 | this.mainWindow = new BrowserWindow({ 1075 | width: 1600, // 优化窗口尺寸 1076 | height: 1000, 1077 | minWidth: 1400, 1078 | minHeight: 900, 1079 | title: 'FastWLAT - Web日志分析工具 v1.0.0', 1080 | webPreferences: { 1081 | preload: join(__dirname, '../preload/index.js'), 1082 | sandbox: false 1083 | } 1084 | }) 1085 | } 1086 | 1087 | private setupIPC() { 1088 | // 地理位置查询IPC 1089 | ipcMain.handle('geo:batch-query', async (event, ips: string[]) => { 1090 | return await this.geoService.batchQuery(ips) 1091 | }) 1092 | 1093 | // Redis操作IPC 1094 | ipcMain.handle('redis:connect', async (event, config) => { 1095 | return await this.redisService.connect(config) 1096 | }) 1097 | } 1098 | } 1099 | ``` 1100 | 1101 | #### 地理位置服务 1102 | ```typescript 1103 | // IP地理位置服务 - 高性能批量查询 1104 | export class IPGeoLocationService { 1105 | private cityReader: Reader 1106 | private countryReader: Reader 1107 | private asnReader: Reader 1108 | private cache = new Map() 1109 | 1110 | async batchQuery(ips: string[]): Promise> { 1111 | const results = new Map() 1112 | const uncachedIPs: string[] = [] 1113 | 1114 | // 1. 检查缓存 1115 | ips.forEach(ip => { 1116 | const cached = this.cache.get(ip) 1117 | if (cached) { 1118 | results.set(ip, cached) 1119 | } else { 1120 | uncachedIPs.push(ip) 1121 | } 1122 | }) 1123 | 1124 | // 2. 批量查询未缓存的IP 1125 | const batchSize = 100 1126 | for (let i = 0; i < uncachedIPs.length; i += batchSize) { 1127 | const batch = uncachedIPs.slice(i, i + batchSize) 1128 | 1129 | await Promise.all(batch.map(async ip => { 1130 | try { 1131 | const location = await this.querySingle(ip) 1132 | this.cache.set(ip, location) 1133 | results.set(ip, location) 1134 | } catch (error) { 1135 | console.warn(`Failed to query IP ${ip}:`, error) 1136 | results.set(ip, this.getDefaultLocation()) 1137 | } 1138 | })) 1139 | } 1140 | 1141 | return results 1142 | } 1143 | 1144 | private async queryBingle(ip: string): Promise { 1145 | // 并行查询三个数据库 1146 | const [cityResult, countryResult, asnResult] = await Promise.all([ 1147 | this.cityReader.city(ip).catch(() => null), 1148 | this.countryReader.country(ip).catch(() => null), 1149 | this.asnReader.asn(ip).catch(() => null) 1150 | ]) 1151 | 1152 | return { 1153 | country: cityResult?.country?.names?.zh_CN || 1154 | countryResult?.country?.names?.zh_CN || '未知国家', 1155 | region: cityResult?.subdivisions?.[0]?.names?.zh_CN || '未知地区', 1156 | city: cityResult?.city?.names?.zh_CN || '未知城市', 1157 | coordinates: cityResult?.location ? 1158 | `${cityResult.location.latitude}, ${cityResult.location.longitude}` : 1159 | '0, 0', 1160 | asn: asnResult?.autonomous_system_organization || '未知ISP', 1161 | source: this.getDataSource(cityResult, countryResult, asnResult) 1162 | } 1163 | } 1164 | } 1165 | ``` 1166 | 1167 | --- 1168 | 1169 | ## 🚀 性能优化 1170 | 1171 | ### 🔧 Web Workers多线程架构 1172 | 1173 | ```typescript 1174 | // 工作线程管理器 1175 | export class WorkerManager { 1176 | private workers: Worker[] = [] 1177 | private taskQueue: ParseTask[] = [] 1178 | private maxWorkers = navigator.hardwareConcurrency || 4 1179 | 1180 | constructor() { 1181 | this.initializeWorkers() 1182 | } 1183 | 1184 | private initializeWorkers() { 1185 | for (let i = 0; i < this.maxWorkers; i++) { 1186 | const worker = new Worker(new URL('./parseWorker.ts', import.meta.url)) 1187 | worker.onmessage = this.handleWorkerMessage.bind(this) 1188 | this.workers.push(worker) 1189 | } 1190 | } 1191 | 1192 | async parseFile(content: string, format: LogFormat): Promise { 1193 | // 智能分块策略 1194 | const chunks = this.splitIntoOptimalChunks(content) 1195 | 1196 | // 并行处理 1197 | const results = await Promise.all( 1198 | chunks.map((chunk, index) => 1199 | this.assignToWorker(chunk, format, index) 1200 | ) 1201 | ) 1202 | 1203 | // 合并结果并排序 1204 | return results.flat().sort((a, b) => 1205 | new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() 1206 | ) 1207 | } 1208 | 1209 | private splitIntoOptimalChunks(content: string): string[] { 1210 | const lines = content.split('\n') 1211 | const chunkSize = Math.ceil(lines.length / this.maxWorkers) 1212 | const chunks: string[] = [] 1213 | 1214 | for (let i = 0; i < lines.length; i += chunkSize) { 1215 | chunks.push(lines.slice(i, i + chunkSize).join('\n')) 1216 | } 1217 | 1218 | return chunks 1219 | } 1220 | } 1221 | ``` 1222 | 1223 | ### 🧠 智能缓存策略 1224 | 1225 | ```typescript 1226 | // 多层缓存架构 1227 | export class CacheManager { 1228 | private memoryCache = new Map() 1229 | private persistentCache: IDBPDatabase 1230 | private readonly CACHE_DURATION = 5 * 60 * 1000 // 5分钟 1231 | 1232 | async get(key: string): Promise { 1233 | // 1. 检查内存缓存 1234 | const memoryItem = this.memoryCache.get(key) 1235 | if (memoryItem && !this.isExpired(memoryItem)) { 1236 | return memoryItem.data 1237 | } 1238 | 1239 | // 2. 检查持久化缓存 1240 | const persistentItem = await this.persistentCache 1241 | .get('cache', key) 1242 | .catch(() => null) 1243 | 1244 | if (persistentItem && !this.isExpired(persistentItem)) { 1245 | // 回写到内存缓存 1246 | this.memoryCache.set(key, persistentItem) 1247 | return persistentItem.data 1248 | } 1249 | 1250 | return null 1251 | } 1252 | 1253 | async set(key: string, data: T, ttl?: number): Promise { 1254 | const item: CacheItem = { 1255 | data, 1256 | timestamp: Date.now(), 1257 | ttl: ttl || this.CACHE_DURATION 1258 | } 1259 | 1260 | // 同时写入内存和持久化缓存 1261 | this.memoryCache.set(key, item) 1262 | await this.persistentCache.put('cache', item, key) 1263 | } 1264 | } 1265 | ``` 1266 | 1267 | ### ⚡ 虚拟化渲染优化 1268 | 1269 | ```typescript 1270 | // 虚拟滚动优化算法 1271 | export function useVirtualScroll( 1272 | items: Ref, 1273 | itemHeight: number, 1274 | containerHeight: number 1275 | ) { 1276 | const scrollTop = ref(0) 1277 | const overscan = 5 // 预渲染项目数 1278 | 1279 | const visibleRange = computed(() => { 1280 | const start = Math.max(0, Math.floor(scrollTop.value / itemHeight) - overscan) 1281 | const visibleCount = Math.ceil(containerHeight / itemHeight) 1282 | const end = Math.min(items.value.length, start + visibleCount + overscan * 2) 1283 | 1284 | return { start, end } 1285 | }) 1286 | 1287 | const visibleItems = computed(() => { 1288 | const { start, end } = visibleRange.value 1289 | return items.value.slice(start, end).map((item, index) => ({ 1290 | item, 1291 | index: start + index, 1292 | top: (start + index) * itemHeight 1293 | })) 1294 | }) 1295 | 1296 | const totalHeight = computed(() => items.value.length * itemHeight) 1297 | 1298 | return { 1299 | visibleItems, 1300 | totalHeight, 1301 | scrollTop, 1302 | visibleRange 1303 | } 1304 | } 1305 | ``` 1306 | 1307 | --- 1308 | 1309 | 1310 | 1311 | ## 🎯 工具亮点总结 1312 | 1313 | ### 🏆 核心创新点 1314 | 1315 | 1. **🌳 树状视图技术**: 1316 | - Trie树算法构建URL层次结构 1317 | - 虚拟化渲染支持大节点 1318 | - 交互式详情分析和快速定位 1319 | 1320 | 2. **🛡️ 多维度威胁检测**: 1321 | - 状态码+内容的组合判断 1322 | - 误报率降低50% 1323 | - 高度可定制的规则引擎 1324 | 1325 | 3. **🗺️ 地理可视化系统**: 1326 | - MaxMind数据库批量查询优化 1327 | - 双模式地图 (流量+攻击) 1328 | - 交互式节点详情展示 1329 | 1330 | 4. **⚡ 多模式存储架构**: 1331 | - 本地/IndexedDB/Zinc三模式 1332 | - 运行时无缝切换 1333 | - 性能与成本的完美平衡 1334 | 1335 | 5. **🎨 现代化用户体验**: 1336 | - CSS变量驱动的主题系统 1337 | - 响应式设计适配大屏 1338 | - Web Workers多线程优化 1339 | 1340 | 1341 | 1342 | --- 1343 | 1344 | ## 🎉 结语 1345 | 1346 | FastWLAT不仅仅是一个日志分析工具,更是我们对Web日志分析工具的方法和技巧的思考和实践结果。通过创新、高度自定义的功能实现和友好的用户界面,希望能够真正解决一部分WEB日志分析的痛点,进一步降低WEB日志分析的门槛。 1347 | 1348 | 程序还在不断完善中,目前版本可能存在较多的BUG,欢迎各位贡献告警规则和提出贴合实战改进建议,在程序开源前都会进行持续的优化。由于图形界面和Tree视图的影响,目前在处理大日志文件时存在一些问题,后续版本会开放Zinc*(ES兼容)模式,支持更大体量的日志导入分析。 1349 | --------------------------------------------------------------------------------